Co-authored-by: dbroqua <contact@darkou.fr>
Reviewed-on: #31
This commit is contained in:
Damien Broqua 2022-03-06 14:38:26 +01:00
parent ac72c1c13c
commit aeb5df067c
28 changed files with 630 additions and 66 deletions

View file

@ -27,9 +27,6 @@
}, },
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.17.0",
"@babel/core": "^7.17.2",
"@babel/preset-env": "^7.16.11",
"eslint": "^8.9.0", "eslint": "^8.9.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
@ -38,11 +35,12 @@
"husky": "^7.0.4", "husky": "^7.0.4",
"lint-staged": "^12.3.3", "lint-staged": "^12.3.3",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"npm-run-all": "^4.1.5", "prettier": "^2.5.1"
"prettier": "^2.5.1",
"rimraf": "^3.0.2"
}, },
"dependencies": { "dependencies": {
"@babel/cli": "^7.17.0",
"@babel/core": "^7.17.2",
"@babel/preset-env": "^7.16.11",
"axios": "^0.26.0", "axios": "^0.26.0",
"connect-ensure-login": "^0.1.1", "connect-ensure-login": "^0.1.1",
"connect-flash": "^0.1.1", "connect-flash": "^0.1.1",
@ -54,14 +52,17 @@
"excel4node": "^1.7.2", "excel4node": "^1.7.2",
"express": "^4.17.2", "express": "^4.17.2",
"express-session": "^1.17.2", "express-session": "^1.17.2",
"joi": "^17.6.0",
"knacss": "^8.0.4", "knacss": "^8.0.4",
"moment": "^2.29.1", "moment": "^2.29.1",
"moment-timezone": "^0.5.34", "moment-timezone": "^0.5.34",
"mongoose": "^6.2.1", "mongoose": "^6.2.1",
"mongoose-unique-validator": "^3.0.0", "mongoose-unique-validator": "^3.0.0",
"npm-run-all": "^4.1.5",
"passport": "^0.5.2", "passport": "^0.5.2",
"passport-http": "^0.3.0", "passport-http": "^0.3.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"rimraf": "^3.0.2",
"sass": "^1.49.7", "sass": "^1.49.7",
"vue": "^3.2.31" "vue": "^3.2.31"
}, },

Binary file not shown.

View file

@ -34,6 +34,8 @@
<glyph glyph-name="moon" unicode="&#xf186;" d="M704 123q-30-5-61-5-102 0-188 50t-137 137-50 188q0 107 58 199-112-33-183-128t-72-214q0-72 29-139t76-113 114-77 139-28q80 0 152 34t123 96z m114 47q-53-113-159-181t-230-68q-87 0-167 34t-136 92-92 137-34 166q0 85 32 163t87 135 132 92 161 38q25 1 34-22 11-23-8-40-48-43-73-101t-26-122q0-83 41-152t111-111 152-41q66 0 127 29 23 10 40-7 8-8 10-19t-2-22z" horiz-adv-x="857.1" /> <glyph glyph-name="moon" unicode="&#xf186;" d="M704 123q-30-5-61-5-102 0-188 50t-137 137-50 188q0 107 58 199-112-33-183-128t-72-214q0-72 29-139t76-113 114-77 139-28q80 0 152 34t123 96z m114 47q-53-113-159-181t-230-68q-87 0-167 34t-136 92-92 137-34 166q0 85 32 163t87 135 132 92 161 38q25 1 34-22 11-23-8-40-48-43-73-101t-26-122q0-83 41-152t111-111 152-41q66 0 127 29 23 10 40-7 8-8 10-19t-2-22z" horiz-adv-x="857.1" />
<glyph glyph-name="share" unicode="&#xf1e0;" d="M679 279q74 0 126-53t52-126-52-126-126-53-127 53-52 126q0 7 1 19l-201 100q-51-48-121-48-75 0-127 53t-52 126 52 126 127 53q70 0 121-48l201 100q-1 12-1 19 0 74 52 126t127 53 126-53 52-126-52-126-126-53q-71 0-122 48l-201-100q1-12 1-19t-1-19l201-100q51 48 122 48z" horiz-adv-x="857.1" />
<glyph glyph-name="trash" unicode="&#xf1f8;" d="M286 82v393q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q8 0 13 5t5 13z m143 0v393q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q8 0 13 5t5 13z m142 0v393q0 8-5 13t-12 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q7 0 12 5t5 13z m-303 554h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q23 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" /> <glyph glyph-name="trash" unicode="&#xf1f8;" d="M286 82v393q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q8 0 13 5t5 13z m143 0v393q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q8 0 13 5t5 13z m142 0v393q0 8-5 13t-12 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q7 0 12 5t5 13z m-303 554h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q23 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
<glyph glyph-name="blind" unicode="&#xf29d;" d="M204 677q-35 0-61 25t-26 62q0 35 26 61t61 25 61-25 26-61q0-37-26-62t-61-25z m308-359q0-28-17-37t-35-4-27 19l-205 244q-4 7-8 9t-6 1l-1-2q-4-4 2-12l68-77 1-198-90-255q-38-107-52-130-8-15-15-18-28-15-58-1-16 7-23 24t-5 32q1 9 110 345l3 232-48-91 20-124q2-14-1-24t-8-15-10-9-10-4l-4-1q-10-2-19 1t-13 9-8 13-4 10-2 6l-25 167 118 212q12 19 63 19 41 0 59-22l237-291q4-3 8-9l1-2 0-1q4-7 4-16z m-225-83q24-64 49-126t39-94l13-31q21-51 24-69 6-39-20-54-20-13-37-9t-28 12-17 19h0q-4 9-5 14l-69 196z m460-331q17-27 17-32 0-3-2-4-5-2-8 1t-8 14-9 17q-64 96-236 369 1 0 4 1t3 2l2 1q6 5 6 10z" horiz-adv-x="785.7" /> <glyph glyph-name="blind" unicode="&#xf29d;" d="M204 677q-35 0-61 25t-26 62q0 35 26 61t61 25 61-25 26-61q0-37-26-62t-61-25z m308-359q0-28-17-37t-35-4-27 19l-205 244q-4 7-8 9t-6 1l-1-2q-4-4 2-12l68-77 1-198-90-255q-38-107-52-130-8-15-15-18-28-15-58-1-16 7-23 24t-5 32q1 9 110 345l3 232-48-91 20-124q2-14-1-24t-8-15-10-9-10-4l-4-1q-10-2-19 1t-13 9-8 13-4 10-2 6l-25 167 118 212q12 19 63 19 41 0 59-22l237-291q4-3 8-9l1-2 0-1q4-7 4-16z m-225-83q24-64 49-126t39-94l13-31q21-51 24-69 6-39-20-54-20-13-37-9t-28 12-17 19h0q-4 9-5 14l-69 196z m460-331q17-27 17-32 0-3-2-4-5-2-8 1t-8 14-9 17q-64 96-236 369 1 0 4 1t3 2l2 1q6 5 6 10z" horiz-adv-x="785.7" />

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -2,13 +2,16 @@
* Fonction permettant d'afficher un message dans un toastr * Fonction permettant d'afficher un message dans un toastr
* @param {String} message * @param {String} message
*/ */
function showToastr(message) { function showToastr(message, success = false) {
let x = document.getElementById("toastr"); let x = document.getElementById("toastr");
if ( message ) { if ( message ) {
x.getElementsByTagName("SPAN")[0].innerHTML = 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); setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000);
}; };

View file

@ -1,4 +1,9 @@
.ma-collection { .collection {
h1 {
i {
cursor: pointer;
}
}
.filters { .filters {
display: flex; display: flex;
justify-content: end; justify-content: end;

View file

@ -52,6 +52,23 @@ $pagination-hover-color: rgb(115, 151, 186);
--box-shadow-color: #{rgba($nord4, 0.35)}; --box-shadow-color: #{rgba($nord4, 0.35)};
--border-color: #{$nord4}; --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"] { [data-theme="dark"] {

13
sass/composants.scss Normal file
View file

@ -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;
}
}
}

View file

@ -82,4 +82,8 @@ html {
@include respond-to("small-up") { @include respond-to("small-up") {
display: initial; display: initial;
} }
}
.is-danger {
color: $nord12;
} }

View file

@ -46,6 +46,7 @@
.icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */
.icon-sun:before { content: '\f185'; } /* '' */ .icon-sun:before { content: '\f185'; } /* '' */
.icon-moon:before { content: '\f186'; } /* '' */ .icon-moon:before { content: '\f186'; } /* '' */
.icon-share:before { content: '\f1e0'; } /* '' */
.icon-trash:before { content: '\f1f8'; } /* '' */ .icon-trash:before { content: '\f1f8'; } /* '' */
.icon-blind:before { content: '\f29d'; } /* '' */ .icon-blind:before { content: '\f29d'; } /* '' */

View file

@ -43,5 +43,6 @@
@import './error'; @import './error';
@import './home'; @import './home';
@import './ajouter-un-album'; @import './ajouter-un-album';
@import './ma-collection'; @import './collection';
@import './ma-collection-details'; @import './ma-collection-details';
@import './composants';

View file

@ -13,6 +13,11 @@
color: $button-alternate-color; color: $button-alternate-color;
border-radius: 6px; border-radius: 6px;
&.success {
background-color: $success-color;
color: $button-font-color;
}
&.show { &.show {
visibility: visible; visibility: visible;
animation: toastrFadein 0.5s, toastrFadeout 0.5s 2.5s; animation: toastrFadein 0.5s, toastrFadeout 0.5s 2.5s;

View file

@ -13,9 +13,11 @@ import { isXhr } from "./helpers";
import indexRouter from "./routes"; import indexRouter from "./routes";
import maCollectionRouter from "./routes/ma-collection"; import maCollectionRouter from "./routes/ma-collection";
import collectionRouter from "./routes/collection";
import importAlbumRouterApiV1 from "./routes/api/v1/albums"; import importAlbumRouterApiV1 from "./routes/api/v1/albums";
import importSearchRouterApiV1 from "./routes/api/v1/search"; import importSearchRouterApiV1 from "./routes/api/v1/search";
import importMeRouterApiV1 from "./routes/api/v1/me";
// Mongoose schema init // Mongoose schema init
require("./models/users"); require("./models/users");
@ -82,8 +84,10 @@ app.use(
app.use("/", indexRouter); app.use("/", indexRouter);
app.use("/ma-collection", maCollectionRouter); app.use("/ma-collection", maCollectionRouter);
app.use("/collection", collectionRouter);
app.use("/api/v1/albums", importAlbumRouterApiV1); app.use("/api/v1/albums", importAlbumRouterApiV1);
app.use("/api/v1/search", importSearchRouterApiV1); app.use("/api/v1/search", importSearchRouterApiV1);
app.use("/api/v1/me", importMeRouterApiV1);
// Handle 404 // Handle 404
app.use((req, res) => { app.use((req, res) => {
@ -113,7 +117,10 @@ app.use((error, req, res, next) => {
} else { } else {
res.status(error.errorCode || 500); res.status(error.errorCode || 500);
res.render("index", { 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, errorCode: error.errorCode || 500,
viewname: "error", viewname: "error",
user: req.user || null, user: req.user || null,

View file

@ -4,9 +4,10 @@
class ErrorEvent extends Error { class ErrorEvent extends Error {
/** /**
* @param {Number} errorCode * @param {Number} errorCode
* @param {String} title
* @param {Mixed} ...params * @param {Mixed} ...params
*/ */
constructor(errorCode, ...params) { constructor(errorCode, title, ...params) {
super(...params); super(...params);
if (Error.captureStackTrace) { if (Error.captureStackTrace) {
@ -14,6 +15,7 @@ class ErrorEvent extends Error {
} }
this.errorCode = parseInt(errorCode, 10); this.errorCode = parseInt(errorCode, 10);
this.title = title;
this.date = new Date(); this.date = new Date();
} }
} }

View file

@ -5,12 +5,19 @@ import xl from "excel4node";
import Pages from "./Pages"; import Pages from "./Pages";
import AlbumsModel from "../models/albums"; import AlbumsModel from "../models/albums";
import UsersModel from "../models/users";
import ErrorEvent from "../libs/error"; import ErrorEvent from "../libs/error";
/** /**
* Classe permettant la gestion des albums d'un utilisateur * Classe permettant la gestion des albums d'un utilisateur
*/ */
class Albums extends Pages { class Albums extends Pages {
/**
* Méthode permettant de remplacer certains cartactères par leur équivalents html
* @param {String} str
*
* @return {String}
*/
static replaceSpecialChars(str) { static replaceSpecialChars(str) {
if (!str) { if (!str) {
return ""; return "";
@ -487,7 +494,7 @@ class Albums extends Pages {
static async getAllDistincts(field, user) { static async getAllDistincts(field, user) {
const distincts = await AlbumsModel.find( const distincts = await AlbumsModel.find(
{ {
user, User: user,
}, },
[], [],
{ {
@ -513,8 +520,11 @@ class Albums extends Pages {
order = "asc", order = "asc",
artists_sort, artists_sort,
format, format,
userId: collectionUserId,
} = this.req.query; } = this.req.query;
let userId = this.req.user?._id;
const where = {}; const where = {};
if (artists_sort) { if (artists_sort) {
@ -524,8 +534,35 @@ class Albums extends Pages {
where["formats.name"] = format; 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({ const count = await AlbumsModel.count({
user: this.req.user._id, User: userId,
...where, ...where,
}); });
@ -547,7 +584,7 @@ class Albums extends Pages {
const rows = await AlbumsModel.find( const rows = await AlbumsModel.find(
{ {
user: this.req.user._id, User: userId,
...where, ...where,
}, },
[], [],
@ -619,6 +656,29 @@ class Albums extends Pages {
this.setPageContent("item", item); 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; export default Albums;

45
src/middleware/Me.js Normal file
View file

@ -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;

View file

@ -1,5 +1,7 @@
/* eslint-disable func-names */ /* eslint-disable func-names */
/* eslint-disable no-invalid-this */ /* eslint-disable no-invalid-this */
/* eslint-disable no-param-reassign */
import mongoose from "mongoose"; import mongoose from "mongoose";
import uniqueValidator from "mongoose-unique-validator"; import uniqueValidator from "mongoose-unique-validator";
import crypto from "crypto"; import crypto from "crypto";
@ -23,8 +25,20 @@ const UserSchema = new mongoose.Schema(
}, },
hash: String, hash: String,
salt: 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é" }); UserSchema.plugin(uniqueValidator, { message: "est déjà utilisé" });

View file

@ -9,7 +9,7 @@ const router = express.Router();
router router
.route("/") .route("/")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => { .get(async (req, res, next) => {
try { try {
const albums = new Albums(req); const albums = new Albums(req);
const data = await albums.getAll(); const data = await albums.getAll();

24
src/routes/api/v1/me.js Normal file
View file

@ -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;

22
src/routes/collection.js Normal file
View file

@ -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;

View file

@ -10,7 +10,7 @@ const router = express.Router();
router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => { router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try { try {
const page = new Albums(req, "mon-compte/ma-collection"); const page = new Albums(req, "mon-compte/ma-collection/index");
await page.loadMyCollection(); await page.loadMyCollection();

View file

@ -5,7 +5,9 @@
<img src="/img/404.svg" alt="Erreur 404" style="max-height: 400px;" /> <img src="/img/404.svg" alt="Erreur 404" style="max-height: 400px;" />
</p> </p>
<% } %> <% } %>
<% if ( process.env.NODE_ENV !== 'production' ) { %>
<div> <div>
<pre><%= page.error %></pre> <pre><%= page.error %></pre>
</div> </div>
<% } %>
</main> </main>

View file

@ -1,5 +1,8 @@
<main class="layout-maxed ma-collection" id="app"> <main class="layout-maxed collection" id="app">
<h1>Ma collection</h1> <h1>
Collection de <%= page.username %>
</h1>
<div class="filters"> <div class="filters">
<div class="field"> <div class="field">
<label for="artist">Artiste</label> <label for="artist">Artiste</label>
@ -40,12 +43,11 @@
<div class="grid grid-cols-1 md:grid-cols-2 list"> <div class="grid grid-cols-1 md:grid-cols-2 list">
<div class="item" v-if="!loading" v-for="item in items"> <div class="item" v-if="!loading" v-for="item in items">
<span class="title"> <span class="title">
<a :href="'/ma-collection/' + item._id">{{ item.artists_sort}} - {{ item.title }}</a> {{ item.artists_sort}} - {{ item.title }}
<i class="icon-trash" @click="showConfirmDelete(item._id)"></i>
</span> </span>
<div class="grid grid-cols-2 md:grid-cols-4"> <div class="grid grid-cols-2 md:grid-cols-4">
<div> <div>
<a :href="'/ma-collection/' + item._id"><img :src="item.thumb" :alt="item.title" /></a> <img :src="item.thumb" :alt="item.title" />
</div> </div>
<div class="md:col-span-3"> <div class="md:col-span-3">
<span><strong>Année :</strong> {{ item.year }}</span> <span><strong>Année :</strong> {{ item.year }}</span>
@ -91,23 +93,14 @@
</template> </template>
</ul> </ul>
</nav> </nav>
<div class="modal" :class="{'is-visible': showModalDelete}">
<div class="modal-background"></div>
<div class="modal-card">
<header></header>
<section>
Êtes-vous sûr de vouloir supprimer cet album ?
</section>
<footer>
<button class="button is-primary" @click="deleteItem">Supprimer</button>
<button class="button" @click="toggleModal">Annuler</button>
</footer>
</div>
</div>
</main> </main>
<script> <script>
const {
protocol,
host
} = window.location;
Vue.createApp({ Vue.createApp({
data() { data() {
return { return {
@ -122,8 +115,7 @@
sortOrder: 'artists_sort-asc', sortOrder: 'artists_sort-asc',
sort: 'artists_sort', sort: 'artists_sort',
order: 'asc', order: 'asc',
itemId: null, userId: "<%= params.userId %>",
showModalDelete: false,
} }
}, },
created() { created() {
@ -133,7 +125,7 @@
fetch() { fetch() {
this.loading = true; this.loading = true;
let url = `/api/v1/albums?page=${this.page}&limit=${this.limit}&sort=${this.sort}&order=${this.order}`; let url = `/api/v1/albums?userId=${this.userId}&page=${this.page}&limit=${this.limit}&sort=${this.sort}&order=${this.order}`;
if ( this.artist ) { if ( this.artist ) {
url += `&artists_sort=${this.artist}`; url += `&artists_sort=${this.artist}`;
} }
@ -149,7 +141,7 @@
}) })
.catch((err) => { .catch((err) => {
showToastr(err.response?.data?.message || "Impossible de charger votre collection"); showToastr(err.response?.data?.message || "Impossible de charger cette collection");
}) })
.finally(() => { .finally(() => {
this.loading = false; this.loading = false;
@ -187,25 +179,6 @@
this.fetch(); this.fetch();
}, },
toggleModal() {
this.showModalDelete = !this.showModalDelete;
},
showConfirmDelete(itemId) {
this.itemId = itemId;
this.toggleModal();
},
deleteItem() {
axios.delete(`/api/v1/albums/${this.itemId}`)
.then( () => {
this.fetch();
})
.catch((err) => {
showToastr(err.response?.data?.message || "Impossible de supprimer cet album");
})
.finally(() => {
this.toggleModal();
});
}
} }
}).mount('#app'); }).mount('#app');
</script> </script>

View file

@ -1,8 +1,9 @@
<main class="layout-maxed" id="app"> <main class="layout-maxed composants" id="app">
<h1>Les composants</h1> <h1>Les composants</h1>
<ul> <ul>
<li><a href="#titres">Les titres</a></li> <li><a href="#titres">Les titres</a></li>
<li><a href="#couleurs">Les couleurs</a></li>
<li><a href="#grilles">Les grilles</a></li> <li><a href="#grilles">Les grilles</a></li>
<li><a href="#boutons">Les boutons</a></li> <li><a href="#boutons">Les boutons</a></li>
<li><a href="#formulaires">Les formulaires</a></li> <li><a href="#formulaires">Les formulaires</a></li>
@ -24,6 +25,87 @@
<h5>Titre de niveau 5</h5> <h5>Titre de niveau 5</h5>
<h6>Titre de niveau 6</h6> <h6>Titre de niveau 6</h6>
<h2 id="couleurs">Les couleurs</h2>
<h3>Polar Night</h3>
<div class="grid grid-cols-5 gap-5">
<div class="couleur">
<div style="background-color: var(--nord0);">&nbsp;</div>
nord0
</div>
<div class="couleur">
<div style="background-color: var(--nord1);">&nbsp;</div>
nord1
</div>
<div class="couleur">
<div style="background-color: var(--nord2);">&nbsp;</div>
nord2
</div>
<div class="couleur">
<div style="background-color: var(--nord3);">&nbsp;</div>
nord3
</div>
</div>
<h3>Snow Storm</h3>
<div class="grid grid-cols-5 gap-5">
<div class="couleur">
<div style="background-color: var(--nord4);">&nbsp;</div>
nord4
</div>
<div class="couleur">
<div style="background-color: var(--nord5);">&nbsp;</div>
nord5
</div>
<div class="couleur">
<div style="background-color: var(--nord6);">&nbsp;</div>
nord6
</div>
</div>
<h3>Frost</h3>
<div class="grid grid-cols-5 gap-5">
<div class="couleur">
<div style="background-color: var(--nord7);">&nbsp;</div>
nord7
</div>
<div class="couleur">
<div style="background-color: var(--nord8);">&nbsp;</div>
nord8
</div>
<div class="couleur">
<div style="background-color: var(--nord9);">&nbsp;</div>
nord9
</div>
<div class="couleur">
<div style="background-color: var(--nord10);">&nbsp;</div>
nord10
</div>
</div>
<h3>Aurora</h3>
<div class="grid grid-cols-5 gap-5">
<div class="couleur">
<div style="background-color: var(--nord11);">&nbsp;</div>
nord11
</div>
<div class="couleur">
<div style="background-color: var(--nord12);">&nbsp;</div>
nord12
</div>
<div class="couleur">
<div style="background-color: var(--nord13);">&nbsp;</div>
nord13
</div>
<div class="couleur">
<div style="background-color: var(--nord14);">&nbsp;</div>
nord14
</div>
<div class="couleur">
<div style="background-color: var(--nord15);">&nbsp;</div>
nord15
</div>
</div>
<p>
Vous pourrez trouver plus d'informations sur le <a href="https://www.nordtheme.com/" target="_blank" rel="noopener noreferrer">site offciel</a> du projet nord.
</p>
<h2 id="grilles">Les grilles</h2> <h2 id="grilles">Les grilles</h2>
<p> <p>
Se référer à la documentation de <a href="https://www.knacss.com/doc.html#grid" target="_blank" rel="noopener noreferrer">Knacss</a>. Se référer à la documentation de <a href="https://www.knacss.com/doc.html#grid" target="_blank" rel="noopener noreferrer">Knacss</a>.
@ -225,13 +307,15 @@
<i class="icon-link-ext">.icon-link-ext</i> <i class="icon-link-ext">.icon-link-ext</i>
<i class="icon-heart">.icon-heart</i> <i class="icon-heart">.icon-heart</i>
<i class="icon-eye">.icon-eye</i> <i class="icon-eye">.icon-eye</i>
<i class="icon-left-open">.icon-left-open</i>
<i class="icon-right-open">.icon-right-open</i>
<i class="icon-export">.icon-export</i>
<i class="icon-share">.icon-share</i>
<i class="icon-spin">.icon-spin</i> <i class="icon-spin">.icon-spin</i>
<i class="icon-sun">.icon-sun</i> <i class="icon-sun">.icon-sun</i>
<i class="icon-moon">.icon-moon</i> <i class="icon-moon">.icon-moon</i>
<i class="icon-trash">.icon-trash</i> <i class="icon-trash">.icon-trash</i>
<i class="icon-blind">.icon-blind</i> <i class="icon-blind">.icon-blind</i>
<i class="icon-left-open">.icon-left-open</i>
<i class="icon-right-open">.icon-right-open</i>
<h2 id="listes">Les listes</h2> <h2 id="listes">Les listes</h2>
<div class="grid grid-cols-1 md:grid-cols-2 list"> <div class="grid grid-cols-1 md:grid-cols-2 list">

View file

@ -1,4 +1,4 @@
<main class="layout-maxed ma-collection-exporter" id="app"> <main class="layout-maxed" id="app">
<h1>Exporter ma collection</h1> <h1>Exporter ma collection</h1>
<p> <p>
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 : 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 @@
<input type="radio" name="format" v-model="format" value="musictopus" id="musictopus"> <input type="radio" name="format" v-model="format" value="musictopus" id="musictopus">
<label for="musictopus">MusicTopus</label> <label for="musictopus">MusicTopus</label>
</div> </div>
<button type="submit" class="button is-primary my-16"> <button type="submit" class="button is-primary my-16">
<i class="icon-export"></i> <i class="icon-export"></i>
Exporter Exporter

View file

@ -0,0 +1,279 @@
<main class="layout-maxed collection" id="app">
<h1>
Ma collection
<i class="icon-share" @click="toggleModalShare" aria-label="Partager ma collection" title="Votre collection sera visible en lecture aux personnes ayant le lien de partage"></i>
</h1>
<a :href="shareLink" v-if="isPublicCollection" target="_blank">
<i class="icon-share"></i> Voir ma collection partagée
</a>
<div class="filters">
<div class="field">
<label for="artist">Artiste</label>
<select id="artist" v-model="artist" @change="changeFilter">
<option value="">Tous</option>
<%
for (let i = 0; i < page.artists.length; i += 1 ) {
__append(`<option value="${page.artists[i]}">${page.artists[i]}</option>`);
}
%>
</select>
</div>
<div class="field">
<label for="format">Format</label>
<select id="format" v-model="format" @change="changeFilter">
<option value="">Tous</option>
<%
for (let i = 0; i < page.formats.length; i += 1 ) {
__append(`<option value="${page.formats[i]}">${page.formats[i]}</option>`);
}
%>
</select>
</div>
<div class="field">
<label for="sortOrder">Trier par</label>
<select id="sortOrder" v-model="sortOrder" @change="changeSort">
<option value="artists_sort-asc">Artiste (A-Z)</option>
<option value="artists_sort-desc">Artiste (Z-A)</option>
<option value="year-asc">Année (A-Z)</option>
<option value="year-desc">Année (Z-A)</option>
<option value="country-asc">Pays (A-Z)</option>
<option value="country-desc">Pays (Z-A)</option>
<option value="formats.name-asc">Format (A-Z)</option>
<option value="formats.name-desc">Format (Z-A)</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 list">
<div class="item" v-if="!loading" v-for="item in items">
<span class="title">
<a :href="'/ma-collection/' + item._id">{{ item.artists_sort}} - {{ item.title }}</a>
<i class="icon-trash" @click="showConfirmDelete(item._id)"></i>
</span>
<div class="grid grid-cols-2 md:grid-cols-4">
<div>
<a :href="'/ma-collection/' + item._id"><img :src="item.thumb" :alt="item.title" /></a>
</div>
<div class="md:col-span-3">
<span><strong>Année :</strong> {{ item.year }}</span>
<br />
<span><strong>Pays :</strong> {{ item.country }}</span>
<br />
<span>
<strong>Format : </strong>
<span v-for="(format, index) in item.formats">
{{ format.name }}
<template v-if="format.descriptions">
(<template v-for="(description, j) in format.descriptions">
{{description}}<template v-if="j < format.descriptions.length - 1">, </template>
</template>)
</template>
<template v-if="index < item.formats.length - 1">, </template>
</span>
</span>
<br />
<span><strong>Genre :</strong> <template v-for="(genre, index) in item.genres">{{ genre }}<template v-if="index < item.genres.length - 1">, </template></template></span>
<br />
<span><strong>Style :</strong> <template v-for="(style, index) in item.styles">{{ style }}<template v-if="index < item.styles.length - 1">, </template></template></span>
</div>
</div>
</div>
</div>
<div class="total">
<strong>Nombre total d'éléments : </strong>{{total}}
</div>
<nav class="pagination" role="navigation" aria-label="Pagination">
<ul class="pagination-list">
<template v-for="p in Array.from({length: totalPages}, (v, i) => (i+1))">
<template v-if="p < 2 || p > (totalPages - 1) || (page - 1) <= p && page + 1 >= p">
<li>
<a class="pagination-link" :class="{'is-current': p === page}" @click="goTo(p)" aria-label="Aller à la page {{p}}">{{ p }}</a>
</li>
</template>
<template v-if="(page - 3 === p && page - 2 > 1) || (page + 2 === p && page + 2 < totalPages - 1)">
<li>
<a class="pagination-link is-disabled">…</a>
</li>
</template>
</template>
</ul>
</nav>
<div class="modal" :class="{'is-visible': showModalDelete}">
<div class="modal-background"></div>
<div class="modal-card">
<header></header>
<section>
Êtes-vous sûr de vouloir supprimer cet album ?
</section>
<footer>
<button class="button is-primary" @click="deleteItem">Supprimer</button>
<button class="button" @click="toggleModal">Annuler</button>
</footer>
</div>
</div>
<div class="modal" :class="{'is-visible': showModalShare}">
<div class="modal-background"></div>
<div class="modal-card">
<header>
Partager ma collection
</header>
<section>
<template v-if="!isPublicCollection">
Votre collection sera visible de toute personne disposant du lien suivant :
<br />
<a :href="shareLink" target="_blank">{{shareLink}}</a>
<br />
Ce lien permet uniquement de visualiser l'ensemble de votre collection mais ne perment <strong class="is-danger">en aucun cas</strong> de la modifier.
<br />
Vous pourrez à tout moment supprimer le lien de partage en cliquant à nouveau sur l'icône <i class="icon-share"></i> sur votre collection.
</template>
<template v-if="isPublicCollection">
Vous êtes sur le point de rendre votre collection privée.
<br />
Toute les personnes ayant le lien partagé ne pourront plus accéder à votre collection.
<br />
Vous pourrez à tout moment rendre à nouveau votre collection publique en cliquant sur l'icône <i class="icon-share"></i>.
</template>
</section>
<footer>
<button v-if="!isPublicCollection" class="button is-primary" @click="shareCollection">Partager</button>
<button v-if="isPublicCollection" class="button is-danger" @click="shareCollection">Supprimer</button>
<button class="button" @click="toggleModalShare">Annuler</button>
</footer>
</div>
</div>
</main>
<script>
const {
protocol,
host
} = window.location;
Vue.createApp({
data() {
return {
loading: false,
items: [],
total: 0,
page: 1,
totalPages: 1,
limit: 16,
artist: '',
format: '',
sortOrder: 'artists_sort-asc',
sort: 'artists_sort',
order: 'asc',
itemId: null,
showModalDelete: false,
showModalShare: false,
shareLink: `${protocol}//${host}/collection/<%= user._id %>`,
isPublicCollection: <%= user.isPublicCollection ? 'true' : 'false' %>,
}
},
created() {
this.fetch();
},
methods: {
fetch() {
this.loading = true;
let url = `/api/v1/albums?page=${this.page}&limit=${this.limit}&sort=${this.sort}&order=${this.order}`;
if ( this.artist ) {
url += `&artists_sort=${this.artist}`;
}
if ( this.format ) {
url += `&format=${this.format}`;
}
axios.get(url)
.then( response => {
this.items = response.data.rows;
this.total = response.data.count;
this.totalPages = parseInt(response.data.count / this.limit) + (response.data.count % this.limit > 0 ? 1 : 0);
})
.catch((err) => {
showToastr(err.response?.data?.message || "Impossible de charger votre collection");
})
.finally(() => {
this.loading = false;
});
},
next(event) {
event.preventDefault();
this.page += 1;
this.fetch();
},
previous(event) {
event.preventDefault();
this.page -= 1;
this.fetch();
},
goTo(page) {
this.page = page;
this.fetch();
},
changeSort() {
const [sort,order] = this.sortOrder.split('-');
this.sort = sort;
this.order = order;
this.page = 1;
this.fetch();
},
changeFilter() {
this.page = 1;
this.fetch();
},
toggleModal() {
this.showModalDelete = !this.showModalDelete;
},
toggleModalShare() {
this.showModalShare = !this.showModalShare;
},
showConfirmDelete(itemId) {
this.itemId = itemId;
this.toggleModal();
},
deleteItem() {
axios.delete(`/api/v1/albums/${this.itemId}`)
.then( () => {
this.fetch();
})
.catch((err) => {
showToastr(err.response?.data?.message || "Impossible de supprimer cet album");
})
.finally(() => {
this.toggleModal();
});
},
shareCollection() {
axios.patch(`/api/v1/me`, {
isPublicCollection: !this.isPublicCollection,
})
.then( (res) => {
this.isPublicCollection = res.data.isPublicCollection;
if ( this.isPublicCollection ) {
showToastr("Votre collection est désormais publique", true);
} else {
showToastr("Votre collection n'est plus partagée", true);
}
})
.catch((err) => {
showToastr(err.response?.data?.message || "Impossible de supprimer cet album");
})
.finally(() => {
this.toggleModalShare();
});
}
}
}).mount('#app');
</script>