diff --git a/.gitignore b/.gitignore index 3054b59..8c2c770 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,4 @@ public/css public/js docker-compose.yml dump +data diff --git a/README.md b/README.md index bfc29e6..af9a072 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MusicTopus est une application Web (que vous pouvez auto-héberger) et un site Web (sur lequel vous pouvez créer un compte) permettant de gérer votre liste des CDs et Vinyles et de l'utiliser facilement n'importe où. -Le code source est publié sous licence libre [GNU GPL-3.0-or-later](LICENSE) et est disponible sur [git.darkou.fr](https://git.darkou.fr/dbroqua/MusicTopus). +Le code source est publié sous licence libre [GNU GPL-3.0-or-later](LICENSE) et est disponible sur [forge.darkou.fr](https://forge.darkou.fr/contact/MusicTopus). ## Utilisation @@ -33,7 +33,7 @@ En mode standalone il vous faudra : Quelque que soit la méthode, la première étape est de cloner le projet : ```bash -git clone https://git.darkou.fr/dbroqua/MusicTopus.git +git clone https://forge.darkou.fr/contact/MusicTopus.git ``` ### Installation @@ -240,5 +240,5 @@ MAIL_TO # Adresse mail du contact qui recevra les messages de la page "nous cont ## Contributeurs -- Damien Broqua (développeur principal du projet) -- Brunus (Logo et fournisseur d'idées :wink: ) +- [DarKou](https://darkou.link/) (développeur principal du projet) +- [Brunus](https://www.brunuslab.net/) (Logo et fournisseur d'idées 😉 ) diff --git a/docker-compose.yml.dev b/docker-compose.yml.dev index 6b6a0d5..d33ff77 100644 --- a/docker-compose.yml.dev +++ b/docker-compose.yml.dev @@ -1,61 +1,62 @@ version: "2.4" services: - musictopus-www: - container_name: musictopus-www - image: "node:18" - restart: always - user: "node" - working_dir: /home/node/app - command: > - bash -c " - yarn install && - yarn watch" - volumes: - - ./:/home/node/app - - /home/node/node_modules - ports: - - 3001:3001 - depends_on: - - musictopus-db - environment: - NODE_ENV: ${NODE_ENV} - PORT: ${PORT} - MONGODB_URI: ${MONGODB_URI} - SECRET: ${SECRET} - DISCOGS_TOKEN: ${DISCOGS_TOKEN} - FORMSPREE_ID: ${FORMSPREE_ID} - MATOMO_URL: ${MATOMO_URL} - MATOMO_ID: ${MATOMO_ID} - SITE_NAME: ${SITE_NAME} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - S3_BASEFOLDER: ${S3_BASEFOLDER} - S3_BUCKET: ${S3_BUCKET} - S3_ENDPOINT: ${S3_ENDPOINT} - S3_SIGNATURE: ${S3_SIGNATURE} - JOBS_HEADER_KEY: ${JOBS_HEADER_KEY} - JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE} - REGISTRATION_OPEN: ${REGISTRATION_OPEN} - MAIL_METHOD: ${MAIL_METHOD} - MAIL_HOST: ${MAIL_HOST} - MAIL_PORT: ${MAIL_PORT} - MAIL_USER: ${MAIL_USER} - MAIL_PASSWORD: ${MAIL_PASSWORD} - MAIL_TO: ${MAIL_TO} - networks: - - musictopus - musictopus-db: - container_name: musictopus-db - image: mongo:4.4 - restart: always - ports: - - 27617:27017 - networks: - - musictopus - volumes: - - ./dump:/dump + musictopus-www: + container_name: musictopus-www + image: "node:18" + restart: always + user: "node" + working_dir: /home/node/app + command: > + bash -c " + yarn install && + yarn watch" + volumes: + - ./:/home/node/app + - /home/node/node_modules + ports: + - 3001:3001 + depends_on: + - musictopus-db + environment: + NODE_ENV: ${NODE_ENV} + PORT: ${PORT} + MONGODB_URI: ${MONGODB_URI} + SECRET: ${SECRET} + DISCOGS_TOKEN: ${DISCOGS_TOKEN} + FORMSPREE_ID: ${FORMSPREE_ID} + MATOMO_URL: ${MATOMO_URL} + MATOMO_ID: ${MATOMO_ID} + SITE_NAME: ${SITE_NAME} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + S3_BASEFOLDER: ${S3_BASEFOLDER} + S3_BUCKET: ${S3_BUCKET} + S3_ENDPOINT: ${S3_ENDPOINT} + S3_SIGNATURE: ${S3_SIGNATURE} + JOBS_HEADER_KEY: ${JOBS_HEADER_KEY} + JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE} + REGISTRATION_OPEN: ${REGISTRATION_OPEN} + MAIL_METHOD: ${MAIL_METHOD} + MAIL_HOST: ${MAIL_HOST} + MAIL_PORT: ${MAIL_PORT} + MAIL_USER: ${MAIL_USER} + MAIL_PASSWORD: ${MAIL_PASSWORD} + MAIL_TO: ${MAIL_TO} + networks: + - musictopus + musictopus-db: + container_name: musictopus-db + image: mongo:4.4 + restart: always + ports: + - 27617:27017 + networks: + - musictopus + volumes: + - ./dump:/dump + - ./data:/data/db networks: - musictopus: - driver: bridge + musictopus: + driver: bridge diff --git a/fontello.json b/fontello.json index 11e03d9..efb7b52 100644 --- a/fontello.json +++ b/fontello.json @@ -6,6 +6,12 @@ "units_per_em": 1000, "ascent": 850, "glyphs": [ + { + "uid": "ca90da02d2c6a3183f2458e4dc416285", + "css": "adjust", + "code": 59408, + "src": "fontawesome" + }, { "uid": "44e04715aecbca7f266a17d5a7863c68", "css": "plus", diff --git a/gulpfile.js b/gulpfile.js index 8e76f38..fe1fe6f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -10,6 +10,7 @@ const babel = require("gulp-babel"); const sourceJs = "javascripts/**/*.js"; const sourceRemoteJS = [ "./node_modules/vue/dist/vue.global.prod.js", + "./node_modules/chart.js/dist/chart.umd.js", "./node_modules/axios/dist/axios.min.js", ]; diff --git a/javascripts/ajouter-un-album.js b/javascripts/ajouter-un-album.js index 02f50d4..3bce60c 100644 --- a/javascripts/ajouter-un-album.js +++ b/javascripts/ajouter-un-album.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ Vue.createApp({ data() { return { @@ -78,6 +79,12 @@ Vue.createApp({ ], }; }, + created() { + window.addEventListener("keydown", this.keyDown); + }, + destroyed() { + window.removeEventListener("keydown", this.keyDown); + }, methods: { search(event) { event.preventDefault(); @@ -171,12 +178,15 @@ Vue.createApp({ this.submitting = true; return axios - .post("/api/v1/albums", { + .post(`/api/v1/${action}`, { album: this.details, share: this.share, }) .then(() => { - window.location.href = "/ma-collection"; + window.location.href = + action === "albums" + ? "/ma-collection" + : "/ma-liste-de-souhaits"; }) .catch((err) => { this.submitting = false; @@ -189,5 +199,13 @@ Vue.createApp({ orderedItems(items) { return items.sort(); }, + keyDown(event) { + const keycode = event.code; + + if (this.modalIsVisible && keycode === "Escape") { + event.preventDefault(); + this.modalIsVisible = false; + } + }, }, }).mount("#ajouter-album"); diff --git a/javascripts/collection.js b/javascripts/collection.js index 21ff703..9e222d2 100644 --- a/javascripts/collection.js +++ b/javascripts/collection.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ Vue.createApp({ data() { return { @@ -7,8 +8,8 @@ Vue.createApp({ total: 0, // eslint-disable-next-line no-undef page: query.page || 1, - totalPages: 1, limit: 16, + totalPages: 1, artist: "", format: "", year: "", @@ -34,10 +35,18 @@ Vue.createApp({ }, created() { this.fetch(); + + window.addEventListener("keydown", this.keyDown); + }, + destroyed() { + window.removeEventListener("keydown", this.keyDown); }, methods: { formatParams(param) { - return param.replace("&", "%26").replace("+", "%2B"); + return param + .replace("&", "%26") + .replace("+", "%2B") + .replace('"', "%22"); }, fetch() { this.loading = true; @@ -57,7 +66,7 @@ Vue.createApp({ const [key, value] = entry; switch (key) { case "artists_sort": - this.artist = value; + this.artist = value.replaceAll('"', "%22"); break; default: if (["order", "sort"].indexOf(key) !== -1) { @@ -69,7 +78,7 @@ Vue.createApp({ this.sortOrder = `${sortOrder.sort}-${sortOrder.order}`; - let url = `/api/v1/albums?page=${this.page}&limit=${this.limit}&sort=${this.sort}&order=${this.order}`; + let url = `/api/v1/${action}?page=${this.page}&sort=${this.sort}&order=${this.order}`; if (this.artist) { url += `&artist=${this.formatParams(this.artist)}`; } @@ -94,6 +103,7 @@ Vue.createApp({ .get(url) .then((response) => { this.items = response.data.rows; + this.limit = response.data.limit; this.total = response.data.count || 0; this.totalPages = parseInt(response.data.count / this.limit, 10) + @@ -180,7 +190,7 @@ Vue.createApp({ return false; } return axios - .delete(`/api/v1/albums/${this.itemId}`) + .delete(`/api/v1/${action}/${this.itemId}`) .then(() => { this.fetch(); }) @@ -240,5 +250,16 @@ Vue.createApp({ return render; }, + keyDown(event) { + const keycode = event.code; + if (this.showModalDelete && keycode === "Escape") { + event.preventDefault(); + this.showModalDelete = false; + } + if (this.showModalShare && keycode === "Escape") { + event.preventDefault(); + this.showModalShare = false; + } + }, }, }).mount("#collection"); diff --git a/javascripts/mon-compte/index.js b/javascripts/mon-compte/index.js index 2fb6f0f..464c8b6 100644 --- a/javascripts/mon-compte/index.js +++ b/javascripts/mon-compte/index.js @@ -1,4 +1,3 @@ - if (typeof email !== "undefined" && typeof username !== "undefined") { Vue.createApp({ data() { @@ -12,15 +11,21 @@ if (typeof email !== "undefined" && typeof username !== "undefined") { password: "", passwordConfirm: "", // eslint-disable-next-line no-undef + pagination, + // eslint-disable-next-line no-undef mastodon: mastodon || { publish: false, url: "", token: "", message: "Je viens d'ajouter {artist} - {album} à ma collection !", + wantlist: + "Je viens d'ajouter {artist} - {album} à ma liste de souhaits !", }, + delete: false, }, loading: false, + deleting: false, errors: [], }; }, @@ -57,8 +62,13 @@ if (typeof email !== "undefined" && typeof username !== "undefined") { // eslint-disable-next-line no-unused-vars async updateProfil() { this.errors = []; - const { oldPassword, password, passwordConfirm, mastodon } = - this.formData; + const { + oldPassword, + password, + passwordConfirm, + mastodon, + pagination, + } = this.formData; if (password && !oldPassword) { this.errors.push("emptyPassword"); @@ -83,6 +93,8 @@ if (typeof email !== "undefined" && typeof username !== "undefined") { data.oldPassword = oldPassword; } + data.pagination = pagination; + try { await axios.patch(`/api/v1/me`, data); @@ -98,6 +110,20 @@ if (typeof email !== "undefined" && typeof username !== "undefined") { return true; }, + async deleteAccount() { + try { + await axios.delete(`/api/v1/me`); + + showToastr("Compte supprimé", true); + + window.location.href = "/se-deconnecter"; + } catch (err) { + showToastr( + err.response?.data?.message || + "Impossible de mettre à jour votre profil" + ); + } + }, }, }).mount("#mon-compte"); } diff --git a/javascripts/mon-compte/ma-collection/details.js b/javascripts/mon-compte/ma-collection/details.js index af0414f..2297bdd 100644 --- a/javascripts/mon-compte/ma-collection/details.js +++ b/javascripts/mon-compte/ma-collection/details.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ if (typeof item !== "undefined") { Vue.createApp({ data() { @@ -25,10 +26,10 @@ if (typeof item !== "undefined") { this.setTrackList(); this.setIdentifiers(); - window.addEventListener("keydown", this.changeImage); + window.addEventListener("keydown", this.keyDown); }, destroyed() { - window.removeEventListener("keydown", this.changeImage); + window.removeEventListener("keydown", this.keyDown); }, watch: { shareMessage(message) { @@ -40,6 +41,8 @@ if (typeof item !== "undefined") { this.shareMessageTransformed = message .replaceAll("{artist}", this.item.artists[0].name) .replaceAll("{format}", this.item.formats[0].name) + .replaceAll("{genres}", this.item.genres.join(", ")) + .replaceAll("{styles}", this.item.styles.join(", ")) .replaceAll("{year}", this.item.year) .replaceAll("{video}", video) .replaceAll("{album}", this.item.title); @@ -139,12 +142,12 @@ if (typeof item !== "undefined") { this.setImage(); }, changeImage(event) { + event.preventDefault(); const direction = event.code; if ( - this.modalIsVisible && ["ArrowRight", "ArrowLeft", "Escape"].indexOf(direction) !== - -1 + -1 ) { switch (direction) { case "ArrowRight": @@ -159,6 +162,20 @@ if (typeof item !== "undefined") { return true; }, + keyDown(event) { + const keycode = event.code; + if (this.modalIsVisible) { + this.changeImage(event); + } + if (this.showModalDelete && keycode === "Escape") { + event.preventDefault(); + this.showModalDelete = false; + } + if (this.showModalShare && keycode === "Escape") { + event.preventDefault(); + this.showModalShare = false; + } + }, showAllIdentifiers() { this.identifiersMode = "all"; this.setIdentifiers(); @@ -180,7 +197,7 @@ if (typeof item !== "undefined") { updateItem() { showToastr("Mise à jour en cours…", true); axios - .patch(`/api/v1/albums/${this.item._id}`) + .patch(`/api/v1/${action}/${this.item._id}`) .then((res) => { showToastr("Mise à jour réalisée avec succès", true); this.item = res.data; @@ -199,9 +216,12 @@ if (typeof item !== "undefined") { }, deleteItem() { axios - .delete(`/api/v1/albums/${this.item._id}`) + .delete(`/api/v1/${action}/${this.item._id}`) .then(() => { - window.location.href = "/ma-collection"; + window.location.href = + action === "albums" + ? "/ma-collection" + : "/ma-liste-de-souhaits"; }) .catch((err) => { showToastr( @@ -222,7 +242,7 @@ if (typeof item !== "undefined") { } this.shareSubmiting = true; axios - .post(`/api/v1/albums/${this.item._id}/share`, { + .post(`/api/v1/${action}/${this.item._id}/share`, { message: this.shareMessageTransformed, }) .then(() => { diff --git a/javascripts/mon-compte/ma-collection/exporter.js b/javascripts/mon-compte/ma-collection/exporter.js index b391436..90ef809 100644 --- a/javascripts/mon-compte/ma-collection/exporter.js +++ b/javascripts/mon-compte/ma-collection/exporter.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ Vue.createApp({ data() { return { @@ -10,7 +11,10 @@ Vue.createApp({ exportCollection(event) { event.preventDefault(); - window.open(`/api/v1/albums?exportFormat=${this.format}`, "_blank"); + window.open( + `/api/v1/${action}?exportFormat=${this.format}`, + "_blank" + ); }, }, }).mount("#exporter"); diff --git a/javascripts/mon-compte/ma-collection/importer.js b/javascripts/mon-compte/ma-collection/importer.js index 08f2b6f..6103bfa 100644 --- a/javascripts/mon-compte/ma-collection/importer.js +++ b/javascripts/mon-compte/ma-collection/importer.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ Vue.createApp({ data() { return { @@ -66,11 +67,11 @@ Vue.createApp({ try { const res = await axios.get( - `/api/v1/albums?discogsId=${release_id}` + `/api/v1/${action}?discogsId=${release_id}` ); if (res.status === 204) { - await axios.post("/api/v1/albums", { + await axios.post(`/api/v1/${action}`, { discogsId: release_id, share: false, }); diff --git a/javascripts/theme.js b/javascripts/theme.js index 4625673..e39836b 100644 --- a/javascripts/theme.js +++ b/javascripts/theme.js @@ -1,19 +1,3 @@ -/** - * Fonction permettant de sauvegarder dans le stockage local le choix du thème - * @param {String} scheme - */ -function saveColorScheme(scheme) { - localStorage.setItem("theme", scheme); -} - -/** - * Fonction permettant de changer le thème du site - * @param {String} scheme - */ -function setColorScheme(scheme) { - document.documentElement.setAttribute("data-theme", scheme); -} - /** * Fonction permettant de récupérer le thème du système * @return {String} @@ -28,10 +12,56 @@ function getPreferredColorScheme() { return "light"; } +/** + * @param {String} scheme + */ +function setPictoOnMenu(scheme) { + document.querySelectorAll(".icon-theme").forEach((item) => { + item.classList.add("hidden"); + }); + document + .querySelector(`.icon-theme.theme-${scheme}`) + .classList.remove("hidden"); +} + +/** + * Fonction permettant de sauvegarder dans le stockage local le choix du thème + * @param {String} scheme + */ +function saveColorScheme(scheme) { + localStorage.setItem("theme", scheme); +} + +/** + * Fonction permettant de changer le thème du site + * @param {String} scheme + */ +function setColorScheme(scheme) { + document.documentElement.setAttribute( + "data-theme", + scheme === "system" ? getPreferredColorScheme() : scheme + ); + + setPictoOnMenu(scheme); +} + +/** + * Fonction déclenchée lorsqu'un utilisateur clique sur un bouton dans le menu déroulant + * @param {Object} e + */ +function changeTheme(e) { + e.preventDefault(); + + const scheme = this.dataset.value; + + saveColorScheme(scheme); + setColorScheme(scheme); +} + // INFO: On place un event sur le bouton -const toggleSwitch = document.querySelector( - '.theme-switch input[type="checkbox"]' -); +const buttonsTheme = document.getElementsByClassName("theme"); +// INFO: On récupère du local storage (ou des préférences navigateur) le thème actuel +const currentTheme = localStorage.getItem("theme") || getPreferredColorScheme(); /** * Event permettant de détecter les changements de thème du système @@ -44,28 +74,14 @@ if (window.matchMedia) { if (selectedColorScheme === "system") { const preferedColorScheme = getPreferredColorScheme(); setColorScheme(preferedColorScheme); - - toggleSwitch.checked = preferedColorScheme === "dark"; } }); } -const currentTheme = localStorage.getItem("theme") || getPreferredColorScheme(); - // INFO: Au chargement de la page on détecte le thème à charger setColorScheme(currentTheme); -toggleSwitch.checked = currentTheme === "dark"; - -toggleSwitch.addEventListener( - "change", - (e) => { - e.preventDefault(); - - const scheme = e.target.checked ? "dark" : "light"; - - saveColorScheme(scheme); - setColorScheme(scheme); - }, - false -); +// INFO: On place un event au click sur chacun des boutons du menu +for (let i = 0; i < buttonsTheme.length; i += 1) { + buttonsTheme[i].addEventListener("click", changeTheme, false); +} diff --git a/package-lock.json b/package-lock.json index 1a49ca9..64fdc92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@babel/core": "^7.17.2", "@babel/preset-env": "^7.16.11", "axios": "^0.26.0", + "chart.js": "^4.4.1", "connect-ensure-login": "^0.1.1", "connect-flash": "^0.1.1", "connect-mongo": "^4.6.0", @@ -5257,6 +5258,11 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "license": "MIT" }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz", @@ -7913,6 +7919,17 @@ "node": ">=4" } }, + "node_modules/chart.js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", diff --git a/package.json b/package.json index e8815fb..b9cba5b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "repository": { "type": "git", - "url": "git@git.darkou.fr:dbroqua/MusicTopus.git" + "url": "git@forge.darkou.fr:contact/MusicTopus.git" }, "author": { "name": "Damien Broqua", @@ -45,6 +45,7 @@ "@babel/core": "^7.17.2", "@babel/preset-env": "^7.16.11", "axios": "^0.26.0", + "chart.js": "^4.4.1", "connect-ensure-login": "^0.1.1", "connect-flash": "^0.1.1", "connect-mongo": "^4.6.0", diff --git a/public/500.html b/public/500.html index 76eb123..0de99d6 100644 --- a/public/500.html +++ b/public/500.html @@ -17,7 +17,7 @@ - + Image représentant la mascotte tenant un vinyle cassé Nous sommes désolé mais quelque chose a mal tourné de notre côté.
diff --git a/public/font/icon.eot b/public/font/icon.eot index 424368f..20368e4 100644 Binary files a/public/font/icon.eot and b/public/font/icon.eot differ diff --git a/public/font/icon.svg b/public/font/icon.svg index 52ad914..04a06e1 100644 --- a/public/font/icon.svg +++ b/public/font/icon.svg @@ -1,7 +1,7 @@ -Copyright (C) 2022 by original authors @ fontello.com +Copyright (C) 2024 by original authors @ fontello.com @@ -28,6 +28,8 @@ + + diff --git a/public/font/icon.ttf b/public/font/icon.ttf index 6688a79..3941480 100644 Binary files a/public/font/icon.ttf and b/public/font/icon.ttf differ diff --git a/public/font/icon.woff b/public/font/icon.woff index 9543cf5..add6217 100644 Binary files a/public/font/icon.woff and b/public/font/icon.woff differ diff --git a/public/font/icon.woff2 b/public/font/icon.woff2 index c2acb70..eb6196f 100644 Binary files a/public/font/icon.woff2 and b/public/font/icon.woff2 differ diff --git a/public/img/emoji-lmhf.svg b/public/img/emoji-lmhf.svg new file mode 100644 index 0000000..b235367 --- /dev/null +++ b/public/img/emoji-lmhf.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + diff --git a/public/img/lmhf.svg b/public/img/lmhf.svg new file mode 100644 index 0000000..bfcc992 --- /dev/null +++ b/public/img/lmhf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sass/500.scss b/sass/500.scss index 910f8dd..8b18265 100644 --- a/sass/500.scss +++ b/sass/500.scss @@ -1,16 +1,9 @@ .body-500 { padding: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-weight: 400; font-size: larger; line-height: 200%; - text-align: center; - img { max-width: 60%; margin-bottom: 32px; diff --git a/sass/box.scss b/sass/box.scss index 95c138e..5f3d322 100644 --- a/sass/box.scss +++ b/sass/box.scss @@ -3,17 +3,20 @@ border-radius: 6px; box-shadow: var(--box-shadow-color) 0px 3px 6px 0px; color: var(--font-color); - display: block; padding: 1.25rem; - width: calc(100% - 2rem); - margin: auto; + @include transition() {} - @include respond-to("small-up") { - width: 65%; - } - @include respond-to("medium-up") { - width: 35%; + &.mini { + margin: auto; + width: calc(100% - 2rem); + + @include respond-to("small-up") { + width: 65%; + } + @include respond-to("medium-up") { + width: 35%; + } } h1 { diff --git a/sass/collection.scss b/sass/collection.scss index 88f94b5..c8feca2 100644 --- a/sass/collection.scss +++ b/sass/collection.scss @@ -4,30 +4,6 @@ cursor: pointer; } } - .filters { - display: flex; - justify-content: end; - padding: 0.5rem 0; - - .field { - padding: 0 0.5rem; - - select { - width: auto; - } - - @include respond-to("small-up") { - width: 33%; - &:last-child { - padding-right: 0; - } - } - } - - @include respond-to("small") { - flex-direction: column; - } - } .showMoreFilters { cursor: pointer; @@ -53,8 +29,4 @@ } } } - - .total { - margin: 0.75rem 0; - } } \ No newline at end of file diff --git a/sass/colors.scss b/sass/colors.scss index 4328cf3..ae39df3 100644 --- a/sass/colors.scss +++ b/sass/colors.scss @@ -45,6 +45,7 @@ $close-background-dark: rgba(240,240,240,.6); --bg-color: #{darken($white, 5%)}; --bg-alternate-color: #{darken($white, 8%)}; --font-color: #{$nord3}; + --hover-font-color: #{lighten($nord3, 16%)}; --footer-color: #{$darken-white}; --link-color: #{$nord1}; @@ -88,6 +89,7 @@ $close-background-dark: rgba(240,240,240,.6); --bg-color: #{lighten($nord0, 2%)}; --bg-alternate-color: #{lighten($nord3, 8%)}; --font-color: #{$nord6}; + --hover-font-color: #{darken($nord6, 16%)}; --footer-color: #{$nord1}; --link-color: #{$nord4}; diff --git a/sass/forms.scss b/sass/forms.scss index 125527a..1e71919 100644 --- a/sass/forms.scss +++ b/sass/forms.scss @@ -70,78 +70,4 @@ background-size: 1.2rem; padding-right: 2.4rem; } -} - -.theme-switch-wrapper { - display: flex; - align-items: center; - - em { - margin-left: 10px; - font-size: 1rem; - } -} - -.theme-switch { - display: inline-block; - height: 34px; - position: relative; - width: 60px; -} - -.theme-switch input { - display:none; -} - -.slider { - background-color: #ccc; - bottom: 0; - cursor: pointer; - left: 0; - position: absolute; - right: 0; - top: 0; - transition: .4s; - @include transition() {} -} - -.slider:before { - background-color: #fff; - bottom: 4px; - content: '\f185'; - height: 26px; - left: 4px; - position: absolute; - transition: .4s; - width: 26px; - padding: 0; - - font-family: "icon"; - font-style: normal; - font-weight: normal; - display: inline-block; - text-decoration: inherit; - text-align: center; - font-variant: normal; - text-transform: none; -} - -input:checked + .slider { - background-color: $primary-color; - @include transition() {} -} - -input:checked + .slider:before { - transform: translateX(26px); - content: '\f186'; - background-color: var(--input-active-color); - @include transition() {} -} - -.slider.round { - border-radius: 34px; -} - -.slider.round:before { - border-radius: 50%; -} +} \ No newline at end of file diff --git a/sass/icons.scss b/sass/icons.scss index 7b2150b..ce6b38f 100644 --- a/sass/icons.scss +++ b/sass/icons.scss @@ -1,66 +1,79 @@ @font-face { font-family: 'icon'; - src: url('/font/icon.eot?41426785'); - src: url('/font/icon.eot?41426785#iefix') format('embedded-opentype'), - url('/font/icon.woff2?41426785') format('woff2'), - url('/font/icon.woff?41426785') format('woff'), - url('/font/icon.ttf?41426785') format('truetype'), - url('/font/icon.svg?41426785#icon') format('svg'); + src: url('/font/icon.eot?15219908'); + src: url('/font/icon.eot?15219908#iefix') format('embedded-opentype'), + url('/font/icon.woff2?15219908') format('woff2'), + url('/font/icon.woff?15219908') format('woff'), + url('/font/icon.ttf?15219908') format('truetype'), + url('/font/icon.svg?15219908#icon') format('svg'); font-weight: normal; font-style: normal; -} - -[class^="icon-"]:before, [class*=" icon-"]:before { + } + /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ + /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ + /* + @media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'icon'; + src: url('../font/icon.svg?15219908#icon') format('svg'); + } + } + */ + [class^="icon-"]:before, [class*=" icon-"]:before { font-family: "icon"; font-style: normal; font-weight: normal; + speak: never; display: inline-block; text-decoration: inherit; width: 1em; margin-right: .2em; text-align: center; + /* opacity: .8; */ + /* For safety - reset parent styles, that can break glyph codes*/ font-variant: normal; text-transform: none; + /* fix buttons height, for twitter bootstrap */ line-height: 1em; + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ margin-left: .2em; + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + + /* Font smoothing. That was taken from TWBS */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -} -.icon-plus:before { content: '\e800'; } /* '' */ -.icon-user:before { content: '\e801'; } /* '' */ -.icon-search:before { content: '\e802'; } /* '' */ -.icon-mail:before { content: '\e803'; } /* '' */ -.icon-link:before { content: '\e804'; } /* '' */ -.icon-heart:before { content: '\e805'; } /* '' */ -.icon-eye:before { content: '\e806'; } /* '' */ -.icon-left-open:before { content: '\e807'; } /* '' */ -.icon-right-open:before { content: '\e808'; } /* '' */ -.icon-export:before { content: '\e809'; } /* '' */ -.icon-refresh:before { content: '\e80a'; } /* '' */ -.icon-spin:before { content: '\e839'; } /* '' */ -.icon-link-ext:before { content: '\f08e'; } /* '' */ -.icon-sun:before { content: '\f185'; } /* '' */ -.icon-moon:before { content: '\f186'; } /* '' */ -.icon-share:before { content: '\f1e0'; } /* '' */ -.icon-trash:before { content: '\f1f8'; } /* '' */ -.icon-blind:before { content: '\f29d'; } /* '' */ + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ + } -.animate-spin { - animation: spin 2s infinite linear; - display: inline-block; -} -@keyframes spin { - 0% { - transform: rotate(0deg); - } + .icon-plus:before { content: '\e800'; } /* '' */ + .icon-user:before { content: '\e801'; } /* '' */ + .icon-search:before { content: '\e802'; } /* '' */ + .icon-mail:before { content: '\e803'; } /* '' */ + .icon-link:before { content: '\e804'; } /* '' */ + .icon-heart:before { content: '\e805'; } /* '' */ + .icon-eye:before { content: '\e806'; } /* '' */ + .icon-left-open:before { content: '\e807'; } /* '' */ + .icon-right-open:before { content: '\e808'; } /* '' */ + .icon-export:before { content: '\e809'; } /* '' */ + .icon-refresh:before { content: '\e80a'; } /* '' */ + .icon-adjust:before { content: '\e810'; } /* '' */ + .icon-spin:before { content: '\e839'; } /* '' */ + .icon-link-ext:before { content: '\f08e'; } /* '' */ + .icon-sun:before { content: '\f185'; } /* '' */ + .icon-moon:before { content: '\f186'; } /* '' */ + .icon-share:before { content: '\f1e0'; } /* '' */ + .icon-trash:before { content: '\f1f8'; } /* '' */ + .icon-blind:before { content: '\f29d'; } /* '' */ - 100% { - transform: rotate(359deg); - } -} +.lmhf { + height: 16px; +} \ No newline at end of file diff --git a/sass/index.scss b/sass/index.scss index 476e4b8..808151b 100644 --- a/sass/index.scss +++ b/sass/index.scss @@ -16,6 +16,7 @@ @import './list'; @import './box'; @import './loader'; +@import './table'; @import './error'; @import './messages.scss'; diff --git a/sass/navbar.scss b/sass/navbar.scss index b1aab70..593bb5f 100644 --- a/sass/navbar.scss +++ b/sass/navbar.scss @@ -103,7 +103,7 @@ .navbar-item { line-height: 1.5; - padding: .5rem .75rem; + padding: .5rem; position: relative; flex-grow: 0; flex-shrink: 0; @@ -117,7 +117,6 @@ @include respond-to("medium-up") { display: flex; align-items: stretch; - color: rgba(0,0,0,.7); .navbar-dropdown { background-color: var(--default-color); @@ -127,8 +126,7 @@ box-shadow: 0 8px 8px rgba(10,10,10,.1); display: none; font-size: .875rem; - left: 0; - min-width: 100%; + min-width: 280px; position: absolute; top: 100%; } @@ -138,7 +136,7 @@ .navbar-link { background-color: var(--default-hl-color); - color: rgba(0,0,0,.7); + color: var(--hover-font-color); } .navbar-dropdown { @@ -167,6 +165,29 @@ @include respond-to("medium-up") { display: flex; align-items: center; + + &::after { + border: 3px solid transparent; + border-radius: 2px; + border-right: 0; + border-top: 0; + content: " "; + display: block; + height: .625em; + pointer-events: none; + position: absolute; + top: 50%; + transform: rotate(-45deg); + transform-origin: center; + width: .625em; + border-color: var(--secondary-color); + margin-top: -0.375em; + right: 1.125em; + + @include respond-to("medium-up") { + border-color: var(--font-color); + } + } } .icon { @@ -177,28 +198,7 @@ width: 1.5rem; } - &::after { - border: 3px solid transparent; - border-radius: 2px; - border-right: 0; - border-top: 0; - content: " "; - display: block; - height: .625em; - pointer-events: none; - position: absolute; - top: 50%; - transform: rotate(-45deg); - transform-origin: center; - width: .625em; - border-color: var(--secondary-color); - margin-top: -0.375em; - right: 1.125em; - @include respond-to("medium-up") { - border-color: rgba(0,0,0,.7); - } - } } .navbar-menu { @@ -260,13 +260,7 @@ background-color: var(--font-color); border: none; height: 2px; - margin: .5rem 0; - } - - .navbar-item { - cursor: pointer; - padding-left: 1.5rem; - padding-right: 1.5rem; + margin: .5rem 0 0 1.5rem; } @include respond-to("medium-up") { @@ -277,16 +271,19 @@ box-shadow: 0 8px 8px rgba(10,10,10,.1); display: none; font-size: .875rem; - left: 0; + right: 0; min-width: 100%; position: absolute; top: 100%; - .navbar-item { - white-space: nowrap; - padding: .375rem 1rem; - padding-right: 3rem; + hr { + margin: 0.5rem 0; } + + // .navbar-item { + // white-space: nowrap; + // padding-block: .375rem; + // } } } @@ -320,4 +317,10 @@ .navbar { box-shadow: none; } +} + +nav { + ul { + padding-inline: 16px; + } } \ No newline at end of file diff --git a/sass/pagination.scss b/sass/pagination.scss index ddf2695..e43fc74 100644 --- a/sass/pagination.scss +++ b/sass/pagination.scss @@ -1,25 +1,8 @@ .pagination { font-size: 1rem; - align-items: center; - display: flex; - text-align: center; - justify-content: space-between; margin: 0.75rem 0; - .pagination-list { - align-items: center; - display: flex; - text-align: center; - flex-wrap: wrap; - flex-grow: 1; - flex-shrink: 1; - justify-content: flex-end; - - @include respond-to("small") { - justify-content: center; - } - .pagination-link { align-items: center; border: 1px solid transparent; diff --git a/sass/table.scss b/sass/table.scss new file mode 100644 index 0000000..44565df --- /dev/null +++ b/sass/table.scss @@ -0,0 +1,23 @@ +table { + th, + td { + padding: 0.75rem; + text-align: left; + } + + thead { + tr { + border-bottom: 2px solid var(--font-color); + } + } + tbody { + tr { + background-color: var(--default-color); + + &:nth-child(2n) { + background-color: var(--bg-alternate-color); + } + } + + } +} \ No newline at end of file diff --git a/src/app.js b/src/app.js index b293a37..3c1344d 100644 --- a/src/app.js +++ b/src/app.js @@ -6,32 +6,37 @@ import mongoose from "mongoose"; import flash from "connect-flash"; import session from "express-session"; import MongoStore from "connect-mongo"; +import debugLib from "debug"; import passportConfig from "./libs/passport"; -import config, { env, mongoDbUri, secret } from "./config"; +import config, { env, mongoDbUri, port, secret } from "./config"; import { isXhr } from "./helpers"; import indexRouter from "./routes"; import maCollectionRouter from "./routes/ma-collection"; +import wantlistRouter from "./routes/wantlist"; import monCompteRouter from "./routes/mon-compte"; import collectionRouter from "./routes/collection"; import importJobsRouter from "./routes/jobs"; import importAlbumRouterApiV1 from "./routes/api/v1/albums"; +import importWantlistRouterApiV1 from "./routes/api/v1/wantlist"; import importSearchRouterApiV1 from "./routes/api/v1/search"; import importMastodonRouterApiV1 from "./routes/api/v1/mastodon"; import importMeRouterApiV1 from "./routes/api/v1/me"; import importContactRouterApiV1 from "./routes/api/v1/contact"; passportConfig(passport); +const debug = debugLib("musictopus:errors"); mongoose.set("strictQuery", false); mongoose .connect(mongoDbUri, { useNewUrlParser: true, useUnifiedTopology: true }) - .catch(() => { + .catch((err) => { + debug(`Mongo error:`, err); process.exit(); }); @@ -81,9 +86,11 @@ app.use(express.static(path.join(__dirname, "../public"))); app.use("/", indexRouter); app.use("/mon-compte", monCompteRouter); app.use("/ma-collection", maCollectionRouter); +app.use("/ma-liste-de-souhaits", wantlistRouter); app.use("/collection", collectionRouter); app.use("/jobs", importJobsRouter); app.use("/api/v1/albums", importAlbumRouterApiV1); +app.use("/api/v1/wantlist", importWantlistRouterApiV1); app.use("/api/v1/search", importSearchRouterApiV1); app.use("/api/v1/mastodon", importMastodonRouterApiV1); app.use("/api/v1/me", importMeRouterApiV1); @@ -150,4 +157,6 @@ app.use((error, req, res, next) => { } }); +console.log(`Server listening on port ${port}!`); + export default app; diff --git a/src/helpers/index.js b/src/helpers/index.js index d07fe14..59d92ca 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -53,3 +53,28 @@ export const isXhr = (req) => { return is; }; + +/** + * Méthode permettant de récupérer les éléments distincts d'une collection + * @param {Object} model + * @param {String} field + * @param {import("mongoose").ObjectId} user + * @returns + */ +export const getAllDistincts = async (model, field, user) => { + const distincts = await model + .find( + { + User: user, + }, + [], + { + sort: { + [field]: 1, + }, + } + ) + .distinct(field); + + return distincts; +}; diff --git a/src/libs/format.js b/src/libs/format.js index 14dfaf9..4d0da37 100644 --- a/src/libs/format.js +++ b/src/libs/format.js @@ -34,6 +34,23 @@ export const sendResponse = (req, res, data) => { } }; +export const setHashTags = (arr) => { + let hashTags = ""; + + for (let i = 0; i < arr.length; i += 1) { + let currentHash = `${hashTags ? ", " : ""} #`; + const currentItem = arr[i].split(" "); + for (let j = 0; j < currentItem.length; j += 1) { + currentHash += + currentItem[j].toLowerCase().charAt(0).toUpperCase() + + currentItem[j].toLowerCase().slice(1); + } + hashTags += currentHash; + } + + return hashTags; +}; + export default (res, page) => { res.status(200).render("index", page.render()); }; diff --git a/src/middleware/Albums.js b/src/middleware/Albums.js index 59d6c5c..ac19eac 100644 --- a/src/middleware/Albums.js +++ b/src/middleware/Albums.js @@ -1,4 +1,4 @@ -import { format as formatDate } from "date-fns"; +import { format as formatDate, startOfYear, subYears } from "date-fns"; import fs from "fs"; import Mastodon from "mastodon"; @@ -12,7 +12,8 @@ import JobsModel from "../models/jobs"; import UsersModel from "../models/users"; import ErrorEvent from "../libs/error"; -import { getAlbumDetails } from "../helpers"; +import { getAlbumDetails, getAllDistincts } from "../helpers"; +import { setHashTags } from "../libs/format"; /** * Classe permettant la gestion des albums d'un utilisateur @@ -42,8 +43,11 @@ class Albums extends Pages { discogsId: albumDetails.id, User: user._id, }; + // eslint-disable-next-line no-nested-ternary data.released = data.released - ? new Date(data.released.replace("-00", "-01")) + ? typeof data.released === "string" + ? new Date(data.released.replace("-00", "-01")) + : data.released : null; delete data.id; @@ -83,6 +87,9 @@ class Albums extends Pages { ) .replaceAll("{artist}", data.artists[0].name) .replaceAll("{format}", data.formats[0].name) + .replaceAll("{#genres}", setHashTags(data.genres)) + .replaceAll("{styles}", data.styles.join(", ")) + .replaceAll("{#styles}", setHashTags(data.styles)) .replaceAll("{year}", data.year) .replaceAll("{video}", video) .replaceAll("{album}", data.title)} @@ -140,26 +147,22 @@ Publié automatiquement via #musictopus`; return album; } - /** - * Méthode permettant de récupérer les éléments distincts d'une collection - * @param {String} field - * @param {ObjectId} user - * @return {Array} - */ - static async getAllDistincts(field, user) { - const distincts = await AlbumsModel.find( - { - User: user, - }, - [], - { - sort: { - [field]: 1, - }, - } - ).distinct(field); + constructor(req, viewname) { + super(req, viewname); - return distincts; + this.colors = [ + "#2e3440", + "#d8dee9", + "#8fbcbb", + "#5e81ac", + "#d08770", + "#bf616a", + "#ebcb8b", + "#a3be8c", + "#b48ead", + ]; + + this.setPageContent("action", "albums"); } /** @@ -169,7 +172,6 @@ Publié automatiquement via #musictopus`; async getAll() { const { page, - limit, exportFormat = "json", sort = "artists_sort", order = "asc", @@ -183,6 +185,8 @@ Publié automatiquement via #musictopus`; discogsId, } = this.req.query; + const limit = this.req.user?.pagination || 16; + let userId = this.req.user?._id; const where = {}; @@ -248,7 +252,7 @@ Publié automatiquement via #musictopus`; }, }; - if (page && limit) { + if (exportFormat === "json" && page && limit) { const skip = (page - 1) * limit; params = { @@ -280,6 +284,7 @@ Publié automatiquement via #musictopus`; default: return { rows, + limit, count, }; } @@ -437,20 +442,28 @@ Publié automatiquement via #musictopus`; * Méthode permettant de créer la page "ma-collection" */ async loadMyCollection() { - const artists = await Albums.getAllDistincts( + const artists = await getAllDistincts( + AlbumsModel, "artists.name", this.req.user._id ); - const formats = await Albums.getAllDistincts( + const formats = await getAllDistincts( + AlbumsModel, "formats.name", this.req.user._id ); - const years = await Albums.getAllDistincts("year", this.req.user._id); - const genres = await Albums.getAllDistincts( + const years = await getAllDistincts( + AlbumsModel, + "year", + this.req.user._id + ); + const genres = await getAllDistincts( + AlbumsModel, "genres", this.req.user._id ); - const styles = await Albums.getAllDistincts( + const styles = await getAllDistincts( + AlbumsModel, "styles", this.req.user._id ); @@ -500,6 +513,230 @@ Publié automatiquement via #musictopus`; await this.loadItem(); } + /** + * Méthode permettant d'afficher des statistiques au sujet de ma collection + */ + async statistics() { + const { _id: User } = this.req.user; + const top = {}; + const byGenres = {}; + const byStyles = {}; + const byFormats = {}; + const top10 = []; + let byStyles10 = []; + const max = this.colors.length - 1; + const startYear = startOfYear(new Date()); + const lastStartYear = startOfYear(subYears(new Date(), 1)); + const currentYear = { + year: formatDate(startYear, "yyyy"), + total: 0, + byGenres: {}, + byStyles: {}, + byFormats: {}, + }; + const lastYear = { + year: formatDate(lastStartYear, "yyyy"), + total: 0, + byGenres: {}, + byStyles: {}, + byFormats: {}, + }; + + const colorsCount = this.colors.length; + + const albums = await AlbumsModel.find({ + User, + artists: { $exists: true, $not: { $size: 0 } }, + }); + + for (let i = 0; i < albums.length; i += 1) { + const currentFormats = []; + const { artists, genres, styles, formats, createdAt } = albums[i]; + + // INFO: On s'occupe de la rétrospective + if (createdAt > startYear) { + currentYear.total += 1; + } else if (createdAt > lastStartYear) { + lastYear.total += 1; + } + + // INFO: On regroupe les artistes par nom pour en faire le top10 + for (let j = 0; j < artists.length; j += 1) { + const { name } = artists[j]; + if (!top[name]) { + top[name] = { + name, + count: 0, + }; + } + top[name].count += 1; + } + + // INFO: On regroupe les genres + for (let j = 0; j < genres.length; j += 1) { + const name = genres[j]; + if (!byGenres[name]) { + byGenres[name] = { + name, + count: 0, + color: this.colors[ + Object.keys(byGenres).length % colorsCount + ], + }; + } + + byGenres[name].count += 1; + + // INFO: On s'occupe de la rétrospective + if (createdAt > startYear) { + if (!currentYear.byGenres[name]) { + currentYear.total += 1; + currentYear.byGenres[name] = { + name, + count: 0, + color: byGenres[name].color, + }; + } + currentYear.byGenres[name].count += 1; + } else if (createdAt > lastStartYear) { + if (!lastYear.byGenres[name]) { + lastYear.byGenres[name] = { + name, + count: 0, + color: byGenres[name].color, + }; + } + lastYear.byGenres[name].count += 1; + } + } + + // INFO: On regroupe les styles + for (let j = 0; j < styles.length; j += 1) { + const name = styles[j]; + if (!byStyles[name]) { + byStyles[name] = { + name, + count: 0, + color: this.colors[ + Object.keys(byStyles).length % colorsCount + ], + }; + } + + byStyles[name].count += 1; + + // INFO: On s'occupe de la rétrospective + if (createdAt > startYear) { + if (!currentYear.byStyles[name]) { + currentYear.total += 1; + currentYear.byStyles[name] = { + name, + count: 0, + color: byStyles[name].color, + }; + } + currentYear.byStyles[name].count += 1; + } else if (createdAt > lastStartYear) { + if (!lastYear.byStyles[name]) { + lastYear.byStyles[name] = { + name, + count: 0, + color: byStyles[name].color, + }; + } + lastYear.byStyles[name].count += 1; + } + } + + // INFO: On regroupe les formats + for (let j = 0; j < formats.length; j += 1) { + const { name } = formats[j]; + // INFO: On évite qu'un album avec 2 vinyles soit compté 2x + if (!currentFormats.includes(name)) { + if (!byFormats[name]) { + byFormats[name] = { + name, + count: 0, + color: this.colors[ + Object.keys(byFormats).length % colorsCount + ], + }; + } + + byFormats[name].count += 1; + currentFormats.push(name); + + // INFO: On s'occupe de la rétrospective + if (createdAt > startYear) { + if (!currentYear.byFormats[name]) { + currentYear.total += 1; + currentYear.byFormats[name] = { + name, + count: 0, + color: byFormats[name].color, + }; + } + currentYear.byFormats[name].count += 1; + } else if (createdAt > lastStartYear) { + if (!lastYear.byFormats[name]) { + lastYear.byFormats[name] = { + name, + count: 0, + color: byFormats[name].color, + }; + } + lastYear.byFormats[name].count += 1; + } + } + } + } + + // INFO: On convertit le top en tableau + Object.keys(top).forEach((index) => { + top10.push(top[index]); + }); + + // INFO: On convertit les styles en tableau + Object.keys(byStyles).forEach((index) => { + byStyles10.push(byStyles[index]); + }); + + // INFO: On ordonne les artistes par quantité d'albums + top10.sort((a, b) => (a.count > b.count ? -1 : 1)); + + // INFO: On ordonne les styles par quantité + byStyles10.sort((a, b) => (a.count > b.count ? -1 : 1)); + const tmp = []; + + // INFO: On recupère le top N des styles et on mets le reste dans le label "autre" + for (let i = 0; i < byStyles10.length; i += 1) { + if (i < max) { + tmp.push({ + ...byStyles10[i], + color: this.colors[max - i], + }); + } else if (i === max) { + tmp.push({ + name: "Autre", + count: 0, + color: this.colors[0], + }); + tmp[max].count += byStyles10[i].count; + } else { + tmp[max].count += byStyles10[i].count; + } + } + byStyles10 = tmp; + + this.setPageTitle("Mes statistiques"); + this.setPageContent("top10", top10.splice(0, 10)); + this.setPageContent("byGenres", byGenres); + this.setPageContent("byStyles", byStyles10); + this.setPageContent("byFormats", byFormats); + this.setPageContent("currentYear", currentYear); + this.setPageContent("lastYear", lastYear); + } + /** * Méthode permettant de créer la page "collection/:userId" */ @@ -516,14 +753,22 @@ Publié automatiquement via #musictopus`; ); } - const artists = await Albums.getAllDistincts("artists.name", userId); - const formats = await Albums.getAllDistincts("formats.name", userId); - const years = await Albums.getAllDistincts("year", userId); - const genres = await Albums.getAllDistincts("genres", userId); - const styles = await Albums.getAllDistincts("styles", userId); + const artists = await getAllDistincts( + AlbumsModel, + "artists.name", + userId + ); + const formats = await getAllDistincts( + AlbumsModel, + "formats.name", + userId + ); + const years = await getAllDistincts(AlbumsModel, "year", userId); + const genres = await getAllDistincts(AlbumsModel, "genres", userId); + const styles = await getAllDistincts(AlbumsModel, "styles", userId); - this.setPageContent("username", user.username); this.setPageTitle(`Collection publique de ${user.username}`); + this.setPageContent("username", user.username); this.setPageContent("artists", artists); this.setPageContent("formats", formats); this.setPageContent("years", years); diff --git a/src/middleware/Export.js b/src/middleware/Export.js index 513e440..de06604 100644 --- a/src/middleware/Export.js +++ b/src/middleware/Export.js @@ -123,8 +123,8 @@ class Export { ws.cell(currentRow, 1).string(artists_sort).style(style); ws.cell(currentRow, 2).string(title).style(style); - ws.cell(currentRow, 3).string(genres.join()).style(style); - ws.cell(currentRow, 4).string(styles.join()).style(style); + ws.cell(currentRow, 3).string(genres.join(", ")).style(style); + ws.cell(currentRow, 4).string(styles.join(", ")).style(style); if (country) { ws.cell(currentRow, 5).string(country).style(style); } diff --git a/src/middleware/Jobs.js b/src/middleware/Jobs.js index 91964f2..4273d83 100644 --- a/src/middleware/Jobs.js +++ b/src/middleware/Jobs.js @@ -5,6 +5,7 @@ import { getAlbumDetails } from "../helpers"; import JobsModel from "../models/jobs"; import AlbumsModel from "../models/albums"; +import WantListModel from "../models/wantlist"; class Jobs { /** @@ -51,6 +52,50 @@ class Jobs { return true; } + /** + * Méthode permettant de télécharger toute les images d'un album + * @param {ObjectId} itemId + */ + static async importAlbumForWantListAssets(itemId) { + const album = await WantListModel.findById(itemId); + + if (!album) { + throw new ErrorEvent( + 404, + "Item non trouvé", + `L'album avec l'id ${itemId} n'existe plus dans la collection` + ); + } + + const item = await getAlbumDetails(album.discogsId); + + if (!item) { + throw new ErrorEvent( + 404, + "Erreur de communication", + "Erreur lors de la récupération des informations sur Discogs" + ); + } + + if (item.thumb) { + album.thumb = await uploadFromUrl(item.thumb); + album.thumbType = "local"; + } + const { images } = item; + if (images && images.length > 0) { + for (let i = 0; i < images.length; i += 1) { + images[i].uri150 = await uploadFromUrl(images[i].uri150); + images[i].uri = await uploadFromUrl(images[i].uri); + } + } + + album.images = images; + + await album.save(); + + return true; + } + /** * Point d'entrée * @param {String} state @@ -78,6 +123,9 @@ class Jobs { case "Albums": await Jobs.importAlbumAssets(job.id); break; + case "WantList": + await Jobs.importAlbumForWantListAssets(job.id); + break; default: throw new ErrorEvent( 500, diff --git a/src/middleware/Me.js b/src/middleware/Me.js index edf40db..5a477cc 100644 --- a/src/middleware/Me.js +++ b/src/middleware/Me.js @@ -1,6 +1,8 @@ import Joi from "joi"; import UsersModel from "../models/users"; +import AlbumsModel from "../models/albums"; +import WantlistModel from "../models/wantlist"; import Pages from "./Pages"; /** @@ -16,6 +18,7 @@ class Me extends Pages { const { _id } = this.req.user; const schema = Joi.object({ + pagination: Joi.number(), isPublicCollection: Joi.boolean(), oldPassword: Joi.string(), password: Joi.string(), @@ -25,6 +28,7 @@ class Me extends Pages { url: Joi.string().uri().allow(null, ""), token: Joi.string().allow(null, ""), message: Joi.string().allow(null, ""), + wantlist: Joi.string().allow(null, ""), }, }); @@ -45,6 +49,10 @@ class Me extends Pages { user.salt = value.password; } + if (value.pagination) { + user.pagination = value.pagination; + } + if (value.isPublicCollection !== undefined) { user.isPublicCollection = value.isPublicCollection; } @@ -63,6 +71,23 @@ class Me extends Pages { return user; } + + /** + * Méthode permettant de supprimer un utilisateur et toutes ses données + * + * @return {Object} + */ + async deleteMe() { + const { _id } = this.req.user; + + await AlbumsModel.deleteMany({ User: _id }); + await WantlistModel.deleteMany({ User: _id }); + await UsersModel.deleteOne({ _id }); + + return { + deleted: true, + }; + } } export default Me; diff --git a/src/middleware/Wantlist.js b/src/middleware/Wantlist.js new file mode 100644 index 0000000..0f51ec3 --- /dev/null +++ b/src/middleware/Wantlist.js @@ -0,0 +1,690 @@ +import { format as formatDate } from "date-fns"; +import fs from "fs"; + +import Mastodon from "mastodon"; +import { v4 } from "uuid"; +import axios from "axios"; +import Pages from "./Pages"; +import Export from "./Export"; + +import WantListModel from "../models/wantlist"; +import JobsModel from "../models/jobs"; +import UsersModel from "../models/users"; +import ErrorEvent from "../libs/error"; + +import { getAlbumDetails, getAllDistincts } from "../helpers"; +import { setHashTags } from "../libs/format"; + +/** + * Classe permettant la gestion da la liste de souhaits d'un utilisateur + */ +class Wantlist extends Pages { + /** + * Méthode permettant d'ajouter un album dans une collection + * @param {Object} req + * @return {Object} + */ + static async postAddOne(req) { + const { body, user } = req; + const { share, discogsId } = body; + + let albumDetails = body.album; + if (discogsId) { + albumDetails = await getAlbumDetails(discogsId); + body.id = discogsId; + } + + if (!albumDetails) { + throw new ErrorEvent(406, "Aucun album à ajouter"); + } + + const data = { + ...albumDetails, + discogsId: albumDetails.id, + User: user._id, + }; + // eslint-disable-next-line no-nested-ternary + data.released = data.released + ? typeof data.released === "string" + ? new Date(data.released.replace("-00", "-01")) + : data.released + : null; + delete data.id; + + const album = new WantListModel(data); + + await album.save(); + + const jobData = { + model: "WantList", + id: album._id, + }; + const job = new JobsModel(jobData); + + job.save(); + + try { + const User = await UsersModel.findOne({ _id: user._id }); + + const { mastodon: mastodonConfig } = User; + + const { publish, token, url, wantlist } = mastodonConfig; + + if (share && publish && url && token) { + const M = new Mastodon({ + access_token: token, + api_url: url, + }); + + const video = + data.videos && data.videos.length > 0 + ? data.videos[0].uri + : ""; + + const status = `${( + wantlist || + "Je viens d'ajouter {artist} - {album} à ma liste de souhaits !" + ) + .replaceAll("{artist}", data.artists[0].name) + .replaceAll("{format}", data.formats[0].name) + .replaceAll("{genres}", data.genres.join(", ")) + .replaceAll("{#genres}", setHashTags(data.genres)) + .replaceAll("{styles}", data.styles.join(", ")) + .replaceAll("{#styles}", setHashTags(data.styles)) + .replaceAll("{year}", data.year) + .replaceAll("{video}", video) + .replaceAll("{album}", data.title)} + +Publié automatiquement via #musictopus`; + + const media_ids = []; + + if (data.images.length > 0) { + for (let i = 0; i < data.images.length; i += 1) { + if (media_ids.length === 4) { + break; + } + + const filename = `${v4()}.jpg`; + const file = `/tmp/${filename}`; + + // eslint-disable-next-line no-await-in-loop + const { data: buff } = await axios.get( + data.images[i].uri, + { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + }, + responseType: "arraybuffer", + } + ); + + fs.writeFileSync(file, buff); + + // eslint-disable-next-line no-await-in-loop + const { data: media } = await M.post("media", { + file: fs.createReadStream(file), + }); + + const { id } = media; + + media_ids.push(id); + + fs.unlinkSync(file); + } + } + + await M.post("statuses", { status, media_ids }); + } + } catch (err) { + throw new ErrorEvent( + 500, + "Mastodon", + "Album ajouté à votre collection mais impossible de publier sur Mastodon" + ); + } + + return album; + } + + constructor(req, viewname) { + super(req, viewname); + + this.colors = [ + "#2e3440", + "#d8dee9", + "#8fbcbb", + "#5e81ac", + "#d08770", + "#bf616a", + "#ebcb8b", + "#a3be8c", + "#b48ead", + ]; + + this.setPageContent("action", "wantlist"); + } + + /** + * Méthode permettant de récupérer la liste des albums d'une collection + * @return {Object} + */ + async getAll() { + const { + page, + exportFormat = "json", + sort = "artists_sort", + order = "asc", + artist, + format, + year, + genre, + style, + userId: collectionUserId, + discogsIds, + discogsId, + } = this.req.query; + + const limit = this.req.user?.pagination || 16; + + let userId = this.req.user?._id; + + const where = {}; + + if (artist) { + where["artists.name"] = artist; + } + if (format) { + where["formats.name"] = format; + } + if (year) { + where.year = year; + } + if (genre) { + where.genres = genre; + } + if (style) { + where.styles = style; + } + + if (!this.req.user && !collectionUserId) { + throw new ErrorEvent( + 401, + "Collection", + "Cette collection n'est pas publique" + ); + } + + if (collectionUserId) { + const userIsSharingCollection = await UsersModel.findById( + collectionUserId + ); + + if ( + !userIsSharingCollection || + !userIsSharingCollection.isPublicCollection + ) { + throw new ErrorEvent( + 401, + "Collection", + "Cette collection n'est pas publique" + ); + } + + userId = userIsSharingCollection._id; + } + + if (discogsIds) { + where.discogsId = { $in: discogsIds }; + } + if (discogsId) { + where.discogsId = Number(discogsId); + } + + const count = await WantListModel.count({ + User: userId, + ...where, + }); + + let params = { + sort: { + [sort]: order.toLowerCase() === "asc" ? 1 : -1, + }, + }; + + if (exportFormat === "json" && page && limit) { + const skip = (page - 1) * limit; + + params = { + ...params, + skip, + limit, + }; + } + + const rows = await WantListModel.find( + { + User: userId, + ...where, + }, + [], + params + ); + + switch (exportFormat) { + case "csv": + return Export.convertToCsv(rows); + case "xls": + return Export.convertToXls(rows); + case "xml": + return Export.convertToXml(rows); + case "musictopus": + return Export.convertToMusicTopus(rows); + case "json": + default: + return { + rows, + limit, + count, + }; + } + } + + /** + * Méthode permettant de récupérer le détails d'un album + * + * @return {Object} + */ + async getOne() { + const { itemId: _id } = this.req.params; + const { _id: User } = this.req.user; + const album = await WantListModel.findOne({ + _id, + User, + }); + + return { + ...album.toJSON(), + released: album.released + ? formatDate(album.released, "MM/dd/yyyy") + : null, + }; + } + + /** + * Méthode permettant de mettre à jour un album + * + * @return {Object} + */ + async patchOne() { + const { itemId: _id } = this.req.params; + const { _id: User } = this.req.user; + const query = { + _id, + User, + }; + const album = await WantListModel.findOne(query); + + if (!album) { + throw new ErrorEvent( + 404, + "Mise à jour", + "Impossible de trouver cet album" + ); + } + + const values = await getAlbumDetails(album.discogsId); + + await WantListModel.findOneAndUpdate(query, values, { new: true }); + + return this.getOne(); + } + + /** + * Méthode permettant de supprimer un élément d'une collection + * @return {Boolean} + */ + async deleteOne() { + const res = await WantListModel.findOneAndDelete({ + User: this.req.user._id, + _id: this.req.params.itemId, + }); + + if (res) { + return true; + } + + throw new ErrorEvent( + 404, + "Suppression", + "Impossible de trouver cet album" + ); + } + + async shareOne() { + const { message: status } = this.req.body; + const { itemId: _id } = this.req.params; + const { _id: User } = this.req.user; + const query = { + _id, + User, + }; + + const album = await WantListModel.findOne(query); + + if (!album) { + throw new ErrorEvent( + 404, + "Mise à jour", + "Impossible de trouver cet album" + ); + } + + const { mastodon: mastodonConfig } = this.req.user; + const { publish, token, url } = mastodonConfig; + + if (publish && url && token) { + const M = new Mastodon({ + access_token: token, + api_url: url, + }); + + const media_ids = []; + + if (album.images.length > 0) { + for (let i = 0; i < album.images.length; i += 1) { + if (media_ids.length === 4) { + break; + } + + const filename = `${v4()}.jpg`; + const file = `/tmp/${filename}`; + + // eslint-disable-next-line no-await-in-loop + const { data: buff } = await axios.get( + album.images[i].uri, + { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + }, + responseType: "arraybuffer", + } + ); + + fs.writeFileSync(file, buff); + + // eslint-disable-next-line no-await-in-loop + const { data: media } = await M.post("media", { + file: fs.createReadStream(file), + }); + + const { id } = media; + + media_ids.push(id); + + fs.unlinkSync(file); + } + } + + await M.post("statuses", { status, media_ids }); + } else { + throw new ErrorEvent( + 406, + `Vous n'avez pas configuré vos options de partage sur votre compte` + ); + } + + return true; + } + + /** + * Méthode permettant de créer la page "ma-collection" + */ + async loadMyCollection() { + const artists = await getAllDistincts( + WantListModel, + "artists.name", + this.req.user._id + ); + const formats = await getAllDistincts( + WantListModel, + "formats.name", + this.req.user._id + ); + const years = await getAllDistincts( + WantListModel, + "year", + this.req.user._id + ); + const genres = await getAllDistincts( + WantListModel, + "genres", + this.req.user._id + ); + const styles = await getAllDistincts( + WantListModel, + "styles", + this.req.user._id + ); + + this.setPageContent("artists", artists); + this.setPageContent("formats", formats); + this.setPageContent("years", years); + this.setPageContent("genres", genres); + this.setPageContent("styles", styles); + this.setPageTitle("Ma collection"); + } + + /** + * Méthode permettant d'afficher le détails d'un album + */ + async loadItem() { + const item = await this.getOne(); + + this.setPageContent("item", item); + this.setPageTitle( + `Détails de l'album ${item.title} de ${item.artists_sort}` + ); + } + + /** + * Méthode permettant de choisir un album de manière aléatoire dans la collection d'un utilisateur + */ + async onAir() { + const { _id: User } = this.req.user; + const count = await WantListModel.count({ + User, + }); + + const items = await WantListModel.find( + { + User, + }, + [], + { + skip: Math.floor(Math.random() * (count + 1)), + limit: 1, + } + ); + + this.req.params.itemId = items[0]._id; + + await this.loadItem(); + } + + /** + * Méthode permettant d'afficher des statistiques au sujet de ma collection + */ + async statistics() { + const { _id: User } = this.req.user; + const top = {}; + const byGenres = {}; + const byStyles = {}; + const byFormats = {}; + const top10 = []; + let byStyles10 = []; + const max = this.colors.length - 1; + + const colorsCount = this.colors.length; + + const albums = await WantListModel.find({ + User, + artists: { $exists: true, $not: { $size: 0 } }, + }); + + for (let i = 0; i < albums.length; i += 1) { + const currentFormats = []; + const { artists, genres, styles, formats } = albums[i]; + + // INFO: On regroupe les artistes par nom pour en faire le top10 + for (let j = 0; j < artists.length; j += 1) { + const { name } = artists[j]; + if (!top[name]) { + top[name] = { + name, + count: 0, + }; + } + top[name].count += 1; + } + + // INFO: On regroupe les genres + for (let j = 0; j < genres.length; j += 1) { + const name = genres[j]; + if (!byGenres[name]) { + byGenres[name] = { + name, + count: 0, + color: this.colors[ + Object.keys(byGenres).length % colorsCount + ], + }; + } + + byGenres[name].count += 1; + } + + // INFO: On regroupe les styles + for (let j = 0; j < styles.length; j += 1) { + const name = styles[j]; + if (!byStyles[name]) { + byStyles[name] = { + name, + count: 0, + color: this.colors[ + Object.keys(byStyles).length % colorsCount + ], + }; + } + + byStyles[name].count += 1; + } + + // INFO: On regroupe les formats + for (let j = 0; j < formats.length; j += 1) { + const { name } = formats[j]; + // INFO: On évite qu'un album avec 2 vinyles soit compté 2x + if (!currentFormats.includes(name)) { + if (!byFormats[name]) { + byFormats[name] = { + name, + count: 0, + color: this.colors[ + Object.keys(byFormats).length % colorsCount + ], + }; + } + + byFormats[name].count += 1; + currentFormats.push(name); + } + } + } + + // INFO: On convertit le top en tableau + Object.keys(top).forEach((index) => { + top10.push(top[index]); + }); + + // INFO: On convertit les styles en tableau + Object.keys(byStyles).forEach((index) => { + byStyles10.push(byStyles[index]); + }); + + // INFO: On ordonne les artistes par quantité d'albums + top10.sort((a, b) => (a.count > b.count ? -1 : 1)); + + // INFO: On ordonne les styles par quantité + byStyles10.sort((a, b) => (a.count > b.count ? -1 : 1)); + const tmp = []; + + // INFO: On recupère le top N des styles et on mets le reste dans le label "autre" + for (let i = 0; i < byStyles10.length; i += 1) { + if (i < max) { + tmp.push({ + ...byStyles10[i], + color: this.colors[max - i], + }); + } else if (i === max) { + tmp.push({ + name: "Autre", + count: 0, + color: this.colors[0], + }); + tmp[max].count += byStyles10[i].count; + } else { + tmp[max].count += byStyles10[i].count; + } + } + byStyles10 = tmp; + + this.setPageTitle("Mes statistiques"); + this.setPageContent("top10", top10.splice(0, 10)); + this.setPageContent("byGenres", byGenres); + this.setPageContent("byStyles", byStyles10); + this.setPageContent("byFormats", byFormats); + } + + /** + * Méthode permettant de créer la page "collection/:userId" + */ + async loadPublicCollection() { + const { userId } = this.req.params; + + const user = await UsersModel.findById(userId); + + if (!user || !user.isPublicCollection) { + throw new ErrorEvent( + 401, + "Collection non partagée", + "Cet utilisateur ne souhaite pas partager sa collection" + ); + } + + const artists = await getAllDistincts( + WantListModel, + "artists.name", + userId + ); + const formats = await getAllDistincts( + WantListModel, + "formats.name", + userId + ); + const years = await getAllDistincts(WantListModel, "year", userId); + const genres = await getAllDistincts(WantListModel, "genres", userId); + const styles = await getAllDistincts(WantListModel, "styles", userId); + + this.setPageTitle(`Collection publique de ${user.username}`); + this.setPageContent("username", user.username); + this.setPageContent("artists", artists); + this.setPageContent("formats", formats); + this.setPageContent("years", years); + this.setPageContent("genres", genres); + this.setPageContent("styles", styles); + } +} + +export default Wantlist; diff --git a/src/models/users.js b/src/models/users.js index b8a047a..e0c8a5d 100644 --- a/src/models/users.js +++ b/src/models/users.js @@ -25,6 +25,10 @@ const UserSchema = new mongoose.Schema( }, hash: String, salt: String, + pagination: { + type: Number, + default: 16, + }, isPublicCollection: { type: Boolean, default: false, @@ -34,6 +38,7 @@ const UserSchema = new mongoose.Schema( token: String, url: String, message: String, + wantlist: String, }, }, { diff --git a/src/models/wantlist.js b/src/models/wantlist.js new file mode 100644 index 0000000..1f8b334 --- /dev/null +++ b/src/models/wantlist.js @@ -0,0 +1,37 @@ +import mongoose from "mongoose"; + +const { Schema } = mongoose; + +const WantListSchema = new mongoose.Schema( + { + User: { + type: Schema.Types.ObjectId, + ref: "Users", + }, + discogsId: Number, + year: Number, + released: Date, + uri: String, + artists: Array, + artists_sort: String, + labels: Array, + series: Array, + companies: Array, + formats: Array, + title: String, + country: String, + notes: String, + identifiers: Array, + videos: Array, + genres: Array, + styles: Array, + tracklist: Array, + extraartists: Array, + images: Array, + thumb: String, + thumbType: String, + }, + { timestamps: true } +); + +export default mongoose.model("WantList", WantListSchema); diff --git a/src/routes/api/v1/me.js b/src/routes/api/v1/me.js index 42c46b9..d9e4da0 100644 --- a/src/routes/api/v1/me.js +++ b/src/routes/api/v1/me.js @@ -15,6 +15,16 @@ router const me = new Me(req); const data = await me.patchMe(); + return sendResponse(req, res, data); + } catch (err) { + return next(err); + } + }) + .delete(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const me = new Me(req); + const data = await me.deleteMe(); + return sendResponse(req, res, data); } catch (err) { return next(err); diff --git a/src/routes/api/v1/wantlist.js b/src/routes/api/v1/wantlist.js new file mode 100644 index 0000000..82738b5 --- /dev/null +++ b/src/routes/api/v1/wantlist.js @@ -0,0 +1,84 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import { sendResponse } from "../../../libs/format"; +import Albums from "../../../middleware/Wantlist"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router + .route("/") + .get(async (req, res, next) => { + try { + const albums = new Albums(req, null); + const data = await albums.getAll(); + const { exportFormat = "json" } = req.query; + + switch (exportFormat) { + case "csv": + case "musictopus": + res.header("Content-Type", "text/csv"); + res.attachment("export-musictopus.csv"); + return res.status(200).send(data); + case "xml": + res.header("Content-type", "text/xml"); + res.attachment("export-musictopus.xml"); + return res.status(200).send(data); + case "xls": + return data.write("musictopus.xls", res); + case "json": + default: + return sendResponse(req, res, data); + } + } catch (err) { + return next(err); + } + }) + .post(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const data = await Albums.postAddOne(req); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } + }); + +router + .route("/:itemId") + .patch(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const albums = new Albums(req, null); + const data = await albums.patchOne(); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } + }) + .delete(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const albums = new Albums(req, null); + const data = await albums.deleteOne(); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } + }); + +router + .route("/:itemId/share") + .post(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const albums = new Albums(req, null); + const data = await albums.shareOne(); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } + }); + +export default router; diff --git a/src/routes/index.js b/src/routes/index.js index faa4142..301f25e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -104,8 +104,23 @@ router try { const page = new Pages(req, "ajouter-un-album"); + page.setPageContent("action", "albums"); page.setPageTitle("Ajouter un album"); + render(res, page); + } catch (err) { + next(err); + } + }); +router + .route("/ajouter-a-ma-liste-de-souhaits") + .get(ensureLoggedIn("/connexion"), (req, res, next) => { + try { + const page = new Pages(req, "ajouter-un-album"); + + page.setPageContent("action", "wantlist"); + page.setPageTitle("Ajouter un album à ma liste de souhaits"); + render(res, page); } catch (err) { next(err); diff --git a/src/routes/ma-collection.js b/src/routes/ma-collection.js index 1100bd9..f7a4084 100644 --- a/src/routes/ma-collection.js +++ b/src/routes/ma-collection.js @@ -38,6 +38,23 @@ router } }); +router + .route("/statistiques") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums( + req, + "mon-compte/ma-collection/statistiques" + ); + + await page.statistics(); + + render(res, page); + } catch (err) { + next(err); + } + }); + router .route("/exporter") .get(ensureLoggedIn("/connexion"), async (req, res, next) => { diff --git a/src/routes/wantlist.js b/src/routes/wantlist.js new file mode 100644 index 0000000..6c5394d --- /dev/null +++ b/src/routes/wantlist.js @@ -0,0 +1,99 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import Albums from "../middleware/Wantlist"; + +import render from "../libs/format"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "collection"); + + await page.loadMyCollection(); + + if (page.getPageContent("artists").length > 0) { + render(res, page); + } else { + res.redirect("/ajouter-a-ma-liste-de-souhaits"); + } + } catch (err) { + next(err); + } +}); + +router + .route("/on-air") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "mon-compte/ma-collection/details"); + + await page.onAir(); + + render(res, page); + } catch (err) { + next(err); + } + }); + +router + .route("/statistiques") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums( + req, + "mon-compte/ma-collection/statistiques" + ); + + await page.statistics(); + + render(res, page); + } catch (err) { + next(err); + } + }); + +router + .route("/exporter") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "mon-compte/ma-collection/exporter"); + + page.setPageTitle("Exporter ma collection"); + + render(res, page); + } catch (err) { + next(err); + } + }); +router + .route("/importer") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "mon-compte/ma-collection/importer"); + + page.setPageTitle("Importer une collection"); + + render(res, page); + } catch (err) { + next(err); + } + }); + +router + .route("/:itemId") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Albums(req, "mon-compte/ma-collection/details"); + + await page.loadItem(); + + render(res, page); + } catch (err) { + next(err); + } + }); + +export default router; diff --git a/views/components/album.ejs b/views/components/album.ejs index b834a4d..74d06f2 100644 --- a/views/components/album.ejs +++ b/views/components/album.ejs @@ -1,133 +1,133 @@
-
- -
-
-
-
- Genres -
- -
-
- Styles -
- - {{style}} - -
-
-
-
-
- Pays -
- {{item.country}} -
-
- Année -
- {{item.year}} -
-
- Date de sortie -
- {{item.released}} -
-
-
-
-
- Format -
    -
  • - {{format.name}} - - -
  • -
-
-
-
-
-
- Code barre -
    -
  1. - {{identifier.value}} ({{identifier.type}}) -
  2. -
- -
-
- Label -
    -
  1. - {{label.name}} {{label.catno}} -
  2. -
- Société -
    -
  1. - {{company.entity_type_name}} {{company.name}} -
  2. -
-
-
-
-
-
- Note -
-
-
-
-
-
- Vidéos -
- -
-
-
-
-
\ No newline at end of file +
+ +
+
+
+
+ Genres +
+ +
+
+ Styles +
+ + {{style}} + +
+
+
+
+
+ Pays +
+ {{item.country}} +
+
+ Année +
+ {{item.year}} +
+
+ Date de sortie +
+ {{item.released}} +
+
+
+
+
+ Format +
    +
  • + {{format.name}} + + +
  • +
+
+
+
+
+
+ Code barre +
    +
  1. + {{identifier.value}} ({{identifier.type}}) +
  2. +
+ +
+
+ Label +
    +
  1. + {{label.name}} {{label.catno}} +
  2. +
+ Société +
    +
  1. + {{company.entity_type_name}} {{company.name}} +
  2. +
+
+
+
+
+
+ Note +
+
+
+
+
+
+ Vidéos +
+ +
+
+
+
+ \ No newline at end of file diff --git a/views/components/filters/artist.ejs b/views/components/filters/artist.ejs index 4d806a5..e0a919d 100644 --- a/views/components/filters/artist.ejs +++ b/views/components/filters/artist.ejs @@ -4,7 +4,7 @@ <% for (let i = 0; i < page.artists.length; i += 1 ) { - __append(``); + __append(``); } %> diff --git a/views/components/filters/index.ejs b/views/components/filters/index.ejs index 559a47b..e3347bc 100644 --- a/views/components/filters/index.ejs +++ b/views/components/filters/index.ejs @@ -1,16 +1,16 @@ -
+
<%- include('./artist') %> <%- include('./format') %> <%- include('./sort') %>
-
+
<%- include('./year') %> <%- include('./genre') %> <%- include('./style') %>
- + diff --git a/views/index.ejs b/views/index.ejs index 2fca1a8..198ed3a 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -16,6 +16,9 @@ + + + <% if ( config.matomoUrl ) { %> - - + \ No newline at end of file diff --git a/views/pages/ajouter-un-album.ejs b/views/pages/ajouter-un-album.ejs index f8f7c96..a9a1c2d 100644 --- a/views/pages/ajouter-un-album.ejs +++ b/views/pages/ajouter-un-album.ejs @@ -200,4 +200,5 @@ \ No newline at end of file diff --git a/views/pages/collection.ejs b/views/pages/collection.ejs index 8d18ab2..c16195e 100644 --- a/views/pages/collection.ejs +++ b/views/pages/collection.ejs @@ -3,19 +3,23 @@ %>
-

- <% if ( pageType === 'private' ) { - __append('Ma collection '); - } else { - __append(`Collection de ${page.username}`); - } %> -

- <% if ( pageType === 'private' ) { %> - + <% if (page.action === 'albums') { %> +

+ <% if ( pageType === 'private' ) { + __append('Ma collection '); + } else { + __append(`Collection de ${page.username}`); + } %> +

+ <% if ( pageType === 'private' ) { %> + + <% } %> + <% } else { %> +

Ma liste de souhaits

<% } %> <%- include('../components/filters/index') %> @@ -30,7 +34,7 @@
<% if ( pageType === 'private' ) { %> - {{ renderAlbumTitle(item) }} + {{ renderAlbumTitle(item) }} <% } else { %> {{ item.artists_sort}} - {{ item.title }} @@ -39,7 +43,7 @@
<% if ( pageType === 'private' ) { %> - + <% } else { %> <% } %> @@ -69,31 +73,36 @@
-
- Nombre total d'éléments : {{total}} +
+
+ Nombre total d'éléments : {{total}} +
+ +
- + + <% if ( pageType === 'private' ) { %> @@ -94,5 +98,6 @@ \ No newline at end of file diff --git a/views/pages/mon-compte/ma-collection/exporter.ejs b/views/pages/mon-compte/ma-collection/exporter.ejs index 28cc559..09a0ca1 100644 --- a/views/pages/mon-compte/ma-collection/exporter.ejs +++ b/views/pages/mon-compte/ma-collection/exporter.ejs @@ -1,5 +1,9 @@
-

Exporter ma collection

+ <% if (page.action === 'albums') { %> +

Exporter ma collection

+ <% } else { %> +

Exporter ma liste de souhaits

+ <% } %>

Les formats CSV et Excel sont facilement lisiblent par un humain. Dans ces 2 formats vous trouverez seulement les informations principales de vos albums, à savoir :

@@ -44,4 +48,8 @@ Exporter -
\ No newline at end of file +
+ + \ No newline at end of file diff --git a/views/pages/mon-compte/ma-collection/importer.ejs b/views/pages/mon-compte/ma-collection/importer.ejs index f1a8b49..1383d16 100644 --- a/views/pages/mon-compte/ma-collection/importer.ejs +++ b/views/pages/mon-compte/ma-collection/importer.ejs @@ -1,7 +1,11 @@
-

Importer une collection

+ <% if (page.action === 'albums') { %> +

Importer une collection

+ <% } else { %> +

Importer une liste de souhaits

+ <% } %>

- Il est actuellement possible d'importer une collection provenant de discogs. + Il est actuellement possible d'importer <%= page.action === 'albums' ? "une collection" : "une liste de souhaits" %> provenant de discogs.
Vous devez dans un premier temps vous rendre sur la page Exporter de discogs.
@@ -40,4 +44,8 @@ Importatation terminée -

\ No newline at end of file + + + \ No newline at end of file diff --git a/views/pages/mon-compte/ma-collection/statistiques.ejs b/views/pages/mon-compte/ma-collection/statistiques.ejs new file mode 100644 index 0000000..605c1b7 --- /dev/null +++ b/views/pages/mon-compte/ma-collection/statistiques.ejs @@ -0,0 +1,238 @@ +
+

+ Mes statistiques +

+ +
+
+

Mon top 10

+ + + + + + + + + + <% for ( let i = 0 ; i < page.top10.length ; i += 1 ) { %> + + + + + + <% } %> + +
ArtisteAlbums
<%= i+1 %><%= page.top10[i].name %><%= page.top10[i].count %>
+
+ +
+

Genres

+ +
+ +
+

Styles

+ +
+ +
+

Formats

+ +
+ +
+ +
+

<%= page.currentYear.year %> (<%= page.currentYear.total %> ajout<%= page.currentYear.total > 1 ? 's' : '' %>)

+

Genres

+ +

Styles

+ +

Formats

+ +
+ +
+

<%= page.lastYear.year %> (<%= page.lastYear.total %> ajout<%= page.lastYear.total > 1 ? 's' : '' %>)

+

Genres

+ +

Styles

+ +

Formats

+ +
+
+
+ + diff --git a/views/pages/nous-contacter.ejs b/views/pages/nous-contacter.ejs index f179135..39b1c91 100644 --- a/views/pages/nous-contacter.ejs +++ b/views/pages/nous-contacter.ejs @@ -1,4 +1,4 @@ -
+

Nous contacter

id="contact" method="POST" action="https://formspree.io/f/<%= config.formspreeId %>" <% } %>>