Version 1.1 #43
28 changed files with 630 additions and 66 deletions
13
package.json
13
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"
|
||||
},
|
||||
|
|
Binary file not shown.
|
@ -34,6 +34,8 @@
|
|||
|
||||
<glyph glyph-name="moon" unicode="" 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="" 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="" 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="" 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.5 KiB After Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
.ma-collection {
|
||||
.collection {
|
||||
h1 {
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
justify-content: end;
|
|
@ -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"] {
|
||||
|
|
13
sass/composants.scss
Normal file
13
sass/composants.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -83,3 +83,7 @@ html {
|
|||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.is-danger {
|
||||
color: $nord12;
|
||||
}
|
|
@ -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'; } /* '' */
|
||||
|
||||
|
|
|
@ -43,5 +43,6 @@
|
|||
@import './error';
|
||||
@import './home';
|
||||
@import './ajouter-un-album';
|
||||
@import './ma-collection';
|
||||
@import './collection';
|
||||
@import './ma-collection-details';
|
||||
@import './composants';
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
45
src/middleware/Me.js
Normal file
45
src/middleware/Me.js
Normal 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;
|
|
@ -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é" });
|
||||
|
|
|
@ -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();
|
||||
|
|
24
src/routes/api/v1/me.js
Normal file
24
src/routes/api/v1/me.js
Normal 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
22
src/routes/collection.js
Normal 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;
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
<img src="/img/404.svg" alt="Erreur 404" style="max-height: 400px;" />
|
||||
</p>
|
||||
<% } %>
|
||||
<% if ( process.env.NODE_ENV !== 'production' ) { %>
|
||||
<div>
|
||||
<pre><%= page.error %></pre>
|
||||
</div>
|
||||
<% } %>
|
||||
</main>
|
|
@ -1,5 +1,8 @@
|
|||
<main class="layout-maxed ma-collection" id="app">
|
||||
<h1>Ma collection</h1>
|
||||
<main class="layout-maxed collection" id="app">
|
||||
<h1>
|
||||
Collection de <%= page.username %>
|
||||
</h1>
|
||||
|
||||
<div class="filters">
|
||||
<div class="field">
|
||||
<label for="artist">Artiste</label>
|
||||
|
@ -40,12 +43,11 @@
|
|||
<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>
|
||||
{{ item.artists_sort}} - {{ item.title }}
|
||||
</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>
|
||||
<img :src="item.thumb" :alt="item.title" />
|
||||
</div>
|
||||
<div class="md:col-span-3">
|
||||
<span><strong>Année :</strong> {{ item.year }}</span>
|
||||
|
@ -91,23 +93,14 @@
|
|||
</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>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const {
|
||||
protocol,
|
||||
host
|
||||
} = window.location;
|
||||
|
||||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
|
@ -122,8 +115,7 @@
|
|||
sortOrder: 'artists_sort-asc',
|
||||
sort: 'artists_sort',
|
||||
order: 'asc',
|
||||
itemId: null,
|
||||
showModalDelete: false,
|
||||
userId: "<%= params.userId %>",
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
@ -133,7 +125,7 @@
|
|||
fetch() {
|
||||
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 ) {
|
||||
url += `&artists_sort=${this.artist}`;
|
||||
}
|
||||
|
@ -149,7 +141,7 @@
|
|||
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(err.response?.data?.message || "Impossible de charger votre collection");
|
||||
showToastr(err.response?.data?.message || "Impossible de charger cette collection");
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
|
@ -187,25 +179,6 @@
|
|||
|
||||
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');
|
||||
</script>
|
|
@ -1,8 +1,9 @@
|
|||
<main class="layout-maxed" id="app">
|
||||
<main class="layout-maxed composants" id="app">
|
||||
<h1>Les composants</h1>
|
||||
|
||||
<ul>
|
||||
<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="#boutons">Les boutons</a></li>
|
||||
<li><a href="#formulaires">Les formulaires</a></li>
|
||||
|
@ -24,6 +25,87 @@
|
|||
<h5>Titre de niveau 5</h5>
|
||||
<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);"> </div>
|
||||
nord0
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord1);"> </div>
|
||||
nord1
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord2);"> </div>
|
||||
nord2
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord3);"> </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);"> </div>
|
||||
nord4
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord5);"> </div>
|
||||
nord5
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord6);"> </div>
|
||||
nord6
|
||||
</div>
|
||||
</div>
|
||||
<h3>Frost</h3>
|
||||
<div class="grid grid-cols-5 gap-5">
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord7);"> </div>
|
||||
nord7
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord8);"> </div>
|
||||
nord8
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord9);"> </div>
|
||||
nord9
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord10);"> </div>
|
||||
nord10
|
||||
</div>
|
||||
</div>
|
||||
<h3>Aurora</h3>
|
||||
<div class="grid grid-cols-5 gap-5">
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord11);"> </div>
|
||||
nord11
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord12);"> </div>
|
||||
nord12
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord13);"> </div>
|
||||
nord13
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord14);"> </div>
|
||||
nord14
|
||||
</div>
|
||||
<div class="couleur">
|
||||
<div style="background-color: var(--nord15);"> </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>
|
||||
<p>
|
||||
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-heart">.icon-heart</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-sun">.icon-sun</i>
|
||||
<i class="icon-moon">.icon-moon</i>
|
||||
<i class="icon-trash">.icon-trash</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>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 list">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<main class="layout-maxed ma-collection-exporter" id="app">
|
||||
<main class="layout-maxed" id="app">
|
||||
<h1>Exporter ma collection</h1>
|
||||
<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 :
|
||||
|
|
279
views/pages/mon-compte/ma-collection/index.ejs
Normal file
279
views/pages/mon-compte/ma-collection/index.ejs
Normal 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>
|
Loading…
Reference in a new issue