diff --git a/.eslintrc.js b/.eslintrc.js index c8f8a9d..143bf10 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,7 +16,7 @@ module.exports = { 'no-underscore-dangle': [ 'error', { - allow: ['_id', 'artists_sort'], + allow: ['_id', 'artists_sort', 'type_'], }, ], 'camelcase': [ diff --git a/package.json b/package.json index 77f10de..8c53d5a 100644 --- a/package.json +++ b/package.json @@ -51,10 +51,12 @@ "debug": "^4.3.3", "disconnect": "^1.2.2", "ejs": "^3.1.6", + "excel4node": "^1.7.2", "express": "^4.17.2", "express-session": "^1.17.2", "knacss": "^8.0.4", "moment": "^2.29.1", + "moment-timezone": "^0.5.34", "mongoose": "^6.2.1", "mongoose-unique-validator": "^3.0.0", "passport": "^0.5.2", diff --git a/public/font/icon.svg b/public/font/icon.svg index 9370b56..51f7015 100644 --- a/public/font/icon.svg +++ b/public/font/icon.svg @@ -24,6 +24,8 @@ + + diff --git a/sass/forms.scss b/sass/forms.scss index 0f4aab7..fd302e0 100644 --- a/sass/forms.scss +++ b/sass/forms.scss @@ -3,6 +3,10 @@ display: flex; flex-direction: column; + &.inline { + flex-direction: row; + } + &.has-addons { display: flex; justify-content: flex-start; @@ -40,6 +44,28 @@ } } + input[type="radio"] { + border-radius: 50%; + appearance: none; + width: 1.2rem; + height: 1.2rem; + vertical-align: text-bottom; + outline: 0; + box-shadow: inset 0 0 0 1px var(--input-active-color); + background-color: #fff; + transition: background-size .15s; + cursor: pointer; + + &:checked { + box-shadow: inset 0 0 0 4px var(--input-active-color); + } + } + + input[type="radio"] + label, + label + input[type="radio"] { + margin-left: 12px; + } + select { appearance: none; background-image: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20standalone%3D%22no%22%3F%3E%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20style%3D%22isolation%3Aisolate%22%20viewBox%3D%220%200%2020%2020%22%20width%3D%2220%22%20height%3D%2220%22%3E%3Cpath%20d%3D%22%20M%209.96%2011.966%20L%203.523%205.589%20C%202.464%204.627%200.495%206.842%201.505%207.771%20L%201.505%207.771%20L%208.494%2014.763%20C%209.138%2015.35%2010.655%2015.369%2011.29%2014.763%20L%2011.29%2014.763%20L%2018.49%207.771%20C%2019.557%206.752%2017.364%204.68%2016.262%205.725%20L%2016.262%205.725%20L%209.96%2011.966%20Z%20%22%20fill%3D%22inherit%22/%3E%3C/svg%3E"); diff --git a/sass/icons.scss b/sass/icons.scss index 05250a2..70ce584 100644 --- a/sass/icons.scss +++ b/sass/icons.scss @@ -41,6 +41,7 @@ .icon-eye:before { content: '\e806'; } /* '' */ .icon-left-open:before { content: '\e807'; } /* '' */ .icon-right-open:before { content: '\e808'; } /* '' */ +.icon-export:before { content: '\e809'; } /* '' */ .icon-spin:before { content: '\e839'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-sun:before { content: '\f185'; } /* '' */ @@ -60,4 +61,4 @@ 100% { transform: rotate(359deg); } -} \ No newline at end of file +} diff --git a/sass/index.scss b/sass/index.scss index 7ff8d31..aa7557a 100644 --- a/sass/index.scss +++ b/sass/index.scss @@ -19,8 +19,8 @@ // COMPOSANTS (à ajouter au besoin) // @import "../node_modules/knacss/sass/components/button"; // @import "components/burger"; -// @import "components/checkbox"; -// @import "components/radio"; +// @import "../node_modules/knacss/sass/components/checkbox"; +@import "../node_modules/knacss/sass/components/radio"; // @import "../node_modules/knacss/sass/components/select"; // @import "components/quote"; diff --git a/src/app.js b/src/app.js index d64e63c..0b32d82 100644 --- a/src/app.js +++ b/src/app.js @@ -12,6 +12,7 @@ import config, { env, mongoDbUri, secret } from "./config"; import { isXhr } from "./helpers"; import indexRouter from "./routes"; +import maCollectionRouter from "./routes/ma-collection"; import importAlbumRouterApiV1 from "./routes/api/v1/albums"; import importSearchRouterApiV1 from "./routes/api/v1/search"; @@ -80,6 +81,7 @@ app.use( ); app.use("/", indexRouter); +app.use("/ma-collection", maCollectionRouter); app.use("/api/v1/albums", importAlbumRouterApiV1); app.use("/api/v1/search", importSearchRouterApiV1); diff --git a/src/middleware/Albums.js b/src/middleware/Albums.js index 7e4d5d7..4796207 100644 --- a/src/middleware/Albums.js +++ b/src/middleware/Albums.js @@ -1,4 +1,6 @@ import moment from "moment"; +import momenttz from "moment-timezone"; +import xl from "excel4node"; import Pages from "./Pages"; @@ -9,6 +11,451 @@ import ErrorEvent from "../libs/error"; * Classe permettant la gestion des albums d'un utilisateur */ class Albums extends Pages { + static replaceSpecialChars(str) { + if (!str) { + return ""; + } + let final = str.toString(); + const find = ["&", "<", ">"]; + const replace = ["&", "<", ">"]; + + for (let i = 0; i < find.length; i += 1) { + final = final.replace(new RegExp(find[i], "g"), replace[i]); + } + + return final; + } + + /** + * Méthode permettant de convertir les rows en csv + * @param {Array} rows + * + * @return {string} + */ + static async convertToCsv(rows) { + let data = + "Artiste;Titre;Genre;Styles;Pays;Année;Date de sortie;Format\n\r"; + + for (let i = 0; i < rows.length; i += 1) { + const { + artists_sort, + title, + genres, + styles, + country, + year, + released, + formats, + } = rows[i]; + + let format = ""; + for (let j = 0; j < formats.length; j += 1) { + format += `${format !== "" ? ", " : ""}${formats[j].name}`; + } + + data += `${artists_sort};${title};${genres.join()};${styles.join()};${country};${year};${released};${format}\n\r`; + } + + return data; + } + + /** + * Méthode permettant de convertir les rows en fichier xls + * @param {Array} rows + * + * @return {Object} + */ + static async convertToXls(rows) { + const wb = new xl.Workbook(); + const ws = wb.addWorksheet("MusicTopus"); + + const headerStyle = wb.createStyle({ + font: { + color: "#FFFFFF", + size: 11, + }, + fill: { + type: "pattern", + patternType: "solid", + bgColor: "#595959", + fgColor: "#595959", + }, + }); + const style = wb.createStyle({ + font: { + color: "#000000", + size: 11, + }, + numberFormat: "0000", + }); + + const header = [ + "Artiste", + "Titre", + "Genre", + "Styles", + "Pays", + "Année", + "Date de sortie", + "Format", + ]; + for (let i = 0; i < header.length; i += 1) { + ws.cell(1, i + 1) + .string(header[i]) + .style(headerStyle); + } + + for (let i = 0; i < rows.length; i += 1) { + const currentRow = i + 2; + const { + artists_sort, + title, + genres, + styles, + country, + year, + released, + formats, + } = rows[i]; + + let format = ""; + for (let j = 0; j < formats.length; j += 1) { + format += `${format !== "" ? ", " : ""}${formats[j].name}`; + } + + 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); + if (country) { + ws.cell(currentRow, 5).string(country).style(style); + } + if (year) { + ws.cell(currentRow, 6).number(year).style(style); + } + if (released) { + ws.cell(currentRow, 7) + .date(momenttz.tz(released, "Europe/Paris").hour(12)) + .style({ numberFormat: "dd/mm/yyyy" }); + } + ws.cell(currentRow, 8).string(format).style(style); + } + + return wb; + } + + /** + * Méthode permettant de convertir les rows en csv pour importer dans MusicTopus + * @param {Array} rows + * + * @return {string} + */ + static async convertToXml(rows) { + let data = '\n\r'; + + for (let i = 0; i < rows.length; i += 1) { + const { + discogsId, + year, + released, + uri, + artists, + artists_sort, + labels, + series, + companies, + formats, + title, + country, + notes, + identifiers, + videos, + genres, + styles, + tracklist, + extraartists, + images, + thumb, + } = rows[i]; + + let artistsList = ""; + let labelList = ""; + let serieList = ""; + let companiesList = ""; + let formatsList = ""; + let identifiersList = ""; + let videosList = ""; + let genresList = ""; + let stylesList = ""; + let tracklistList = ""; + let extraartistsList = ""; + let imagesList = ""; + + for (let j = 0; j < artists.length; j += 1) { + artistsList += ` + ${Albums.replaceSpecialChars(artists[j].name)} + ${Albums.replaceSpecialChars(artists[j].anv)} + ${Albums.replaceSpecialChars(artists[j].join)} + ${Albums.replaceSpecialChars(artists[j].role)} + ${Albums.replaceSpecialChars( + artists[j].tracks + )} + ${Albums.replaceSpecialChars(artists[j].id)} + ${Albums.replaceSpecialChars( + artists[j].resource_url + )} + ${Albums.replaceSpecialChars( + artists[j].thumbnail_url + )} + `; + } + + for (let j = 0; j < labels.length; j += 1) { + labelList += ` + `; + } + + for (let j = 0; j < series.length; j += 1) { + serieList += ` + ${Albums.replaceSpecialChars(series[j].name)} + ${Albums.replaceSpecialChars(series[j].catno)} + ${Albums.replaceSpecialChars( + series[j].entity_type + )} + ${Albums.replaceSpecialChars( + series[j].entity_type_name + )} + ${Albums.replaceSpecialChars(series[j].id)} + ${Albums.replaceSpecialChars( + series[j].resource_url + )} + ${Albums.replaceSpecialChars( + series[j].thumbnail_url + )} + + `; + } + + for (let j = 0; j < companies.length; j += 1) { + companiesList += ` + ${Albums.replaceSpecialChars(companies[j].name)} + ${Albums.replaceSpecialChars(companies[j].catno)} + ${Albums.replaceSpecialChars( + companies[j].entity_type + )} + ${Albums.replaceSpecialChars( + companies[j].entity_type_name + )} + ${Albums.replaceSpecialChars(companies[j].id)} + ${Albums.replaceSpecialChars( + companies[j].resource_url + )} + ${Albums.replaceSpecialChars( + companies[j].thumbnail_url + )} + + `; + } + + for (let j = 0; j < formats.length; j += 1) { + let descriptions = ""; + if (formats[j].descriptions) { + for ( + let k = 0; + k < formats[j].descriptions.length; + k += 1 + ) { + descriptions += `${formats[j].descriptions[k]} + `; + } + } + formatsList += ` + ${Albums.replaceSpecialChars(formats[j].name)} + ${Albums.replaceSpecialChars(formats[j].qty)} + ${Albums.replaceSpecialChars(formats[j].text)} + + ${descriptions} + + + `; + } + + for (let j = 0; j < identifiers.length; j += 1) { + identifiersList += ` + ${Albums.replaceSpecialChars(identifiers[j].type)} + ${Albums.replaceSpecialChars( + identifiers[j].value + )} + ${Albums.replaceSpecialChars( + identifiers[j].description + )} + + `; + } + + for (let j = 0; j < videos.length; j += 1) { + videosList += ` + `; + } + + for (let j = 0; j < genres.length; j += 1) { + genresList += `${Albums.replaceSpecialChars( + genres[j] + )} + `; + } + + for (let j = 0; j < styles.length; j += 1) { + stylesList += ` + `; + } + + for (let j = 0; j < tracklist.length; j += 1) { + tracklistList += ` + ${Albums.replaceSpecialChars(tracklist[j].title)} + + `; + } + + for (let j = 0; j < extraartists.length; j += 1) { + extraartistsList += ` + ${Albums.replaceSpecialChars(extraartists[j].name)} + ${Albums.replaceSpecialChars(extraartists[j].anv)} + ${Albums.replaceSpecialChars(extraartists[j].join)} + ${Albums.replaceSpecialChars(extraartists[j].role)} + ${Albums.replaceSpecialChars( + extraartists[j].tracks + )} + ${Albums.replaceSpecialChars(extraartists[j].id)} + ${Albums.replaceSpecialChars( + extraartists[j].resource_url + )} + ${Albums.replaceSpecialChars( + extraartists[j].thumbnail_url + )} + + `; + } + + for (let j = 0; j < images.length; j += 1) { + imagesList += ` + ${Albums.replaceSpecialChars(images[j].uri)} + ${Albums.replaceSpecialChars( + images[j].resource_url + )} + ${Albums.replaceSpecialChars( + images[j].resource_url + )} + + `; + } + + data += ` + + ${discogsId} + ${Albums.replaceSpecialChars(title)} + ${Albums.replaceSpecialChars(artists_sort)} + + ${artistsList} + + ${year} + ${Albums.replaceSpecialChars(country)} + ${released} + ${uri} + ${thumb} + + ${labelList} + + + ${serieList} + + + ${companiesList} + + + ${formatsList} + + ${Albums.replaceSpecialChars(notes)} + + ${identifiersList} + + + ${videosList} + + + ${genresList} + + + ${stylesList} + + + ${tracklistList} + + + ${extraartistsList} + + + ${imagesList} + + `; + } + + return `${data}`; + } + + /** + * Méthode permettant de convertir les rows en csv pour importer dans MusicTopus + * @param {Array} rows + * + * @return {string} + */ + static async convertToMusicTopus(rows) { + let data = "itemId;createdAt;updatedAt\n\r"; + + for (let i = 0; i < rows.length; i += 1) { + const { discogsId, createdAt, updatedAt } = rows[i]; + + data += `${discogsId};${createdAt};${updatedAt}\n\r`; + } + + data += "v1.0"; + + return data; + } + /** * Méthode permettant d'ajouter un album dans une collection * @param {Object} req @@ -59,16 +506,15 @@ class Albums extends Pages { */ async getAll() { const { - page = 1, - limit = 4, + page, + limit, + exportFormat = "json", sort = "artists_sort", order = "asc", artists_sort, format, } = this.req.query; - const skip = (page - 1) * limit; - const where = {}; if (artists_sort) { @@ -83,25 +529,47 @@ class Albums extends Pages { ...where, }); + let params = { + sort: { + [sort]: order.toLowerCase() === "asc" ? 1 : -1, + }, + }; + + if (page && limit) { + const skip = (page - 1) * limit; + + params = { + ...params, + skip, + limit, + }; + } + const rows = await AlbumsModel.find( { user: this.req.user._id, ...where, }, [], - { - skip, - limit, - sort: { - [sort]: order.toLowerCase() === "asc" ? 1 : -1, - }, - } + params ); - return { - rows, - count, - }; + switch (exportFormat) { + case "csv": + return Albums.convertToCsv(rows); + case "xls": + return Albums.convertToXls(rows); + case "xml": + return Albums.convertToXml(rows); + case "musictopus": + return Albums.convertToMusicTopus(rows); + case "json": + default: + return { + rows, + count, + }; + } } /** diff --git a/src/routes/api/v1/albums.js b/src/routes/api/v1/albums.js index 5d0c68c..164ea40 100644 --- a/src/routes/api/v1/albums.js +++ b/src/routes/api/v1/albums.js @@ -13,10 +13,24 @@ router try { const albums = new Albums(req); const data = await albums.getAll(); + const { exportFormat = "json" } = req.query; - sendResponse(req, res, data); + switch (exportFormat) { + case "csv": + case "musictopus": + res.header("Content-Type", "text/csv"); + return res.status(200).send(data); + case "xml": + res.header("Content-type", "text/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) { - next(err); + return next(err); } }) .post(ensureLoggedIn("/connexion"), async (req, res, next) => { diff --git a/src/routes/index.js b/src/routes/index.js index fca9847..95c9a45 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,7 +4,6 @@ import { ensureLoggedIn } from "connect-ensure-login"; import Pages from "../middleware/Pages"; import Auth from "../middleware/Auth"; -import Albums from "../middleware/Albums"; import render from "../libs/format"; @@ -89,38 +88,6 @@ router } }); -router - .route("/ma-collection") - .get(ensureLoggedIn("/connexion"), async (req, res, next) => { - try { - const page = new Albums(req, "mon-compte/ma-collection"); - - await page.loadMyCollection(); - - if (page.getPageContent("artists").length > 0) { - render(res, page); - } else { - res.redirect("/ajouter-un-album"); - } - } catch (err) { - next(err); - } - }); - -router - .route("/ma-collection/: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); - } - }); - router.route("/nous-contacter").get(async (req, res, next) => { try { const page = new Pages(req, "nous-contacter"); diff --git a/src/routes/ma-collection.js b/src/routes/ma-collection.js new file mode 100644 index 0000000..51ff0ee --- /dev/null +++ b/src/routes/ma-collection.js @@ -0,0 +1,53 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import Albums from "../middleware/Albums"; + +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, "mon-compte/ma-collection"); + + await page.loadMyCollection(); + + if (page.getPageContent("artists").length > 0) { + render(res, page); + } else { + res.redirect("/ajouter-un-album"); + } + } 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"); + + 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/index.ejs b/views/index.ejs index 8d2364f..ddf0b8c 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -68,6 +68,9 @@ Ma collection + + Exporter ma collection + <% } %> diff --git a/views/pages/mon-compte/ma-collection/exporter.ejs b/views/pages/mon-compte/ma-collection/exporter.ejs new file mode 100644 index 0000000..dc1de77 --- /dev/null +++ b/views/pages/mon-compte/ma-collection/exporter.ejs @@ -0,0 +1,68 @@ +
+

Exporter ma collection

+

+ 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 : +

+
    +
  • Nom de l'artiste
  • +
  • Nom de l'album
  • +
  • Liste des genres
  • +
  • Liste des styles
  • +
  • Pays (ou région) de distribution
  • +
  • Année de sortie
  • +
  • Date de sortie
  • +
  • Format de l'album
  • +
+

+ Le format XML quand a lui est un peu moins lisible par un humain, même s'il reste un fichier texte. Dans ce format vous retrouverez toute les informations de vos albums. +

+

+ Enfin le dernier format, MusicTopus, vous permettra d'exporter votre collection afin de l'importer ensuite sur une autre instance MusicTopus. +

+
+ Choisir le format d'export + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + \ No newline at end of file