Compare commits
11 commits
cc25b83b2e
...
bc3bb3b554
Author | SHA1 | Date | |
---|---|---|---|
bc3bb3b554 | |||
da08aa0222 | |||
2da6afa06d | |||
b8b3df2932 | |||
adea857666 | |||
6320764743 | |||
d473899b20 | |||
827dcb9ccc | |||
1377b4c0c1 | |||
080471eb37 | |||
befdfa35a6 |
31 changed files with 684 additions and 67 deletions
13
README.md
13
README.md
|
@ -18,7 +18,9 @@ Vous pouvez, si vous le souhaitez héberger l'application sur votre propre serve
|
|||
|
||||
### Prérequis
|
||||
|
||||
Il existe 2 méthodes d'installation, soit via docker soit en mode standalone. Peu importe la méthode il vous faudra un compte sur [https://formspree.io/](https://formspree.io/) afin d'avoir une page nous-contacter fonctionnelle.
|
||||
Il existe 2 méthodes d'installation, soit via docker soit en mode standalone.
|
||||
|
||||
Peu importe la méthode il vous faudra un compte sur [https://formspree.io/](https://formspree.io/) afin d'avoir une page nous-contacter fonctionnelle ou configurer le SMTP tel que défini dans la section [variables d'environnements](#env-file).
|
||||
|
||||
Pour la méthode docker il ne vous faudra rien de plus que `docker` et `docker-compose`.
|
||||
|
||||
|
@ -90,7 +92,7 @@ C'est terminé !
|
|||
|
||||
Le site est accessible sur [http://localhost:3001](http://localhost:3001).
|
||||
|
||||
:information_source: Information : Vous pouvez, et vous dreviez, également regarder du côté de `systemd`, `pm2` ou encore `supervisor` pour que le service démarre en même temps que votre serveur.
|
||||
:information_source: Information : Vous pouvez, et vous devriez, également regarder du côté de `systemd`, `pm2` ou encore `supervisor` pour que le service démarre en même temps que votre serveur.
|
||||
|
||||
### Aller plus loin
|
||||
|
||||
|
@ -227,6 +229,13 @@ S3_BUCKET # Nom du bucket (musictopus par défaut, à changer impérativement si
|
|||
JOBS_HEADER_KEY # Nom du header utilisé pour l'identification des tâches cron (musictopus par défaut)
|
||||
JOBS_HEADER_VALUE # Valeur de la clé (ooYee9xok7eigo2shiePohyoGh1eepew par défaut)
|
||||
REGISTRATION_OPEN # true/false en fonction de si vous souhaitez activer ou non l'inscription à votre instance (true par défaut)
|
||||
MAIL_METHOD # permet de définir la façon dont les mails de la page contact sont envoyés (formspree ou smtp)
|
||||
MAIL_HOST # Adresse du server mail (dams le cas ou MAIL_METHOD est défini sur smtp)
|
||||
MAIL_PORT # Port d'écoute du serveur smtp (dams le cas ou MAIL_METHOD est défini sur smtp)
|
||||
MAIL_USER # Adresse mail du compte permettant d'envoyer les mails (dams le cas ou MAIL_METHOD est défini sur smtp)
|
||||
MAIL_PASSWORD # Mot de passe du compte email (dams le cas ou MAIL_METHOD est défini sur smtp)
|
||||
MAIL_TO # Adresse mail du contact qui recevra les messages de la page "nous contacter" (dams le cas ou MAIL_METHOD est défini sur smtp)
|
||||
|
||||
```
|
||||
|
||||
## Contributeurs
|
||||
|
|
|
@ -37,6 +37,12 @@ services:
|
|||
JOBS_HEADER_KEY: ${JOBS_HEADER_KEY}
|
||||
JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE}
|
||||
REGISTRATION_OPEN: ${REGISTRATION_OPEN}
|
||||
MAIL_METHOD: ${MAIL_METHOD}
|
||||
MAIL_HOST: ${MAIL_HOST}
|
||||
MAIL_PORT: ${MAIL_PORT}
|
||||
MAIL_USER: ${MAIL_USER}
|
||||
MAIL_PASSWORD: ${MAIL_PASSWORD}
|
||||
MAIL_TO: ${MAIL_TO}
|
||||
networks:
|
||||
- musictopus
|
||||
musictopus-db:
|
||||
|
|
118
fontello.json
Normal file
118
fontello.json
Normal file
|
@ -0,0 +1,118 @@
|
|||
{
|
||||
"name": "icon",
|
||||
"css_prefix_text": "icon-",
|
||||
"css_use_suffix": false,
|
||||
"hinting": true,
|
||||
"units_per_em": 1000,
|
||||
"ascent": 850,
|
||||
"glyphs": [
|
||||
{
|
||||
"uid": "44e04715aecbca7f266a17d5a7863c68",
|
||||
"css": "plus",
|
||||
"code": 59392,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "8b80d36d4ef43889db10bc1f0dc9a862",
|
||||
"css": "user",
|
||||
"code": 59393,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "9dd9e835aebe1060ba7190ad2b2ed951",
|
||||
"css": "search",
|
||||
"code": 59394,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "bf882b30900da12fca090d9796bc3030",
|
||||
"css": "mail",
|
||||
"code": 59395,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "0ddd3e8201ccc7d41f7b7c9d27eca6c1",
|
||||
"css": "link",
|
||||
"code": 59396,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "e15f0d620a7897e2035c18c80142f6d9",
|
||||
"css": "link-ext",
|
||||
"code": 61582,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "9bc2902722abb366a213a052ade360bc",
|
||||
"css": "spin",
|
||||
"code": 59449,
|
||||
"src": "fontelico"
|
||||
},
|
||||
{
|
||||
"uid": "bbfb51903f40597f0b70fd75bc7b5cac",
|
||||
"css": "trash",
|
||||
"code": 61944,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "d73eceadda1f594cec0536087539afbf",
|
||||
"css": "heart",
|
||||
"code": 59397,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "cce5e05853d0798a4d10077ef613387c",
|
||||
"css": "blind",
|
||||
"code": 62109,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "567e3e257f2cc8fba2c12bf691c9f2d8",
|
||||
"css": "moon",
|
||||
"code": 61830,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "aa035df0908c4665c269b7b09a5596f3",
|
||||
"css": "sun",
|
||||
"code": 61829,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "c5fd349cbd3d23e4ade333789c29c729",
|
||||
"css": "eye",
|
||||
"code": 59398,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "d870630ff8f81e6de3958ecaeac532f2",
|
||||
"css": "left-open",
|
||||
"code": 59399,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "399ef63b1e23ab1b761dfbb5591fa4da",
|
||||
"css": "right-open",
|
||||
"code": 59400,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "895405dfac8a3b7b2f23b183c6608ee6",
|
||||
"css": "export",
|
||||
"code": 59401,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "4aad6bb50b02c18508aae9cbe14e784e",
|
||||
"css": "share",
|
||||
"code": 61920,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "a73c5deb486c8d66249811642e5d719a",
|
||||
"css": "refresh",
|
||||
"code": 59402,
|
||||
"src": "fontawesome"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -59,6 +59,7 @@
|
|||
"knacss": "^8.0.4",
|
||||
"mongoose": "^6.2.1",
|
||||
"mongoose-unique-validator": "^3.0.0",
|
||||
"nodemailer": "^6.7.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"passport": "^0.5.2",
|
||||
"passport-custom": "^1.1.1",
|
||||
|
@ -66,6 +67,7 @@
|
|||
"passport-local": "^1.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.49.7",
|
||||
"svg-captcha": "^1.4.0",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.2.31"
|
||||
},
|
||||
|
|
Binary file not shown.
|
@ -26,6 +26,8 @@
|
|||
|
||||
<glyph glyph-name="export" unicode="" d="M786 298v-144q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h142q7 0 13-6t5-12q0-15-15-18-43-15-74-34-5-2-9-2h-62q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v119q0 11 10 16 16 7 31 21 8 9 19 4 12-5 12-16z m132 277l-214-214q-10-11-25-11-7 0-14 3-22 9-22 33v107h-89q-181 0-245-73-66-77-41-264 2-13-11-19-5-1-7-1-9 0-14 7-6 8-12 17t-22 39-28 55-21 64-10 68q0 27 2 51t8 50 15 49 27 45 38 42 52 34 70 27 89 17 110 6h89v107q0 24 22 33 7 3 14 3 14 0 25-11l214-214q11-10 11-25t-11-25z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="refresh" unicode="" d="M843 261q0-3 0-4-36-150-150-243t-267-93q-81 0-157 31t-136 88l-72-72q-11-11-25-11t-25 11-11 25v250q0 14 11 25t25 11h250q14 0 25-11t10-25-10-25l-77-77q40-36 90-57t105-20q74 0 139 37t104 99q6 10 30 66 4 13 16 13h107q8 0 13-6t5-12z m14 446v-250q0-14-10-25t-26-11h-250q-14 0-25 11t-10 25 10 25l77 77q-82 77-194 77-75 0-140-37t-104-99q-6-10-29-66-5-13-17-13h-111q-7 0-13 6t-5 12v4q36 150 151 243t268 93q81 0 158-31t137-88l72 72q11 11 25 11t26-11 10-25z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="spin" unicode="" d="M855 9c-189-190-520-172-705 13-190 190-200 494-28 695 11 13 21 26 35 34 36 23 85 18 117-13 30-31 35-76 16-112-5-9-9-15-16-22-140-151-145-379-8-516 153-153 407-121 542 34 106 122 142 297 77 451-83 198-305 291-510 222l0 1c236 82 492-24 588-252 71-167 37-355-72-493-11-15-23-29-36-42z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="link-ext" unicode="" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
|
||||
|
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/img/loading-dark.gif
Normal file
BIN
public/img/loading-dark.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
BIN
public/img/loading-light.gif
Normal file
BIN
public/img/loading-light.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
|
@ -56,6 +56,8 @@ $pagination-hover-color: rgb(115, 151, 186);
|
|||
|
||||
--button-link-text-color: #2C364A;
|
||||
|
||||
--loader-img: url('/img/loading-light.gif');
|
||||
|
||||
--nord0: #{$nord0};
|
||||
--nord1: #{$nord1};
|
||||
--nord2: #{$nord2};
|
||||
|
@ -94,4 +96,6 @@ $pagination-hover-color: rgb(115, 151, 186);
|
|||
--border-color: #{$nord1};
|
||||
|
||||
--button-link-text-color: #{$white};
|
||||
|
||||
--loader-img: url('/img/loading-dark.gif');
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'icon';
|
||||
src: url('/font/icon.eot?80770511');
|
||||
src: url('/font/icon.eot?80770511#iefix') format('embedded-opentype'),
|
||||
url('/font/icon.woff2?80770511') format('woff2'),
|
||||
url('/font/icon.woff?80770511') format('woff'),
|
||||
url('/font/icon.ttf?80770511') format('truetype'),
|
||||
url('/font/icon.svg?80770511#icon') format('svg');
|
||||
src: url('/font/icon.eot?41426785');
|
||||
src: url('/font/icon.eot?41426785#iefix') format('embedded-opentype'),
|
||||
url('/font/icon.woff2?41426785') format('woff2'),
|
||||
url('/font/icon.woff?41426785') format('woff'),
|
||||
url('/font/icon.ttf?41426785') format('truetype'),
|
||||
url('/font/icon.svg?41426785#icon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -42,6 +42,7 @@
|
|||
.icon-left-open:before { content: '\e807'; } /* '' */
|
||||
.icon-right-open:before { content: '\e808'; } /* '' */
|
||||
.icon-export:before { content: '\e809'; } /* '' */
|
||||
.icon-refresh:before { content: '\e80a'; } /* '' */
|
||||
.icon-spin:before { content: '\e839'; } /* '' */
|
||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
|
||||
.icon-sun:before { content: '\f185'; } /* '' */
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
@import './icons';
|
||||
@import './list';
|
||||
@import './box';
|
||||
@import './loader';
|
||||
|
||||
@import './error';
|
||||
@import './500';
|
||||
|
|
13
sass/loader.scss
Normal file
13
sass/loader.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
.loader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.animation {
|
||||
background-image: var(--loader-img);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,26 @@
|
|||
.ma-collection-details {
|
||||
h1 {
|
||||
i {
|
||||
cursor: pointer;
|
||||
|
||||
&.icon-trash {
|
||||
color: $danger-color;
|
||||
@include transition() {}
|
||||
|
||||
&:hover {
|
||||
color: $danger-color-hl;
|
||||
}
|
||||
}
|
||||
&.icon-refresh {
|
||||
color: $primary-color;
|
||||
@include transition() {}
|
||||
|
||||
&:hover {
|
||||
color: $primary-color-hl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.galerie {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
position: relative;
|
||||
width: 3.25rem;
|
||||
margin-left: auto;
|
||||
color: rgba(0,0,0,.7);
|
||||
color: var(--font-color);
|
||||
|
||||
@include respond-to("medium-up") {
|
||||
display: none;
|
||||
|
|
|
@ -23,6 +23,7 @@ import importJobsRouter from "./routes/jobs";
|
|||
import importAlbumRouterApiV1 from "./routes/api/v1/albums";
|
||||
import importSearchRouterApiV1 from "./routes/api/v1/search";
|
||||
import importMeRouterApiV1 from "./routes/api/v1/me";
|
||||
import importContactRouterApiV1 from "./routes/api/v1/contact";
|
||||
|
||||
passportConfig(passport);
|
||||
|
||||
|
@ -91,6 +92,7 @@ app.use("/jobs", importJobsRouter);
|
|||
app.use("/api/v1/albums", importAlbumRouterApiV1);
|
||||
app.use("/api/v1/search", importSearchRouterApiV1);
|
||||
app.use("/api/v1/me", importMeRouterApiV1);
|
||||
app.use("/api/v1/contact", importContactRouterApiV1);
|
||||
|
||||
// Handle 404
|
||||
app.use((req, res) => {
|
||||
|
|
|
@ -19,4 +19,14 @@ module.exports = {
|
|||
process.env.JOBS_HEADER_VALUE || "ooYee9xok7eigo2shiePohyoGh1eepew",
|
||||
registrationOpen:
|
||||
(process.env.REGISTRATION_OPEN || "true").toLowerCase() === "true",
|
||||
mailMethod: process.env.MAIL_METHOD || "formspree",
|
||||
smtpConfig: {
|
||||
host: process.env.MAIL_HOST,
|
||||
port: process.env.MAIL_PORT,
|
||||
auth: {
|
||||
user: process.env.MAIL_USER,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
},
|
||||
},
|
||||
mailTo: process.env.MAIL_TO,
|
||||
};
|
||||
|
|
|
@ -5,13 +5,25 @@ import { discogsToken } from "../config";
|
|||
|
||||
export const getBaseUrl = (req) => `${req.protocol}://${req.get("host")}`;
|
||||
|
||||
export const searchSong = async (q) => {
|
||||
export const searchSong = async (q, format, year, country) => {
|
||||
const dis = new Discogs({ userToken: discogsToken }).database();
|
||||
|
||||
const res = await dis.search({
|
||||
const params = {
|
||||
q,
|
||||
type: "release",
|
||||
});
|
||||
};
|
||||
|
||||
if (format) {
|
||||
params.format = format;
|
||||
}
|
||||
if (year) {
|
||||
params.year = year;
|
||||
}
|
||||
if (country) {
|
||||
params.country = country;
|
||||
}
|
||||
|
||||
const res = await dis.search(params);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
|
|
@ -7,7 +7,8 @@ import AlbumsModel from "../models/albums";
|
|||
import JobsModel from "../models/jobs";
|
||||
import UsersModel from "../models/users";
|
||||
import ErrorEvent from "../libs/error";
|
||||
// import { uploadFromUrl } from "../libs/aws";
|
||||
|
||||
import { getAlbumDetails } from "../helpers";
|
||||
|
||||
/**
|
||||
* Classe permettant la gestion des albums d'un utilisateur
|
||||
|
@ -182,6 +183,34 @@ class Albums extends Pages {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode permettant de mettre à jour un album
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
async patchOne() {
|
||||
const { itemId: _id } = this.req.params;
|
||||
const { _id: User } = this.req.user;
|
||||
const album = await AlbumsModel.findOne({
|
||||
_id,
|
||||
User,
|
||||
});
|
||||
|
||||
if (!album) {
|
||||
throw new ErrorEvent(
|
||||
404,
|
||||
"Mise à jour",
|
||||
"Impossible de trouver cet album"
|
||||
);
|
||||
}
|
||||
|
||||
const values = await getAlbumDetails(album.discogsId);
|
||||
|
||||
await album.updateOne(values);
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode permettant de supprimer un élément d'une collection
|
||||
* @return {Boolean}
|
||||
|
@ -196,7 +225,11 @@ class Albums extends Pages {
|
|||
return true;
|
||||
}
|
||||
|
||||
throw new ErrorEvent(404, "Impossible de trouver cet album");
|
||||
throw new ErrorEvent(
|
||||
404,
|
||||
"Suppression",
|
||||
"Impossible de trouver cet album"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,7 +18,7 @@ class Jobs {
|
|||
throw new ErrorEvent(
|
||||
404,
|
||||
"Item non trouvé",
|
||||
`L'album avant l'id ${itemId} n'existe plus dans la collection`
|
||||
`L'album avec l'id ${itemId} n'existe plus dans la collection`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,16 @@ router
|
|||
|
||||
router
|
||||
.route("/:itemId")
|
||||
.patch(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
||||
try {
|
||||
const albums = new Albums(req);
|
||||
const data = await albums.patchOne();
|
||||
|
||||
sendResponse(req, res, data);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
})
|
||||
.delete(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
||||
try {
|
||||
const albums = new Albums(req);
|
||||
|
|
77
src/routes/api/v1/contact.js
Normal file
77
src/routes/api/v1/contact.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
import express from "express";
|
||||
import nodemailer from "nodemailer";
|
||||
import svgCaptcha from "svg-captcha";
|
||||
|
||||
import { sendResponse } from "../../../libs/format";
|
||||
|
||||
import { mailMethod, smtpConfig, mailTo, siteName } from "../../../config";
|
||||
import ErrorEvent from "../../../libs/error";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const router = express.Router();
|
||||
|
||||
router
|
||||
.route("/")
|
||||
.get(async (req, res, next) => {
|
||||
try {
|
||||
const captcha = svgCaptcha.create({
|
||||
size: 4,
|
||||
noise: 2,
|
||||
color: true,
|
||||
});
|
||||
req.session.captcha = captcha.text;
|
||||
|
||||
res.type("svg");
|
||||
return res.status(200).send(captcha.data);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
})
|
||||
.post(async (req, res, next) => {
|
||||
try {
|
||||
if (mailMethod === "smtp") {
|
||||
const { email, name, message, captcha } = req.body;
|
||||
|
||||
if (!captcha || captcha !== req.session.captcha) {
|
||||
throw new ErrorEvent(
|
||||
406,
|
||||
"Captcha",
|
||||
"Le captcha n'est pas valide"
|
||||
);
|
||||
}
|
||||
|
||||
if (!email || !message) {
|
||||
throw new ErrorEvent(
|
||||
406,
|
||||
"Erreur de saisie",
|
||||
"Le formulaire n'est pas correctement saisi"
|
||||
);
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(smtpConfig);
|
||||
|
||||
const text = `Bonjour,
|
||||
Vous venez de recevoir un nouveau message de ${name} (${email}) :
|
||||
|
||||
${message}
|
||||
`;
|
||||
|
||||
const data = await transporter.sendMail({
|
||||
from: smtpConfig.auth.user,
|
||||
to: mailTo,
|
||||
subject: `${siteName} : Nouveau message`,
|
||||
text,
|
||||
});
|
||||
|
||||
const { messageId, response } = data;
|
||||
|
||||
return sendResponse(req, res, { messageId, response });
|
||||
}
|
||||
|
||||
throw new ErrorEvent(500, "Routeur", "Méthode non configurée");
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -9,7 +9,12 @@ const router = express.Router();
|
|||
|
||||
router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
||||
try {
|
||||
const data = await searchSong(req.query.q);
|
||||
const data = await searchSong(
|
||||
req.query.q,
|
||||
req.query.format || null,
|
||||
req.query.year || null,
|
||||
req.query.country || null
|
||||
);
|
||||
|
||||
sendResponse(req, res, data);
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,19 +1,37 @@
|
|||
<main class="layout-maxed ajouter-un-album" id="app">
|
||||
<h1>Ajouter un album</h1>
|
||||
<div class="grid sm:grid-cols-2">
|
||||
<div>
|
||||
<form @submit="search">
|
||||
<form @submit="search">
|
||||
<div class="grid sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="q">Nom de l'album ou code barre</label>
|
||||
<div class="field has-addons">
|
||||
<input type="text" name="q" id="q" v-model="q" placeholder="ex : Hybrid Theory" autofocus>
|
||||
<input type="text" name="q" id="q" v-model="q" placeholder="ex : Iron Maiden - Powerslave" autofocus>
|
||||
<button class="button is-primary" :disabled="loading" aria-label="Chercher">
|
||||
<i class="icon-search" v-if="!loading"></i>
|
||||
<i class="icon-spin animate-spin" v-if="loading"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="grid sm:grid-cols-3 gap-5">
|
||||
<div class="field">
|
||||
<label for="format">Trier par</label>
|
||||
<select id="format" v-model="format">
|
||||
<option value="">Tous</option>
|
||||
<option v-for="format in orderedItems(formats)" :value="format">{{format}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="year">Année</label>
|
||||
<input type="number" name="year" v-model="year" id="year" placeholder="1984">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="country">Pays</label>
|
||||
<input type="string" name="country" v-model="country" id="country" placeholder="France">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 list hover">
|
||||
<div class="item" v-if="!loading" v-for="item in items">
|
||||
|
@ -158,10 +176,77 @@
|
|||
data() {
|
||||
return {
|
||||
q: '',
|
||||
year: '',
|
||||
country: '',
|
||||
format: '',
|
||||
loading: false,
|
||||
items: [],
|
||||
details: {},
|
||||
modalIsVisible: false,
|
||||
formats: [
|
||||
'Vinyl',
|
||||
'Acetate',
|
||||
'Flexi-disc',
|
||||
'Lathe Cut',
|
||||
'Mighty Tiny',
|
||||
'Shellac',
|
||||
'Sopic',
|
||||
'Pathé Disc',
|
||||
'Edison Disc',
|
||||
'Cylinder',
|
||||
'CD',
|
||||
'CDr',
|
||||
'CDV',
|
||||
'DVD',
|
||||
'DVDr',
|
||||
'HD DVD',
|
||||
'HD DVD-R',
|
||||
'Blu-ray',
|
||||
'Blu-ray-R',
|
||||
'Ultra HD Blu-ray',
|
||||
'SACD',
|
||||
'4-Track Cartridge',
|
||||
'8-Track Cartridge',
|
||||
'Cassette',
|
||||
'DC-International',
|
||||
'Elcaset',
|
||||
'PlayTape',
|
||||
'RCA Tape Cartridge',
|
||||
'DAT',
|
||||
'DCC',
|
||||
'Microcassette',
|
||||
'NT Cassette',
|
||||
'Pocket Rocker',
|
||||
'Revere Magnetic Stereo Tape Ca',
|
||||
'Tefifon',
|
||||
'Reel-To-Reel',
|
||||
'Sabamobil',
|
||||
'Betacam',
|
||||
'Betacam SP',
|
||||
'Betamax',
|
||||
'Cartrivision',
|
||||
'MiniDV',
|
||||
'Super VHS',
|
||||
'U-matic',
|
||||
'VHS',
|
||||
'Video 2000',
|
||||
'Video8',
|
||||
'Film Reel',
|
||||
'HitClips',
|
||||
'Laserdisc',
|
||||
'SelectaVision',
|
||||
'VHD',
|
||||
'Wire Recording',
|
||||
'Minidisc',
|
||||
'MVD',
|
||||
'UMD',
|
||||
'Floppy Disk',
|
||||
'File',
|
||||
'Memory Stick',
|
||||
'Hybrid',
|
||||
'All Media',
|
||||
'Box Set',
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -173,8 +258,19 @@
|
|||
}
|
||||
|
||||
this.loading = true;
|
||||
let url = `/api/v1/search?q=${this.q}`;
|
||||
|
||||
axios.get(`/api/v1/search?q=${this.q}`)
|
||||
if ( this.year ) {
|
||||
url += `&year=${this.year}`;
|
||||
}
|
||||
if ( this.country ) {
|
||||
url += `&country=${this.country}`;
|
||||
}
|
||||
if ( this.format ) {
|
||||
url += `&format=${this.format}`;
|
||||
}
|
||||
|
||||
axios.get(url)
|
||||
.then( response => {
|
||||
const {
|
||||
results,
|
||||
|
@ -242,6 +338,9 @@
|
|||
showToastr(err.response?.data?.message || "Impossible d'ajouter cet album pour le moment…");
|
||||
});
|
||||
},
|
||||
orderedItems(items) {
|
||||
return items.sort();
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
|
|
|
@ -85,6 +85,12 @@
|
|||
</span>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 list">
|
||||
<div class="loader" v-if="loading">
|
||||
<div class="animation"></div>
|
||||
<div>
|
||||
Chargement des données en cours…
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" v-if="!loading" v-for="item in items">
|
||||
<span class="title">
|
||||
{{ item.artists_sort}} - {{ item.title }}
|
||||
|
@ -172,8 +178,55 @@
|
|||
methods: {
|
||||
fetch() {
|
||||
this.loading = true;
|
||||
this.total = 0;
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const entries = urlParams.entries();
|
||||
|
||||
for(const entry of entries) {
|
||||
switch(entry[0]) {
|
||||
case 'artists_sort':
|
||||
this.artist = entry[1];
|
||||
break;
|
||||
default:
|
||||
this[entry[0]] = entry[1];
|
||||
}
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
if ( this.format ) {
|
||||
url += `&format=${this.format}`;
|
||||
}
|
||||
if ( this.year ) {
|
||||
url += `&year=${this.year}`;
|
||||
}
|
||||
if ( this.genre ) {
|
||||
url += `&genre=${this.genre}`;
|
||||
}
|
||||
if ( this.style ) {
|
||||
url += `&style=${this.style}`;
|
||||
}
|
||||
|
||||
axios.get(url)
|
||||
.then( response => {
|
||||
this.items = response.data.rows;
|
||||
this.total = response.data.count || 0;
|
||||
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 cette collection");
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
changeUrl() {
|
||||
let url = `?page=${this.page}&limit=${this.limit}&sort=${this.sort}&order=${this.order}`;
|
||||
if ( this.artist ) {
|
||||
url += `&artists_sort=${this.artist.replace('&', '%26')}`;
|
||||
}
|
||||
|
@ -190,38 +243,26 @@
|
|||
url += `&style=${this.style.replace('&', '%26')}`;
|
||||
}
|
||||
|
||||
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 cette collection");
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
location.href = url;
|
||||
},
|
||||
next(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page += 1;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
previous(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page -= 1;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
goTo(page) {
|
||||
this.page = page;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
changeSort() {
|
||||
const [sort,order] = this.sortOrder.split('-');
|
||||
|
@ -229,12 +270,12 @@
|
|||
this.order = order;
|
||||
this.page = 1;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
changeFilter() {
|
||||
this.page = 1;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
showMoreFilters() {
|
||||
this.moreFilters = !this.moreFilters;
|
||||
|
|
|
@ -355,6 +355,7 @@
|
|||
<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-refresh">.icon-refresh</i>
|
||||
<i class="icon-share">.icon-share</i>
|
||||
<i class="icon-spin">.icon-spin</i>
|
||||
<i class="icon-sun">.icon-sun</i>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<main class="layout-maxed ma-collection-details" id="app" v-cloak @keyup="changeImage">
|
||||
|
||||
<h1>{{item.artists_sort}} - {{item.title}}</h1>
|
||||
<h1>
|
||||
{{item.artists_sort}} - {{item.title}}
|
||||
<i class="icon-trash" title="Supprimer cette fiche" @click="showConfirmDelete()"></i>
|
||||
<i class="icon-refresh" title="Mettre à jour les données de cette fiche" @click="updateItem()"></i>
|
||||
</h1>
|
||||
<div class="grid sm:grid-cols-3 gap-16">
|
||||
<div class="text-center">
|
||||
<img :src="item.thumb %>" :alt="`Miniature pour l'album ${item.title}`" />
|
||||
|
@ -70,6 +74,9 @@
|
|||
<ul class="ml-4">
|
||||
<li v-for="(format) in item.formats">
|
||||
{{format.name}}
|
||||
<template v-if="format.text">
|
||||
- <i>{{format.text}}</i>
|
||||
</template>
|
||||
<template v-if="format.descriptions && format.descriptions.length > 0">
|
||||
(<span v-for="(description, index) in format.descriptions">
|
||||
{{description}}<template v-if="index < format.descriptions.length - 1">, </template>
|
||||
|
@ -153,6 +160,20 @@
|
|||
<img :src="preview" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
@ -167,6 +188,7 @@
|
|||
identifiersPreviewLength: 16,
|
||||
preview: null,
|
||||
index: null,
|
||||
showModalDelete: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
@ -277,6 +299,35 @@
|
|||
|
||||
document.querySelector('#identifiers').scrollIntoView({ behavior: 'smooth' });
|
||||
},
|
||||
showConfirmDelete() {
|
||||
this.toggleModal();
|
||||
},
|
||||
toggleModal() {
|
||||
this.showModalDelete = !this.showModalDelete;
|
||||
},
|
||||
updateItem() {
|
||||
showToastr("Mise à jour en cours…", true);
|
||||
axios.patch(`/api/v1/albums/${this.item._id}`)
|
||||
.then( (res) => {
|
||||
showToastr("Mise à jour réalisée avec succès", true);
|
||||
this.item = res.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(err.response?.data?.message || "Impossible de mettre à jour cet album", false);
|
||||
});
|
||||
},
|
||||
deleteItem() {
|
||||
axios.delete(`/api/v1/albums/${this.item._id}`)
|
||||
.then( () => {
|
||||
return locatiom.href = "/ma-collection";
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(err.response?.data?.message || "Impossible de supprimer cet album");
|
||||
})
|
||||
.finally(() => {
|
||||
this.toggleModal();
|
||||
});
|
||||
},
|
||||
},
|
||||
}).mount('#app');
|
||||
</script>
|
|
@ -89,6 +89,12 @@
|
|||
</span>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 list hover">
|
||||
<div class="loader" v-if="loading">
|
||||
<div class="animation"></div>
|
||||
<div>
|
||||
Chargement des données en cours…
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
@ -226,8 +232,54 @@
|
|||
methods: {
|
||||
fetch() {
|
||||
this.loading = true;
|
||||
this.total = 0;
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const entries = urlParams.entries();
|
||||
|
||||
for(const entry of entries) {
|
||||
switch(entry[0]) {
|
||||
case 'artists_sort':
|
||||
this.artist = entry[1];
|
||||
break;
|
||||
default:
|
||||
this[entry[0]] = entry[1];
|
||||
}
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
if ( this.year ) {
|
||||
url += `&year=${this.year}`;
|
||||
}
|
||||
if ( this.genre ) {
|
||||
url += `&genre=${this.genre}`;
|
||||
}
|
||||
if ( this.style ) {
|
||||
url += `&style=${this.style}`;
|
||||
}
|
||||
|
||||
axios.get(url)
|
||||
.then( response => {
|
||||
this.items = response.data.rows;
|
||||
this.total = response.data.count || 0;
|
||||
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;
|
||||
});
|
||||
},
|
||||
changeUrl() {
|
||||
let url = `?page=${this.page}&limit=${this.limit}&sort=${this.sort}&order=${this.order}`;
|
||||
if ( this.artist ) {
|
||||
url += `&artists_sort=${this.artist.replace('&', '%26')}`;
|
||||
}
|
||||
|
@ -244,38 +296,26 @@
|
|||
url += `&style=${this.style.replace('&', '%26')}`;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
location.href = url;
|
||||
},
|
||||
next(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page += 1;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
previous(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page -= 1;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
goTo(page) {
|
||||
this.page = page;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
changeSort() {
|
||||
const [sort,order] = this.sortOrder.split('-');
|
||||
|
@ -283,12 +323,12 @@
|
|||
this.order = order;
|
||||
this.page = 1;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
changeFilter() {
|
||||
this.page = 1;
|
||||
|
||||
this.fetch();
|
||||
this.changeUrl();
|
||||
},
|
||||
showMoreFilters() {
|
||||
this.moreFilters = !this.moreFilters;
|
||||
|
|
|
@ -1,22 +1,80 @@
|
|||
<section class="box">
|
||||
<section class="box" id="app">
|
||||
<h1>Nous contacter</h1>
|
||||
<form action="https://formspree.io/f/<%= config.formspreeId %>" method="POST">
|
||||
<form @submit="send" <% if (config.mailMethod === 'formspree' ) { %> id="contact" method="POST" action="https://formspree.io/f/<%= config.formspreeId %>" <% } %>>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16">
|
||||
<div class="field">
|
||||
<label for="email">Addresse e-mail*</label>
|
||||
<input type="email" name="email" id="email" required />
|
||||
<input type="email" name="email" id="email" v-model="email" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="name">Prénom, nom</label>
|
||||
<input type="text" name="name" id="name" />
|
||||
<input type="text" name="name" id="name" v-model="name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="message">Message*</label>
|
||||
<textarea name="message" id="message" rows="6" required ></textarea>
|
||||
<textarea name="message" id="message" rows="6" required v-model="message" ></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button is-primary">Envoyer</button>
|
||||
<% if (config.mailMethod !== 'formspree' ) { %>
|
||||
<img src="/api/v1/contact" alt="Captcha" />
|
||||
<div class="field">
|
||||
<label for="captcha">Captcha</label>
|
||||
<input type="text" name="captcha" id="captcha" v-model="captcha" required />
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<button type="submit" class="button is-primary" :disabled="loading">
|
||||
<% if (config.mailMethod !== 'formspree' ) { %>
|
||||
<i class="icon-spin animate-spin" v-if="loading"></i>
|
||||
<% } %>
|
||||
Envoyer
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<% if (config.mailMethod === 'smtp' ) { %>
|
||||
<script>
|
||||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
name: '',
|
||||
message: '',
|
||||
captcha: '',
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
send(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if ( this.loading ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const {
|
||||
email,
|
||||
message,
|
||||
name,
|
||||
captcha,
|
||||
} = this;
|
||||
|
||||
axios.post('/api/v1/contact', {email, name, message, captcha})
|
||||
.then( () => {
|
||||
showToastr("Message correctement envoyé", true);
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(err.response?.data?.message || "Impossible d'envoyer votre message", false);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
},
|
||||
},
|
||||
}).mount('#app');
|
||||
</script>
|
||||
<% } %>
|
Loading…
Reference in a new issue