Manage display of book covers

This commit is contained in:
2025-10-28 18:35:36 +01:00
parent 8b8eee8210
commit b4df375e4c
20 changed files with 257 additions and 353 deletions

306
front/package-lock.json generated
View File

@@ -23,7 +23,7 @@
"globals": "^16.3.0", "globals": "^16.3.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"vite": "^7.0.6", "vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0" "vite-plugin-vue-devtools": ">=8.0.3"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
@@ -1577,26 +1577,6 @@
"win32" "win32"
] ]
}, },
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1738,14 +1718,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/devtools-core": { "node_modules/@vue/devtools-core": {
"version": "8.0.1", "version": "8.0.3",
"resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.0.3.tgz",
"integrity": "sha512-Lf/+ambV3utWJ18r5TnpePbJ60IcIcqeZSQYLyNcFw2sFel0tGMnMyCdDtR1JNIdVZGAVaksTLhGh0FlrNu+sw==", "integrity": "sha512-gCEQN7aMmeaigEWJQ2Z2o3g7/CMqGTPvNS1U3n/kzpLoAZ1hkAHNgi4ml/POn/9uqGILBk65GGOUdrraHXRj5Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-kit": "^8.0.1", "@vue/devtools-kit": "^8.0.3",
"@vue/devtools-shared": "^8.0.1", "@vue/devtools-shared": "^8.0.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"pathe": "^2.0.3", "pathe": "^2.0.3",
@@ -1756,9 +1736,9 @@
} }
}, },
"node_modules/@vue/devtools-core/node_modules/nanoid": { "node_modules/@vue/devtools-core/node_modules/nanoid": {
"version": "5.1.5", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1775,25 +1755,32 @@
} }
}, },
"node_modules/@vue/devtools-kit": { "node_modules/@vue/devtools-kit": {
"version": "8.0.1", "version": "8.0.3",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.3.tgz",
"integrity": "sha512-7kiPhgTKNtNeXltEHnJJjIDlndlJP4P+UJvCw54uVHNDlI6JzwrSiRmW4cxKTug2wDbc/dkGaMnlZghcwV+aWA==", "integrity": "sha512-UF4YUOVGdfzXLCv5pMg2DxocB8dvXz278fpgEE+nJ/DRALQGAva7sj9ton0VWZ9hmXw+SV8yKMrxP2MpMhq9Wg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-shared": "^8.0.1", "@vue/devtools-shared": "^8.0.3",
"birpc": "^2.5.0", "birpc": "^2.6.1",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^2.0.0",
"speakingurl": "^14.0.1", "speakingurl": "^14.0.1",
"superjson": "^2.2.2" "superjson": "^2.2.2"
} }
}, },
"node_modules/@vue/devtools-kit/node_modules/perfect-debounce": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz",
"integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==",
"dev": true,
"license": "MIT"
},
"node_modules/@vue/devtools-shared": { "node_modules/@vue/devtools-shared": {
"version": "8.0.1", "version": "8.0.3",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.3.tgz",
"integrity": "sha512-PqtWqPPRpMwZ9FjTzyugb5KeV9kmg2C3hjxZHwjl0lijT4QIJDd0z6AWcnbM9w2nayjDymyTt0+sbdTv3pVeNg==", "integrity": "sha512-s/QNll7TlpbADFZrPVsaUNPCOF8NvQgtgmmB7Tip6pLf/HcOvBTly0lfLQ0Eylu9FQ4OqBhFpLyBgwykiSf8zw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1947,9 +1934,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/birpc": { "node_modules/birpc": {
"version": "2.5.0", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz",
"integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==", "integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
@@ -2560,33 +2547,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/execa": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz",
"integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"cross-spawn": "^7.0.6",
"figures": "^6.1.0",
"get-stream": "^9.0.0",
"human-signals": "^8.0.1",
"is-plain-obj": "^4.1.0",
"is-stream": "^4.0.1",
"npm-run-path": "^6.0.0",
"pretty-ms": "^9.2.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^4.0.0",
"yoctocolors": "^2.1.1"
},
"engines": {
"node": "^18.19.0 || >=20.5.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2633,22 +2593,6 @@
} }
} }
}, },
"node_modules/figures": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-unicode-supported": "^2.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -2725,23 +2669,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sec-ant/readable-stream": "^0.4.1",
"is-stream": "^4.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2784,16 +2711,6 @@
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/human-signals": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
"integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2889,45 +2806,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-stream": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-what": { "node_modules/is-what": {
"version": "4.1.16", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
@@ -3171,36 +3049,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/npm-run-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^4.0.0",
"unicorn-magic": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-path/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nth-check": { "node_modules/nth-check": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -3303,19 +3151,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3504,22 +3339,6 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/pretty-ms": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
"integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
"dev": true,
"license": "MIT",
"dependencies": {
"parse-ms": "^4.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3636,19 +3455,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sirv": { "node_modules/sirv": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
@@ -3682,19 +3488,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/strip-final-newline": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -3789,19 +3582,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unplugin-utils": { "node_modules/unplugin-utils": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.0.tgz", "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.0.tgz",
@@ -4013,18 +3793,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite-plugin-vue-devtools": { "node_modules/vite-plugin-vue-devtools": {
"version": "8.0.1", "version": "8.0.3",
"resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.1.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.3.tgz",
"integrity": "sha512-ecm/Xvtg5xsFPfY7SJ38Zb6NfmVrHxBhLMk/3nm5ZDAd7n8Dk2BV8JBuq1L5wRMVfvCth01vtzJViZC9TAC6qg==", "integrity": "sha512-yIi3u31xUi28HcLlTpV0BvSLQHgZ2dA8Zqa59kWfIeMdHqbsunt6TCjq4wCNfOcGSju+E7qyHyI09EjRRFMbuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-core": "^8.0.1", "@vue/devtools-core": "^8.0.3",
"@vue/devtools-kit": "^8.0.1", "@vue/devtools-kit": "^8.0.3",
"@vue/devtools-shared": "^8.0.1", "@vue/devtools-shared": "^8.0.3",
"execa": "^9.6.0", "sirv": "^3.0.2",
"sirv": "^3.0.1", "vite-plugin-inspect": "^11.3.3",
"vite-plugin-inspect": "^11.3.0",
"vite-plugin-vue-inspector": "^5.3.2" "vite-plugin-vue-inspector": "^5.3.2"
}, },
"engines": { "engines": {
@@ -4208,19 +3987,6 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/yoctocolors": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
"integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
} }
} }
} }

View File

@@ -29,6 +29,6 @@
"globals": "^16.3.0", "globals": "^16.3.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"vite": "^7.0.6", "vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0" "vite-plugin-vue-devtools": ">=8.0.3"
} }
} }

View File

@@ -8,7 +8,8 @@
const book = ref({ const book = ref({
title: "", title: "",
author: "" author: "",
coverId: null
}); });
const errors = ref(null) const errors = ref(null)
const titleError = computed(() => { const titleError = computed(() => {
@@ -50,7 +51,7 @@
</div> </div>
<p v-if="authorError" class="help is-danger">{{authorError}}</p> <p v-if="authorError" class="help is-danger">{{authorError}}</p>
</div> </div>
<CoverUpload name="cover"/> <CoverUpload name="cover" @on-image-upload="(id) => book.coverId = id"/>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<button class="button is-link">{{$t('addbook.submit')}}</button> <button class="button is-link">{{$t('addbook.submit')}}</button>

View File

@@ -1,15 +1,17 @@
<script setup> <script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getImagePathOrDefault } from './api.js'
const props = defineProps({ const props = defineProps({
id: Number, id: Number,
title: String, title: String,
author: String, author: String,
imagePath: String, coverPath: String,
rating: Number, rating: Number,
read: Boolean read: Boolean
}); });
const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath === 'undefined') ? "defaultbook.png" : props.imagePath; const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath));
const router = useRouter(); const router = useRouter();
function openBook() { function openBook() {

View File

@@ -1,5 +1,6 @@
<script setup> <script setup>
import { getBook } from './api.js' import { computed } from 'vue'
import { getBook, getImagePathOrDefault } from './api.js'
import { onBeforeRouteUpdate } from 'vue-router' import { onBeforeRouteUpdate } from 'vue-router'
const props = defineProps({ const props = defineProps({
@@ -7,7 +8,7 @@
}); });
let { data, error } = getBook(props.id); let { data, error } = getBook(props.id);
const imagePathOrDefault = "../defaultbook.png" const imagePathOrDefault = computed(() => getImagePathOrDefault(data.value.coverPath));
onBeforeRouteUpdate(async (to, from) => { onBeforeRouteUpdate(async (to, from) => {
let res = getBook(to.params.id); let res = getBook(to.params.id);
data = res.data; data = res.data;

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref, computed } from 'vue'
import { postReadBook } from './api.js' import { postReadBook, getImagePathOrDefault } from './api.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter(); const router = useRouter();
@@ -10,9 +10,9 @@
title: String, title: String,
author: String, author: String,
id: Number, id: Number,
imagePath: String, coverPath: String,
}); });
const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath === 'undefined') ? "../defaultbook.png" : props.imagePath; const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath));
const error = ref(null) const error = ref(null)
async function onUserBookRead() { async function onUserBookRead() {

View File

@@ -2,6 +2,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { postImage } from './api.js' import { postImage } from './api.js'
const emit = defineEmits(['OnImageUpload'])
const props = defineProps({ const props = defineProps({
name: String name: String
}); });
@@ -12,10 +13,15 @@
function onFileChanged(e) { function onFileChanged(e) {
postImage(e.target.files[0]) postImage(e.target.files[0])
.then((res) => res.json()) .then((res) => res.json())
.then((json) => (imagePath.value = json["filepath"])) .then((json) => onJsonResult(json))
.catch((err) => (error.value = err["error"])); .catch((err) => (error.value = err["error"]));
} }
function onJsonResult(json) {
imagePath.value = json["filepath"];
emit('OnImageUpload', json["fileId"])
}
function unsetImage() { function unsetImage() {
imagePath.value = null; imagePath.value = null;
} }

View File

@@ -3,6 +3,11 @@ import { useAuthStore } from './auth.store.js'
const baseUrl = "http://localhost:8080" const baseUrl = "http://localhost:8080"
export function getImagePathOrDefault(path) {
return (path == "" || typeof path === 'undefined') ?
"../defaultbook.png" : "http://localhost:8080" + path;
}
function useFetch(url) { function useFetch(url) {
const data = ref(null); const data = ref(null);
const error = ref(null); const error = ref(null);

View File

@@ -14,9 +14,11 @@ import (
) )
type bookUserGet struct { type bookUserGet struct {
BookId uint `json:"id"`
Title string `json:"title" binding:"required,max=300"` Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"` Author string `json:"author" binding:"max=100"`
Rating int `json:"rating" binding:"min=0,max=10"` Rating int `json:"rating" binding:"min=0,max=10"`
Read bool `json:"read"`
} }
func TestGetBooksHandler_Demo(t *testing.T) { func TestGetBooksHandler_Demo(t *testing.T) {
@@ -35,6 +37,27 @@ func TestGetBooksHandler_Demo2(t *testing.T) {
assert.Equal(t, 2, len(books)) assert.Equal(t, 2, len(books))
} }
func TestGetBooksHandler_CheckOneBook(t *testing.T) {
router := testutils.TestSetup()
token := testutils.ConnectDemo2User(router)
books := testGetbooksHandler(t, router, token, 200)
var book bookUserGet
for _, b := range books {
if b.Title == "De sang-froid" {
book = b
}
}
assert.Equal(t,
bookUserGet{
BookId: 18,
Title: "De sang-froid",
Author: "Truman Capote",
Rating: 6,
Read: true,
}, book)
}
func testGetbooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int) []bookUserGet { func testGetbooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int) []bookUserGet {
req, _ := http.NewRequest("GET", "/mybooks", nil) req, _ := http.NewRequest("GET", "/mybooks", nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", userToken)) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", userToken))

View File

@@ -21,6 +21,7 @@ func Initdb(databasePath string, demoDataPath string) *gorm.DB {
db.AutoMigrate(&model.Book{}) db.AutoMigrate(&model.Book{})
db.AutoMigrate(&model.User{}) db.AutoMigrate(&model.User{})
db.AutoMigrate(&model.UserBook{}) db.AutoMigrate(&model.UserBook{})
db.AutoMigrate(&model.StaticFile{})
var book model.Book var book model.Book
queryResult := db.Limit(1).Find(&book) queryResult := db.Limit(1).Find(&book)
if queryResult.RowsAffected == 0 && demoDataPath != "" { if queryResult.RowsAffected == 0 && demoDataPath != "" {

View File

@@ -0,0 +1,42 @@
package fileutils
import (
"mime/multipart"
"path/filepath"
"strconv"
"git.artlef.fr/PersonalLibraryManager/internal/appcontext"
"git.artlef.fr/PersonalLibraryManager/internal/model"
)
func SaveStaticFile(ac *appcontext.AppContext, file *multipart.FileHeader) (model.StaticFile, error) {
filename := file.Filename
filepath := computePathFromName(ac, filename)
err := ac.C.SaveUploadedFile(file, ac.Config.ImageFolderPath+"/"+filepath)
if err != nil {
return model.StaticFile{}, err
}
staticFile := model.StaticFile{
Name: filename,
Path: filepath,
}
ac.Db.Save(&staticFile)
return staticFile, nil
}
func computePathFromName(ac *appcontext.AppContext, filename string) string {
var existingFiles []model.StaticFile
ac.Db.Where("name = ?", filename).Find(&existingFiles)
l := len(existingFiles)
if l == 0 {
return filename
} else {
extension := filepath.Ext(filename)
basename := filename[:len(filename)-len(extension)]
return basename + "-" + strconv.Itoa(l) + extension
}
}
func GetWsLinkPrefix() string {
return "/bookcover/"
}

View File

@@ -8,4 +8,6 @@ type Book struct {
Author string `json:"author"` Author string `json:"author"`
AddedBy User AddedBy User
AddedByID uint AddedByID uint
Cover StaticFile
CoverID uint
} }

9
internal/model/file.go Normal file
View File

@@ -0,0 +1,9 @@
package model
import "gorm.io/gorm"
type StaticFile struct {
gorm.Model
Name string `gorm:"not null"`
Path string `gorm:"not null;index;uniqueIndex"`
}

View File

@@ -18,6 +18,10 @@ type HttpError struct {
} }
func ValidateId(db *gorm.DB, id uint, value any) error { func ValidateId(db *gorm.DB, id uint, value any) error {
//id = 0 means empty id, so no check
if id == 0 {
return nil
}
record := map[string]any{} record := map[string]any{}
result := db.Model(value).First(&record, id) result := db.Model(value).First(&record, id)
if result.Error == nil { if result.Error == nil {

69
internal/query/query.go Normal file
View File

@@ -0,0 +1,69 @@
package query
import (
"strings"
"git.artlef.fr/PersonalLibraryManager/internal/fileutils"
"git.artlef.fr/PersonalLibraryManager/internal/model"
"gorm.io/gorm"
)
type BookGet struct {
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
Rating int `json:"rating"`
Read bool `json:"read"`
CoverPath string `json:"coverPath"`
}
func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (BookGet, error) {
var book BookGet
query := db.Model(&model.Book{})
query = query.Select("books.title, books.author, user_books.rating, user_books.read, " + selectStaticFilesPath())
query = query.Joins("left join user_books on (user_books.book_id = books.id and user_books.user_id = ?)", userId)
query = query.Joins("left join static_files on (static_files.id = books.cover_id)")
query = query.Where("books.id = ?", bookId)
res := query.First(&book)
return book, res.Error
}
type BookSearchGet struct {
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
ID uint `json:"id"`
CoverPath string `json:"coverPath"`
}
func FetchBookSearchGet(db *gorm.DB, searchterm string) ([]BookSearchGet, error) {
var books []BookSearchGet
query := db.Model(&model.Book{})
query = query.Select("books.title, books.author, books.id," + selectStaticFilesPath())
query = query.Joins("left join static_files on (static_files.id = books.cover_id)")
query = query.Where("LOWER(title) LIKE ?", "%"+strings.ToLower(searchterm)+"%")
res := query.Find(&books)
return books, res.Error
}
type BookUserGet struct {
ID uint `json:"id"`
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
Rating int `json:"rating" binding:"min=0,max=10"`
Read bool `json:"read" binding:"boolean"`
CoverPath string `json:"coverPath"`
}
func FetchBookUserGet(db *gorm.DB, userId uint) ([]BookUserGet, error) {
var books []BookUserGet
query := db.Model(&model.UserBook{})
query = query.Select("books.id, books.title, books.author, user_books.rating, user_books.read," + selectStaticFilesPath())
query = query.Joins("left join books on (books.id = user_books.book_id)")
query = query.Joins("left join static_files on (static_files.id = books.cover_id)")
query = query.Where("user_id = ?", userId)
res := query.Find(&books)
return books, res.Error
}
func selectStaticFilesPath() string {
return "(CASE COALESCE(static_files.path, '') WHEN '' THEN '' ELSE concat('" + fileutils.GetWsLinkPrefix() + "', static_files.path) END) as CoverPath"
}

View File

@@ -7,16 +7,10 @@ import (
"git.artlef.fr/PersonalLibraryManager/internal/appcontext" "git.artlef.fr/PersonalLibraryManager/internal/appcontext"
"git.artlef.fr/PersonalLibraryManager/internal/model" "git.artlef.fr/PersonalLibraryManager/internal/model"
"git.artlef.fr/PersonalLibraryManager/internal/myvalidator" "git.artlef.fr/PersonalLibraryManager/internal/myvalidator"
"git.artlef.fr/PersonalLibraryManager/internal/query"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type bookGet struct {
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
Rating int `json:"rating"`
Read bool `json:"read"`
}
func GetBookHandler(ac appcontext.AppContext) { func GetBookHandler(ac appcontext.AppContext) {
bookId, err := strconv.ParseUint(ac.C.Param("id"), 10, 64) bookId, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
if err != nil { if err != nil {
@@ -33,13 +27,8 @@ func GetBookHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return return
} }
var book bookGet book, queryErr := query.FetchBookGet(ac.Db, user.ID, bookId)
query := ac.Db.Model(&model.Book{}) if queryErr != nil {
query = query.Select("books.title, books.author, user_books.rating, user_books.read")
query = query.Joins("left join user_books on (user_books.book_id = books.id and user_books.user_id = ?)", user.ID)
query = query.Where("books.id = ?", bookId)
res := query.First(&book)
if res.Error != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return return
} }

View File

@@ -9,6 +9,7 @@ import (
type bookPostCreate struct { type bookPostCreate struct {
Title string `json:"title" binding:"required,max=300"` Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"` Author string `json:"author" binding:"max=100"`
CoverID uint `json:"coverId"`
} }
func PostBookHandler(ac appcontext.AppContext) { func PostBookHandler(ac appcontext.AppContext) {
@@ -18,6 +19,11 @@ func PostBookHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return return
} }
err = myvalidator.ValidateId(ac.Db, book.CoverID, &model.StaticFile{})
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
user, fetchUserErr := ac.GetAuthenticatedUser() user, fetchUserErr := ac.GetAuthenticatedUser()
if fetchUserErr != nil { if fetchUserErr != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
@@ -33,9 +39,13 @@ func PostBookHandler(ac appcontext.AppContext) {
} }
func bookWsToDb(b bookPostCreate, user *model.User) model.Book { func bookWsToDb(b bookPostCreate, user *model.User) model.Book {
return model.Book{ book := model.Book{
Title: b.Title, Title: b.Title,
Author: b.Author, Author: b.Author,
AddedBy: *user, AddedBy: *user,
} }
if b.CoverID > 0 {
book.CoverID = b.CoverID
}
return book
} }

View File

@@ -2,33 +2,18 @@ package routes
import ( import (
"net/http" "net/http"
"strings"
"git.artlef.fr/PersonalLibraryManager/internal/appcontext" "git.artlef.fr/PersonalLibraryManager/internal/appcontext"
"git.artlef.fr/PersonalLibraryManager/internal/model" "git.artlef.fr/PersonalLibraryManager/internal/myvalidator"
"git.artlef.fr/PersonalLibraryManager/internal/query"
) )
type bookSearchGet struct {
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
ID uint `json:"id"`
}
func GetSearchBooksHandler(ac appcontext.AppContext) { func GetSearchBooksHandler(ac appcontext.AppContext) {
searchterm := ac.C.Param("searchterm") searchterm := ac.C.Param("searchterm")
var booksDb []model.Book books, err := query.FetchBookSearchGet(ac.Db, searchterm)
ac.Db.Where("LOWER(title) LIKE ?", "%"+strings.ToLower(searchterm)+"%").Find(&booksDb) if err != nil {
books := make([]bookSearchGet, 0) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
for _, b := range booksDb { return
books = append(books, bookDbToWs(&b))
} }
ac.C.JSON(http.StatusOK, books) ac.C.JSON(http.StatusOK, books)
} }
func bookDbToWs(b *model.Book) bookSearchGet {
return bookSearchGet{
Title: b.Title,
Author: b.Author,
ID: b.ID,
}
}

View File

@@ -4,39 +4,16 @@ import (
"net/http" "net/http"
"git.artlef.fr/PersonalLibraryManager/internal/appcontext" "git.artlef.fr/PersonalLibraryManager/internal/appcontext"
"git.artlef.fr/PersonalLibraryManager/internal/model"
"git.artlef.fr/PersonalLibraryManager/internal/myvalidator" "git.artlef.fr/PersonalLibraryManager/internal/myvalidator"
"git.artlef.fr/PersonalLibraryManager/internal/query"
) )
type bookUserGet struct {
BookID uint `json:"id"`
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
Rating int `json:"rating" binding:"min=0,max=10"`
Read bool `json:"read" binding:"boolean"`
}
func GetMyBooksHanderl(ac appcontext.AppContext) { func GetMyBooksHanderl(ac appcontext.AppContext) {
var userbooks []model.UserBook
user, err := ac.GetAuthenticatedUser() user, err := ac.GetAuthenticatedUser()
if err != nil { if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return return
} }
ac.Db.Preload("Book").Where("user_id = ?", user.ID).Find(&userbooks) userbooks, err := query.FetchBookUserGet(ac.Db, user.ID)
booksDto := make([]bookUserGet, 0) ac.C.JSON(http.StatusOK, userbooks)
for _, userbook := range userbooks {
booksDto = append(booksDto, userBookDbToWs(&userbook))
}
ac.C.JSON(http.StatusOK, booksDto)
}
func userBookDbToWs(b *model.UserBook) bookUserGet {
return bookUserGet{
BookID: b.BookID,
Title: b.Book.Title,
Author: b.Book.Author,
Rating: b.Rating,
Read: b.Read,
}
} }

View File

@@ -4,21 +4,33 @@ import (
"net/http" "net/http"
"git.artlef.fr/PersonalLibraryManager/internal/appcontext" "git.artlef.fr/PersonalLibraryManager/internal/appcontext"
"git.artlef.fr/PersonalLibraryManager/internal/fileutils"
"git.artlef.fr/PersonalLibraryManager/internal/model"
"git.artlef.fr/PersonalLibraryManager/internal/myvalidator" "git.artlef.fr/PersonalLibraryManager/internal/myvalidator"
"github.com/gin-gonic/gin"
) )
type fileInfoPost struct {
FileID uint `json:"fileId"`
FilePath string `json:"filepath"`
}
func PostUploadBookCoverHandler(ac appcontext.AppContext) { func PostUploadBookCoverHandler(ac appcontext.AppContext) {
file, err := ac.C.FormFile("file") file, err := ac.C.FormFile("file")
if err != nil { if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return return
} }
filepath := file.Filename staticFile, saveErr := fileutils.SaveStaticFile(&ac, file)
err = ac.C.SaveUploadedFile(file, ac.Config.ImageFolderPath+"/"+filepath) if saveErr != nil {
if err != nil { myvalidator.ReturnErrorsAsJsonResponse(&ac, saveErr)
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return return
} }
ac.C.JSON(http.StatusOK, gin.H{"filepath": "/bookcover/" + filepath}) ac.C.JSON(http.StatusOK, staticFileDbToWs(&staticFile))
}
func staticFileDbToWs(f *model.StaticFile) fileInfoPost {
return fileInfoPost{
FileID: f.ID,
FilePath: fileutils.GetWsLinkPrefix() + f.Path,
}
} }