diff --git a/package.json b/package.json index 8c53d5a..b5aea27 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,6 @@ }, "license": "GPL-3.0-or-later", "devDependencies": { - "@babel/cli": "^7.17.0", - "@babel/core": "^7.17.2", - "@babel/preset-env": "^7.16.11", "eslint": "^8.9.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^8.3.0", @@ -38,11 +35,12 @@ "husky": "^7.0.4", "lint-staged": "^12.3.3", "nodemon": "^2.0.15", - "npm-run-all": "^4.1.5", - "prettier": "^2.5.1", - "rimraf": "^3.0.2" + "prettier": "^2.5.1" }, "dependencies": { + "@babel/cli": "^7.17.0", + "@babel/core": "^7.17.2", + "@babel/preset-env": "^7.16.11", "axios": "^0.26.0", "connect-ensure-login": "^0.1.1", "connect-flash": "^0.1.1", @@ -54,14 +52,17 @@ "excel4node": "^1.7.2", "express": "^4.17.2", "express-session": "^1.17.2", + "joi": "^17.6.0", "knacss": "^8.0.4", "moment": "^2.29.1", "moment-timezone": "^0.5.34", "mongoose": "^6.2.1", "mongoose-unique-validator": "^3.0.0", + "npm-run-all": "^4.1.5", "passport": "^0.5.2", "passport-http": "^0.3.0", "passport-local": "^1.0.0", + "rimraf": "^3.0.2", "sass": "^1.49.7", "vue": "^3.2.31" }, diff --git a/public/font/icon.eot b/public/font/icon.eot index 8664abe..a17c0f6 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 51f7015..d5ac2f6 100644 --- a/public/font/icon.svg +++ b/public/font/icon.svg @@ -34,6 +34,8 @@ + + diff --git a/public/font/icon.ttf b/public/font/icon.ttf index 1f0de7e..4ccfa6d 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 6474d86..3262198 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 72207f3..f73baf4 100644 Binary files a/public/font/icon.woff2 and b/public/font/icon.woff2 differ diff --git a/public/js/main.js b/public/js/main.js index 994b2ae..895683a 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -2,13 +2,16 @@ * Fonction permettant d'afficher un message dans un toastr * @param {String} message */ - function showToastr(message) { + function showToastr(message, success = false) { let x = document.getElementById("toastr"); if ( message ) { x.getElementsByTagName("SPAN")[0].innerHTML = message; } - x.className = `${x.className} show`; + x.className = `${x.className} show`.replace("sucess", ""); + if ( success ) { + x.className = `${x.className} success`; + } setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000); }; diff --git a/sass/ma-collection.scss b/sass/collection.scss similarity index 90% rename from sass/ma-collection.scss rename to sass/collection.scss index af22f2a..6412fe1 100644 --- a/sass/ma-collection.scss +++ b/sass/collection.scss @@ -1,4 +1,9 @@ -.ma-collection { +.collection { + h1 { + i { + cursor: pointer; + } + } .filters { display: flex; justify-content: end; diff --git a/sass/colors.scss b/sass/colors.scss index 5759f90..f100c6b 100644 --- a/sass/colors.scss +++ b/sass/colors.scss @@ -52,6 +52,23 @@ $pagination-hover-color: rgb(115, 151, 186); --box-shadow-color: #{rgba($nord4, 0.35)}; --border-color: #{$nord4}; + + --nord0: #{$nord0}; + --nord1: #{$nord1}; + --nord2: #{$nord2}; + --nord3: #{$nord3}; + --nord4: #{$nord4}; + --nord5: #{$nord5}; + --nord6: #{$nord6}; + --nord7: #{$nord7}; + --nord8: #{$nord8}; + --nord9: #{$nord9}; + --nord10: #{$nord10}; + --nord11: #{$nord11}; + --nord12: #{$nord12}; + --nord13: #{$nord13}; + --nord14: #{$nord14}; + --nord15: #{$nord15}; } [data-theme="dark"] { diff --git a/sass/composants.scss b/sass/composants.scss new file mode 100644 index 0000000..b9d4e65 --- /dev/null +++ b/sass/composants.scss @@ -0,0 +1,13 @@ +.composants { + .couleur { + margin-bottom: 1rem; + text-align: center; + + border: 1px solid var(--input-active-color); + box-shadow: var(--box-shadow-color) 0px 3px 6px 0px; + + div { + height: 56px; + } + } +} \ No newline at end of file diff --git a/sass/global.scss b/sass/global.scss index 491b9ca..454cd14 100644 --- a/sass/global.scss +++ b/sass/global.scss @@ -82,4 +82,8 @@ html { @include respond-to("small-up") { display: initial; } +} + +.is-danger { + color: $nord12; } \ No newline at end of file diff --git a/sass/icons.scss b/sass/icons.scss index 70ce584..4250307 100644 --- a/sass/icons.scss +++ b/sass/icons.scss @@ -46,6 +46,7 @@ .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'; } /* '' */ diff --git a/sass/index.scss b/sass/index.scss index aa7557a..c54fc3c 100644 --- a/sass/index.scss +++ b/sass/index.scss @@ -43,5 +43,6 @@ @import './error'; @import './home'; @import './ajouter-un-album'; -@import './ma-collection'; -@import './ma-collection-details'; \ No newline at end of file +@import './collection'; +@import './ma-collection-details'; +@import './composants'; \ No newline at end of file diff --git a/sass/toast.scss b/sass/toast.scss index 21d1730..46777e7 100644 --- a/sass/toast.scss +++ b/sass/toast.scss @@ -13,6 +13,11 @@ color: $button-alternate-color; border-radius: 6px; + &.success { + background-color: $success-color; + color: $button-font-color; + } + &.show { visibility: visible; animation: toastrFadein 0.5s, toastrFadeout 0.5s 2.5s; diff --git a/src/app.js b/src/app.js index 0b32d82..6d1e507 100644 --- a/src/app.js +++ b/src/app.js @@ -13,9 +13,11 @@ import { isXhr } from "./helpers"; import indexRouter from "./routes"; import maCollectionRouter from "./routes/ma-collection"; +import collectionRouter from "./routes/collection"; import importAlbumRouterApiV1 from "./routes/api/v1/albums"; import importSearchRouterApiV1 from "./routes/api/v1/search"; +import importMeRouterApiV1 from "./routes/api/v1/me"; // Mongoose schema init require("./models/users"); @@ -82,8 +84,10 @@ app.use( app.use("/", indexRouter); app.use("/ma-collection", maCollectionRouter); +app.use("/collection", collectionRouter); app.use("/api/v1/albums", importAlbumRouterApiV1); app.use("/api/v1/search", importSearchRouterApiV1); +app.use("/api/v1/me", importMeRouterApiV1); // Handle 404 app.use((req, res) => { @@ -113,7 +117,10 @@ app.use((error, req, res, next) => { } else { res.status(error.errorCode || 500); res.render("index", { - page: { title: "500: Oups… le serveur a crashé !", error }, + page: { + title: error.title || "500: Oups… le serveur a crashé !", + error, + }, errorCode: error.errorCode || 500, viewname: "error", user: req.user || null, diff --git a/src/libs/error.js b/src/libs/error.js index 562265f..13470f2 100644 --- a/src/libs/error.js +++ b/src/libs/error.js @@ -4,9 +4,10 @@ class ErrorEvent extends Error { /** * @param {Number} errorCode + * @param {String} title * @param {Mixed} ...params */ - constructor(errorCode, ...params) { + constructor(errorCode, title, ...params) { super(...params); if (Error.captureStackTrace) { @@ -14,6 +15,7 @@ class ErrorEvent extends Error { } this.errorCode = parseInt(errorCode, 10); + this.title = title; this.date = new Date(); } } diff --git a/src/middleware/Albums.js b/src/middleware/Albums.js index 4796207..8025973 100644 --- a/src/middleware/Albums.js +++ b/src/middleware/Albums.js @@ -5,12 +5,19 @@ import xl from "excel4node"; import Pages from "./Pages"; import AlbumsModel from "../models/albums"; +import UsersModel from "../models/users"; import ErrorEvent from "../libs/error"; /** * Classe permettant la gestion des albums d'un utilisateur */ class Albums extends Pages { + /** + * Méthode permettant de remplacer certains cartactères par leur équivalents html + * @param {String} str + * + * @return {String} + */ static replaceSpecialChars(str) { if (!str) { return ""; @@ -487,7 +494,7 @@ class Albums extends Pages { static async getAllDistincts(field, user) { const distincts = await AlbumsModel.find( { - user, + User: user, }, [], { @@ -513,8 +520,11 @@ class Albums extends Pages { order = "asc", artists_sort, format, + userId: collectionUserId, } = this.req.query; + let userId = this.req.user?._id; + const where = {}; if (artists_sort) { @@ -524,8 +534,35 @@ class Albums extends Pages { where["formats.name"] = format; } + if (!this.req.user && !collectionUserId) { + throw new ErrorEvent( + 401, + "Cette collection n'est pas publique", + "Cette collection n'est pas publique" + ); + } + + if (collectionUserId) { + const userIsSharingCollection = await UsersModel.findById( + collectionUserId + ); + + if ( + !userIsSharingCollection || + !userIsSharingCollection.isPublicCollection + ) { + throw new ErrorEvent( + 401, + "Cette collection n'est pas publique", + "Cette collection n'est pas publique" + ); + } + + userId = userIsSharingCollection._id; + } + const count = await AlbumsModel.count({ - user: this.req.user._id, + User: userId, ...where, }); @@ -547,7 +584,7 @@ class Albums extends Pages { const rows = await AlbumsModel.find( { - user: this.req.user._id, + User: userId, ...where, }, [], @@ -619,6 +656,29 @@ class Albums extends Pages { this.setPageContent("item", item); } + + /** + * 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, + "Cet utilisateur ne souhaite pas partager sa collection" + ); + } + + const artists = await Albums.getAllDistincts("artists_sort", userId); + const formats = await Albums.getAllDistincts("formats.name", userId); + + this.setPageContent("username", user.username); + this.setPageContent("artists", artists); + this.setPageContent("formats", formats); + } } export default Albums; diff --git a/src/middleware/Me.js b/src/middleware/Me.js new file mode 100644 index 0000000..ae2712c --- /dev/null +++ b/src/middleware/Me.js @@ -0,0 +1,45 @@ +import Joi from "joi"; + +import UsersModel from "../models/users"; + +/** + * Classe permettant la gestion de l'utilisateur connecté + */ +class Me { + constructor(req) { + this.req = req; + } + + /** + * Méthode permettant de modifier le profil d'un utilisateur + * @return {Object} + */ + async patchMe() { + const { body, user } = this.req; + + const schema = Joi.object({ + isPublicCollection: Joi.boolean(), + }); + + const value = await schema.validateAsync(body); + const update = await UsersModel.findByIdAndUpdate( + user._id, + { $set: value }, + { new: true } + ); + + await new Promise((resolve, reject) => { + this.req.login(update, (err) => { + if (err) { + return reject(err); + } + + return resolve(null); + }); + }); + + return update; + } +} + +export default Me; diff --git a/src/models/users.js b/src/models/users.js index 0807ccb..96629c0 100644 --- a/src/models/users.js +++ b/src/models/users.js @@ -1,5 +1,7 @@ /* eslint-disable func-names */ /* eslint-disable no-invalid-this */ +/* eslint-disable no-param-reassign */ + import mongoose from "mongoose"; import uniqueValidator from "mongoose-unique-validator"; import crypto from "crypto"; @@ -23,8 +25,20 @@ const UserSchema = new mongoose.Schema( }, hash: String, salt: String, + isPublicCollection: { + type: Boolean, + default: false, + }, }, - { timestamps: true } + { + timestamps: true, + toJSON: { + transform(doc, ret) { + delete ret.hash; + delete ret.salt; + }, + }, + } ); UserSchema.plugin(uniqueValidator, { message: "est déjà utilisé" }); diff --git a/src/routes/api/v1/albums.js b/src/routes/api/v1/albums.js index c8f0f1a..e1caa1f 100644 --- a/src/routes/api/v1/albums.js +++ b/src/routes/api/v1/albums.js @@ -9,7 +9,7 @@ const router = express.Router(); router .route("/") - .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + .get(async (req, res, next) => { try { const albums = new Albums(req); const data = await albums.getAll(); diff --git a/src/routes/api/v1/me.js b/src/routes/api/v1/me.js new file mode 100644 index 0000000..42c46b9 --- /dev/null +++ b/src/routes/api/v1/me.js @@ -0,0 +1,24 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import { sendResponse } from "../../../libs/format"; + +import Me from "../../../middleware/Me"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router + .route("/") + .patch(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const me = new Me(req); + const data = await me.patchMe(); + + return sendResponse(req, res, data); + } catch (err) { + return next(err); + } + }); + +export default router; diff --git a/src/routes/collection.js b/src/routes/collection.js new file mode 100644 index 0000000..6584992 --- /dev/null +++ b/src/routes/collection.js @@ -0,0 +1,22 @@ +import express from "express"; + +import Albums from "../middleware/Albums"; + +import render from "../libs/format"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router.route("/:userId").get(async (req, res, next) => { + try { + const page = new Albums(req, "collection"); + + await page.loadPublicCollection(); + + render(res, page); + } catch (err) { + next(err); + } +}); + +export default router; diff --git a/src/routes/ma-collection.js b/src/routes/ma-collection.js index 51ff0ee..653c844 100644 --- a/src/routes/ma-collection.js +++ b/src/routes/ma-collection.js @@ -10,7 +10,7 @@ const router = express.Router(); router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => { try { - const page = new Albums(req, "mon-compte/ma-collection"); + const page = new Albums(req, "mon-compte/ma-collection/index"); await page.loadMyCollection(); diff --git a/views/error.ejs b/views/error.ejs index b0e5112..8f80430 100644 --- a/views/error.ejs +++ b/views/error.ejs @@ -5,7 +5,9 @@ Erreur 404

<% } %> + <% if ( process.env.NODE_ENV !== 'production' ) { %>
<%= page.error %>
+ <% } %> \ No newline at end of file diff --git a/views/pages/mon-compte/ma-collection.ejs b/views/pages/collection.ejs similarity index 78% rename from views/pages/mon-compte/ma-collection.ejs rename to views/pages/collection.ejs index b754ccf..da22489 100644 --- a/views/pages/mon-compte/ma-collection.ejs +++ b/views/pages/collection.ejs @@ -1,5 +1,8 @@ -
-

Ma collection

+
+

+ Collection de <%= page.username %> +

+
@@ -40,12 +43,11 @@
- {{ item.artists_sort}} - {{ item.title }} - + {{ item.artists_sort}} - {{ item.title }}
- +
Année : {{ item.year }} @@ -91,23 +93,14 @@ - -
diff --git a/views/pages/composants.ejs b/views/pages/composants.ejs index 7c17b38..dc4f8f2 100644 --- a/views/pages/composants.ejs +++ b/views/pages/composants.ejs @@ -1,8 +1,9 @@ -
+

Les composants

  • Les titres
  • +
  • Les couleurs
  • Les grilles
  • Les boutons
  • Les formulaires
  • @@ -24,6 +25,87 @@
    Titre de niveau 5
    Titre de niveau 6
    +

    Les couleurs

    +

    Polar Night

    +
    +
    +
     
    + nord0 +
    +
    +
     
    + nord1 +
    +
    +
     
    + nord2 +
    +
    +
     
    + nord3 +
    +
    +

    Snow Storm

    +
    +
    +
     
    + nord4 +
    +
    +
     
    + nord5 +
    +
    +
     
    + nord6 +
    +
    +

    Frost

    +
    +
    +
     
    + nord7 +
    +
    +
     
    + nord8 +
    +
    +
     
    + nord9 +
    +
    +
     
    + nord10 +
    +
    +

    Aurora

    +
    +
    +
     
    + nord11 +
    +
    +
     
    + nord12 +
    +
    +
     
    + nord13 +
    +
    +
     
    + nord14 +
    +
    +
     
    + nord15 +
    +
    +

    + Vous pourrez trouver plus d'informations sur le site offciel du projet nord. +

    +

    Les grilles

    Se référer à la documentation de Knacss. @@ -225,13 +307,15 @@ .icon-link-ext .icon-heart .icon-eye + .icon-left-open + .icon-right-open + .icon-export + .icon-share .icon-spin .icon-sun .icon-moon .icon-trash .icon-blind - .icon-left-open - .icon-right-open

    Les listes

    diff --git a/views/pages/mon-compte/ma-collection/exporter.ejs b/views/pages/mon-compte/ma-collection/exporter.ejs index dc1de77..13e3f98 100644 --- a/views/pages/mon-compte/ma-collection/exporter.ejs +++ b/views/pages/mon-compte/ma-collection/exporter.ejs @@ -1,4 +1,4 @@ -
    +

    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 : @@ -38,7 +38,7 @@

    - + + + + + + +
+ +