Merge branch 'develop'

This commit is contained in:
Damien Broqua 2022-04-10 17:32:23 +02:00
commit 06752ebcec
26 changed files with 533 additions and 90 deletions

View file

@ -210,22 +210,23 @@ Voici la liste des variables configurables :
``` ```
NODE_ENV # Environnement dans lequel exécuter le projet (development ou production) NODE_ENV # Environnement dans lequel exécuter le projet (development ou production)
PORT # Port sur lequel éxécuter le serveur (par défaut 3001) PORT # Port sur lequel éxécuter le serveur (3001 par défaut)
MONGODB_URI # Url du serveur mongo (par défaut mongodb://musictopus-db/musictopus) MONGODB_URI # Url du serveur mongo (mongodb://musictopus-db/musictopus par défaut)
SECRET # Hash utilisé pour pour sauvegardé les dessions (par défaut waemaeMe5ahc6ce1chaeKohKa6Io8Eik) SECRET # Hash utilisé pour pour sauvegardé les dessions (waemaeMe5ahc6ce1chaeKohKa6Io8Eik par défault)
DISCOGS_TOKEN # Token Discogs (vous devez créer un compte sur discogs afin d'en obtenir un gratuitement) DISCOGS_TOKEN # Token Discogs (vous devez créer un compte sur discogs afin d'en obtenir un gratuitement)
FORMSPREE_ID # Id du formulaire formspree pour la page "nous-contacter" FORMSPREE_ID # Id du formulaire formspree pour la page "nous-contacter"
MATOMO_URL # Url vers l'instance matomo (exemple: https://analytics.darkou.fr/) MATOMO_URL # Url vers l'instance matomo (exemple: https://analytics.darkou.fr/)
MATOMO_ID # Id du site sur votre instance matomo (exemple: 1) MATOMO_ID # Id du site sur votre instance matomo (exemple: 1)
SITE_NAME # Nom du site (utilisé dans le titre des pages) SITE_NAME # Nom du site utilisé dans le titre des pages (MusicTopus par défaut)
AWS_ACCESS_KEY_ID # Clé d'accès AWS AWS_ACCESS_KEY_ID # Clé d'accès AWS
AWS_SECRET_ACCESS_KEY # Clé secrète AWS AWS_SECRET_ACCESS_KEY # Clé secrète AWS
S3_ENDPOINT # Url de l'instance aws (s3.fr-par.scw.cloud pour scaleway france par exemple) S3_ENDPOINT # Url de l'instance aws (s3.fr-par.scw.cloud par défaut)
S3_SIGNATURE # Version de la signature AWS (s3v4 pour scaleway par exemple) S3_SIGNATURE # Version de la signature AWS (s3v4 par défaut)
S3_BASEFOLDER # Nom du sous dossier dans lequel seront mis les pochettes des albums S3_BASEFOLDER # Nom du sous dossier dans lequel seront mis les pochettes des albums (dev par défaut)
S3_BUCKET # Nom du bucket S3_BUCKET # Nom du bucket (musictopus par défaut, à changer impérativement si vous voulez que cela fonctionne)
JOBS_HEADER_KEY # Nom du header utilisé pour l'identification des tâches cron (par exemple musictopus) 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é 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)
``` ```
## Contributeurs ## Contributeurs

View file

@ -36,6 +36,7 @@ services:
S3_SIGNATURE: ${S3_SIGNATURE} S3_SIGNATURE: ${S3_SIGNATURE}
JOBS_HEADER_KEY: ${JOBS_HEADER_KEY} JOBS_HEADER_KEY: ${JOBS_HEADER_KEY}
JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE} JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE}
REGISTRATION_OPEN: ${REGISTRATION_OPEN}
networks: networks:
- musictopus - musictopus
musictopus-db: musictopus-db:

View file

@ -36,6 +36,7 @@ services:
S3_SIGNATURE: ${S3_SIGNATURE} S3_SIGNATURE: ${S3_SIGNATURE}
JOBS_HEADER_KEY: ${JOBS_HEADER_KEY} JOBS_HEADER_KEY: ${JOBS_HEADER_KEY}
JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE} JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE}
REGISTRATION_OPEN: ${REGISTRATION_OPEN}
networks: networks:
- musictopus - musictopus
musictopus-db: musictopus-db:

View file

@ -47,6 +47,8 @@
"connect-flash": "^0.1.1", "connect-flash": "^0.1.1",
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"date-fns": "^2.28.0",
"date-fns-tz": "^1.3.3",
"debug": "^4.3.3", "debug": "^4.3.3",
"disconnect": "^1.2.2", "disconnect": "^1.2.2",
"ejs": "^3.1.6", "ejs": "^3.1.6",
@ -55,8 +57,6 @@
"express-session": "^1.17.2", "express-session": "^1.17.2",
"joi": "^17.6.0", "joi": "^17.6.0",
"knacss": "^8.0.4", "knacss": "^8.0.4",
"moment": "^2.29.1",
"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", "npm-run-all": "^4.1.5",

0
public/robots.txt Normal file
View file

View file

@ -17,6 +17,7 @@
} }
@include respond-to("small-up") { @include respond-to("small-up") {
width: 33%;
&:last-child { &:last-child {
padding-right: 0; padding-right: 0;
} }
@ -28,6 +29,17 @@
} }
} }
.showMoreFilters {
cursor: pointer;
.up::before {
transform: rotate(90deg);
}
.down::before {
transform: rotate(270deg);
}
}
.list{ .list{
.title { .title {
.icon-trash { .icon-trash {

View file

@ -11,4 +11,12 @@
.header { .header {
font-weight: 800; font-weight: 800;
} }
&.info {
background-color: $warning-color;
}
&.success {
background-color: $success-color;
}
} }

View file

@ -132,6 +132,8 @@
} }
&:hover { &:hover {
background-color: var(--default-color);
.navbar-link { .navbar-link {
background-color: var(--default-hl-color); background-color: var(--default-hl-color);
color: rgba(0,0,0,.7); color: rgba(0,0,0,.7);
@ -252,6 +254,13 @@
padding-bottom: .5rem; padding-bottom: .5rem;
padding-top: .5rem; padding-top: .5rem;
hr {
background-color: var(--font-color);
border: none;
height: 2px;
margin: .5rem 0;
}
.navbar-item { .navbar-item {
cursor: pointer; cursor: pointer;
padding-left: 1.5rem; padding-left: 1.5rem;

View file

@ -15,6 +15,7 @@ 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 monCompteRouter from "./routes/mon-compte";
import collectionRouter from "./routes/collection"; import collectionRouter from "./routes/collection";
import importJobsRouter from "./routes/jobs"; import importJobsRouter from "./routes/jobs";
@ -83,6 +84,7 @@ app.use(
); );
app.use("/", indexRouter); app.use("/", indexRouter);
app.use("/mon-compte", monCompteRouter);
app.use("/ma-collection", maCollectionRouter); app.use("/ma-collection", maCollectionRouter);
app.use("/collection", collectionRouter); app.use("/collection", collectionRouter);
app.use("/jobs", importJobsRouter); app.use("/jobs", importJobsRouter);
@ -97,15 +99,22 @@ app.use((req, res) => {
} else { } else {
res.status(404).render("index", { res.status(404).render("index", {
page: { title: `404: Cette page n'existe pas.` }, page: { title: `404: Cette page n'existe pas.` },
errorCode: 404,
viewname: "error", viewname: "error",
user: req.user || null,
config,
session: req.session || null, session: req.session || null,
flashInfo: null, flash: {
query: null, info: req.flash("info"),
params: null, error: [
error: null, ...req.flash("error"),
...(req.session?.flash?.error || []),
],
success: req.flash("success"),
},
query: req.query,
params: req.params,
user: req.user,
config,
getBaseUrl: null,
errorCode: 404,
}); });
} }
}); });
@ -122,15 +131,22 @@ app.use((error, req, res, next) => {
title: error.title || "500: Oups… le serveur a crashé !", title: error.title || "500: Oups… le serveur a crashé !",
error, error,
}, },
errorCode: error.errorCode || 500,
viewname: "error", viewname: "error",
user: req.user || null,
config,
session: req.session || null, session: req.session || null,
flashInfo: null, flash: {
query: null, info: req.flash("info"),
params: null, error: [
error: null, ...req.flash("error"),
...(req.session?.flash?.error || []),
],
success: req.flash("success"),
},
query: req.query,
params: req.params,
user: req.user,
config,
getBaseUrl: null,
errorCode: error.errorCode || 500,
}); });
next(); next();

View file

@ -17,4 +17,6 @@ module.exports = {
jobsHeaderKey: process.env.JOBS_HEADER_KEY || "musictopus", jobsHeaderKey: process.env.JOBS_HEADER_KEY || "musictopus",
jobsHeaderValue: jobsHeaderValue:
process.env.JOBS_HEADER_VALUE || "ooYee9xok7eigo2shiePohyoGh1eepew", process.env.JOBS_HEADER_VALUE || "ooYee9xok7eigo2shiePohyoGh1eepew",
registrationOpen:
(process.env.REGISTRATION_OPEN || "true").toLowerCase() === "true",
}; };

View file

@ -1,4 +1,4 @@
import moment from "moment"; import { format as formatDate } from "date-fns";
import Pages from "./Pages"; import Pages from "./Pages";
import Export from "./Export"; import Export from "./Export";
@ -26,7 +26,7 @@ class Albums extends Pages {
User: user._id, User: user._id,
}; };
data.released = data.released data.released = data.released
? moment(data.released.replace("-00", "-01")) ? new Date(data.released.replace("-00", "-01"))
: null; : null;
delete data.id; delete data.id;
@ -81,6 +81,9 @@ class Albums extends Pages {
order = "asc", order = "asc",
artists_sort, artists_sort,
format, format,
year,
genre,
style,
userId: collectionUserId, userId: collectionUserId,
} = this.req.query; } = this.req.query;
@ -94,11 +97,20 @@ class Albums extends Pages {
if (format) { if (format) {
where["formats.name"] = format; where["formats.name"] = format;
} }
if (year) {
where.year = year;
}
if (genre) {
where.genres = genre;
}
if (style) {
where.styles = style;
}
if (!this.req.user && !collectionUserId) { if (!this.req.user && !collectionUserId) {
throw new ErrorEvent( throw new ErrorEvent(
401, 401,
"Cette collection n'est pas publique", "Collection",
"Cette collection n'est pas publique" "Cette collection n'est pas publique"
); );
} }
@ -114,7 +126,7 @@ class Albums extends Pages {
) { ) {
throw new ErrorEvent( throw new ErrorEvent(
401, 401,
"Cette collection n'est pas publique", "Collection",
"Cette collection n'est pas publique" "Cette collection n'est pas publique"
); );
} }
@ -199,9 +211,21 @@ class Albums extends Pages {
"formats.name", "formats.name",
this.req.user._id this.req.user._id
); );
const years = await Albums.getAllDistincts("year", this.req.user._id);
const genres = await Albums.getAllDistincts(
"genres",
this.req.user._id
);
const styles = await Albums.getAllDistincts(
"styles",
this.req.user._id
);
this.setPageContent("artists", artists); this.setPageContent("artists", artists);
this.setPageContent("formats", formats); this.setPageContent("formats", formats);
this.setPageContent("years", years);
this.setPageContent("genres", genres);
this.setPageContent("styles", styles);
this.setPageTitle("Ma collection"); this.setPageTitle("Ma collection");
} }
@ -211,11 +235,16 @@ class Albums extends Pages {
async loadItem() { async loadItem() {
const { itemId: _id } = this.req.params; const { itemId: _id } = this.req.params;
const { _id: User } = this.req.user; const { _id: User } = this.req.user;
const item = await AlbumsModel.findOne({ const album = await AlbumsModel.findOne({
_id, _id,
User, User,
}); });
const item = {
...album.toJSON(),
released: formatDate(album.released, "MM/dd/yyyy"),
};
this.setPageContent("item", item); this.setPageContent("item", item);
this.setPageTitle( this.setPageTitle(
`Détails de l'album ${item.title} de ${item.artists_sort}` `Détails de l'album ${item.title} de ${item.artists_sort}`
@ -233,17 +262,24 @@ class Albums extends Pages {
if (!user || !user.isPublicCollection) { if (!user || !user.isPublicCollection) {
throw new ErrorEvent( throw new ErrorEvent(
401, 401,
"Collection non partagée",
"Cet utilisateur ne souhaite pas partager sa collection" "Cet utilisateur ne souhaite pas partager sa collection"
); );
} }
const artists = await Albums.getAllDistincts("artists_sort", userId); const artists = await Albums.getAllDistincts("artists_sort", userId);
const formats = await Albums.getAllDistincts("formats.name", userId); const formats = await Albums.getAllDistincts("formats.name", userId);
const years = await Albums.getAllDistincts("year", userId);
const genres = await Albums.getAllDistincts("genres", userId);
const styles = await Albums.getAllDistincts("styles", userId);
this.setPageContent("username", user.username); this.setPageContent("username", user.username);
this.setPageTitle(`Collection publique de ${user.username}`); this.setPageTitle(`Collection publique de ${user.username}`);
this.setPageContent("artists", artists); this.setPageContent("artists", artists);
this.setPageContent("formats", formats); this.setPageContent("formats", formats);
this.setPageContent("years", years);
this.setPageContent("genres", genres);
this.setPageContent("styles", styles);
} }
} }

View file

@ -1,4 +1,5 @@
import momenttz from "moment-timezone"; import { utcToZonedTime } from "date-fns-tz";
import setHours from "date-fns/setHours";
import xl from "excel4node"; import xl from "excel4node";
class Export { class Export {
@ -132,7 +133,9 @@ class Export {
} }
if (released) { if (released) {
ws.cell(currentRow, 7) ws.cell(currentRow, 7)
.date(momenttz.tz(released, "Europe/Paris").hour(12)) .date(
setHours(utcToZonedTime(released, "Europe/Paris"), 12)
)
.style({ numberFormat: "dd/mm/yyyy" }); .style({ numberFormat: "dd/mm/yyyy" });
} }
ws.cell(currentRow, 8).string(format).style(style); ws.cell(currentRow, 8).string(format).style(style);

View file

@ -1,15 +1,12 @@
import Joi from "joi"; import Joi from "joi";
import UsersModel from "../models/users"; import UsersModel from "../models/users";
import Pages from "./Pages";
/** /**
* Classe permettant la gestion de l'utilisateur connecté * Classe permettant la gestion de l'utilisateur connecté
*/ */
class Me { class Me extends Pages {
constructor(req) {
this.req = req;
}
/** /**
* Méthode permettant de modifier le profil d'un utilisateur * Méthode permettant de modifier le profil d'un utilisateur
* @return {Object} * @return {Object}
@ -40,6 +37,33 @@ class Me {
return update; return update;
} }
/**
* Méthode permettant de modifier le mot de passe d'un utilisateur
*/
async updatePassword() {
const { body } = this.req;
const { _id } = this.req.user;
const schema = Joi.object({
oldPassword: Joi.string().required(),
password: Joi.string().required(),
passwordConfirm: Joi.ref("password"),
});
const value = await schema.validateAsync(body);
const user = await UsersModel.findById(_id);
if (!user.validPassword(value.oldPassword)) {
throw new Error("Votre ancien mot de passe n'est pas valide");
}
user.salt = value.password;
await user.save();
this.req.flash("success", "Profil correctement mis à jour");
}
} }
export default Me; export default Me;

View file

@ -52,21 +52,20 @@ class Pages {
*/ */
render() { render() {
this.pageContent.session = this.req.session; this.pageContent.session = this.req.session;
this.pageContent.flashInfo = this.req.flash("info"); this.pageContent.flash = {
this.pageContent.error = this.req.flash("error") || null; info: this.req.flash("info"),
error: [
...this.req.flash("error"),
...(this.req.session?.flash?.error || []),
],
success: this.req.flash("success"),
};
this.pageContent.query = this.req.query; this.pageContent.query = this.req.query;
this.pageContent.params = this.req.params; this.pageContent.params = this.req.params;
this.pageContent.user = this.user; this.pageContent.user = this.user;
this.pageContent.config = config; this.pageContent.config = config;
this.pageContent.getBaseUrl = getBaseUrl(this.req); this.pageContent.getBaseUrl = getBaseUrl(this.req);
if (this.req.session.flash && this.req.session.flash.error) {
// eslint-disable-next-line prefer-destructuring
this.pageContent.page.failureFlash =
this.req.session.flash.error[0];
this.req.session.flash = null;
}
return this.pageContent; return this.pageContent;
} }
} }

View file

@ -7,6 +7,8 @@ import Auth from "../middleware/Auth";
import render from "../libs/format"; import render from "../libs/format";
import { registrationOpen } from "../config";
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const router = express.Router(); const router = express.Router();
@ -59,11 +61,12 @@ router
} }
); );
router if (registrationOpen) {
router
.route("/inscription") .route("/inscription")
.get((req, res, next) => { .get((req, res, next) => {
try { try {
const page = new Pages(req, "inscription"); const page = new Pages(req, "inscription/index");
page.setPageTitle("Inscription"); page.setPageTitle("Inscription");
@ -81,6 +84,19 @@ router
res.redirect("/inscription"); res.redirect("/inscription");
} }
}); });
} else {
router.route("/inscription").get((req, res, next) => {
try {
const page = new Pages(req, "inscription/desactivee");
page.setPageTitle("Inscription");
render(res, page);
} catch (err) {
next(err);
}
});
}
router router
.route("/ajouter-un-album") .route("/ajouter-un-album")

35
src/routes/mon-compte.js Normal file
View file

@ -0,0 +1,35 @@
import express from "express";
import { ensureLoggedIn } from "connect-ensure-login";
import Me from "../middleware/Me";
import render from "../libs/format";
// eslint-disable-next-line new-cap
const router = express.Router();
router
.route("/")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const page = new Me(req, "mon-compte/index");
page.setPageTitle("Mon compte");
render(res, page);
} catch (err) {
next(err);
}
})
.post(ensureLoggedIn("/connexion"), async (req, res) => {
try {
const page = new Me(req, "mon-compte/index");
await page.updatePassword();
} catch (err) {
req.flash("error", err.toString());
}
res.redirect("/mon-compte");
});
export default router;

View file

@ -1,10 +1,8 @@
<main class="layout-maxed error"> <main class="layout-maxed error">
<h1><%= page.title %></h1> <h1><%= page.title %></h1>
<% if ( errorCode && errorCode === 404 ) { %>
<p class="text-center"> <p class="text-center">
<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' ) { %> <% if ( process.env.NODE_ENV !== 'production' ) { %>
<div> <div>
<pre><%= page.error %></pre> <pre><%= page.error %></pre>

View file

@ -83,6 +83,10 @@
</a> </a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<a class="navbar-item" href="/mon-compte">
Mon compte
</a>
<hr />
<a class="navbar-item" href="/ma-collection"> <a class="navbar-item" href="/ma-collection">
Ma collection Ma collection
</a> </a>
@ -125,40 +129,61 @@
<span></span> <span></span>
</div> </div>
<% if ( page.failureFlash || (error && error.length > 0 ) ) {%> <%
if ( flash.error.length > 0 ) {
for ( let i = 0 ; i < flash.error.length ; i += 1 ) {
%>
<div class="flash"> <div class="flash">
<% if ( page.failureFlash ) {%>
<div class="header"> <div class="header">
Erreur Erreur
</div> </div>
<div class="body"> <div class="body">
<%= page.failureFlash %> <%= flash.error[i].replace('Error: ', '') %>
</div>
</div> </div>
<% } %>
<% <%
if (error && error.length > 0) { }
for( let i = 0 ; i < error.length ; i += 1 ) { }
if ( flash.info.length > 0 ) {
for ( let i = 0 ; i < flash.info.length ; i += 1 ) {
%> %>
<div class="flash info">
<div class="header"> <div class="header">
Erreur Information
</div> </div>
<div class="body"> <div class="body">
<%= error %> <%= flash.info[i] %>
</div>
</div>
<%
}
}
if ( flash.success.length > 0 ) {
for ( let i = 0 ; i < flash.success.length ; i += 1 ) {
%>
<div class="flash success">
<div class="header">
Succès
</div>
<div class="body">
<%= flash.success[i] %>
</div>
</div> </div>
<% <%
} }
} }
%> %>
</div>
<% } %>
<%- include(viewname) %> <%- include(viewname) %>
<footer class="footer layout-hero"> <footer class="footer layout-hero">
<p> <p>
<strong title="Merci Brunus ! 😜">MusicTopus</strong> par <a href="https://www.darkou.fr" target="_blank" rel="noopener noreferrer">Damien Broqua <i class="icon-link"></i></a>. <strong title="Merci Brunus ! 😜">MusicTopus</strong> par <a href="https://www.darkou.fr" target="_blank" rel="noopener noreferrer">Damien Broqua <i class="icon-link"></i></a>.
Logo réalisé par Brunus avec <a href="https://inkscape.org/fr/" target="_blank" rel="noopener noreferrer">Inkscape <i class="icon-link"></i></a>.
<br />
Le code source est sous licence <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html" target="_blank" rel="noopener noreferrer">GNU GPL-3.0-or-later <i class="icon-link"></i></a> et disponible sur <a href="https://git.darkou.fr/dbroqua/MusicTopus" target="_blank">git.darkou.fr <i class="icon-link"></i></a>.
<br />
Fait avec ❤️ à Bordeaux. Fait avec ❤️ à Bordeaux.
Le code source est sous licence <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html" target="_blank" rel="noopener noreferrer">GNU GPL-3.0-or-later <i class="icon-link"></i></a>.
</p> </p>
</footer> </footer>
</body> </body>

View file

@ -239,9 +239,9 @@
window.location.href = '/ma-collection'; window.location.href = '/ma-collection';
}) })
.catch((err) => { .catch((err) => {
showToastr(err.response?.data?.message || "Impossible d'ajouter ce album pour le moment…"); showToastr(err.response?.data?.message || "Impossible d'ajouter cet album pour le moment…");
}); });
}, },
} }
}).mount('#app'); }).mount('#app');
</script> </script>

View file

@ -40,6 +40,50 @@
</select> </select>
</div> </div>
</div> </div>
<div class="filters" v-if="moreFilters">
<div class="field">
<label for="format">Année</label>
<select id="format" v-model="year" @change="changeFilter">
<option value="">Toutes</option>
<%
for (let i = 0; i < page.years.length; i += 1 ) {
__append(`<option value="${page.years[i]}">${page.years[i]}</option>`);
}
%>
</select>
</div>
<div class="field">
<label for="genre">Genre</label>
<select id="genre" v-model="genre" @change="changeFilter">
<option value="">Tous</option>
<%
for (let i = 0; i < page.genres.length; i += 1 ) {
__append(`<option value="${page.genres[i]}">${page.genres[i]}</option>`);
}
%>
</select>
</div>
<div class="field">
<label for="style">Style</label>
<select id="style" v-model="style" @change="changeFilter">
<option value="">Tous</option>
<%
for (let i = 0; i < page.styles.length; i += 1 ) {
__append(`<option value="${page.styles[i]}">${page.styles[i]}</option>`);
}
%>
</select>
</div>
</div>
<span @click="showMoreFilters" class="showMoreFilters">
<template v-if="!moreFilters">Voir plus de filtres</template>
<template v-if="moreFilters">Voir moins de filtres</template>
<i class="icon-left-open down" v-if="!moreFilters"></i>
<i class="icon-left-open up" v-if="moreFilters"></i>
</span>
<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">
@ -105,6 +149,7 @@
data() { data() {
return { return {
loading: false, loading: false,
moreFilters: false,
items: [], items: [],
total: 0, total: 0,
page: 1, page: 1,
@ -112,6 +157,9 @@
limit: 16, limit: 16,
artist: '', artist: '',
format: '', format: '',
year: '',
genre: '',
style: '',
sortOrder: 'artists_sort-asc', sortOrder: 'artists_sort-asc',
sort: 'artists_sort', sort: 'artists_sort',
order: 'asc', order: 'asc',
@ -132,6 +180,15 @@
if ( this.format ) { if ( this.format ) {
url += `&format=${this.format}`; url += `&format=${this.format}`;
} }
if ( this.year ) {
url += `&year=${this.year}`;
}
if ( this.genre ) {
url += `&genre=${this.genre.replace('&', '%26')}`;
}
if ( this.style ) {
url += `&style=${this.style.replace('&', '%26')}`;
}
axios.get(url) axios.get(url)
.then( response => { .then( response => {
@ -179,6 +236,9 @@
this.fetch(); this.fetch();
}, },
showMoreFilters() {
this.moreFilters = !this.moreFilters;
}
} }
}).mount('#app'); }).mount('#app');
</script> </script>

View file

@ -274,6 +274,22 @@
Ceci est une erreur Ceci est une erreur
</div> </div>
</div> </div>
<div class="flash info">
<div class="header">
Information
</div>
<div class="body">
Ceci est une information
</div>
</div>
<div class="flash success">
<div class="header">
Succès
</div>
<div class="body">
Ceci est un succès
</div>
</div>
<pre> <pre>
&lt;div class="flash"&gt; &lt;div class="flash"&gt;
&lt;div class="header"&gt; &lt;div class="header"&gt;

View file

@ -12,9 +12,11 @@
<input type="password" name="password" id="password" placeholder="********"> <input type="password" name="password" id="password" placeholder="********">
</div> </div>
<% if ( config.registrationOpen === true ) { %>
<div class="text-right mt-10"> <div class="text-right mt-10">
<p>Pas encore inscrit ? <a href="/inscription">Inscrivez-vous</a></p> <p>Pas encore inscrit ? <a href="/inscription">Inscrivez-vous</a></p>
</div> </div>
<% } %>
<button type="submit" class="button is-primary">Connexion</button> <button type="submit" class="button is-primary">Connexion</button>
</form> </form>

View file

@ -0,0 +1,19 @@
<main class="layout-maxed">
<div class="header layout-hero"></div>
<h1>
Inscription
</h1>
<div class="container">
<div class="text">
<p class="text-justify">
Les inscriptions sur ce site sont fermées.
</p>
<p class="text-justify">
Vous avez cependant la possibilité d'héberger vous même une version de <a href="https://www.musictopus.fr" target="_blank">MusicTopus</a> en vous rendant directement sur le <a href="https://git.darkou.fr/dbroqua/MusicTopus" target="_blank">dépot du projet</a>.
</p>
</div>
<p class="text-center">
<img src="/img/404.svg" alt="Erreur 404" style="max-height: 400px;" />
</p>
</div>
</main>

View file

@ -0,0 +1,99 @@
<main class="layout-maxed collection" id="app">
<h1>
Mon compte
</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
<form method="POST" action="/mon-compte" @submit="updateProfil">
<div class="field">
<label for="email">Adresse e-mail</label>
<input
type="email"
readonly
disabled
name="email"
id="email"
v-model="email"
/>
</div>
<div class="field">
<label for="username">Nom d'utilisateur</label>
<input
type="string"
readonly
disabled
name="username"
id="username"
v-model="username"
/>
</div>
<div class="field">
<label for="oldPassword">Mot de passe actuel</label>
<input
type="password"
name="oldPassword"
id="oldPassword"
required
placeholder="Saisisssez votre mot de passe actuel"
v-model="oldPassword"
/>
</div>
<div></div>
<div class="field">
<label for="password">Nouveau mot de passe</label>
<input
type="password"
name="password"
id="password"
required
placeholder="Saisisssez votre nouveau mot de passe"
v-model="password"
/>
</div>
<div class="field">
<label for="passwordConfirm">Nouveau mot de passe (confirmation)</label>
<input
type="password"
name="passwordConfirm"
id="passwordConfirm"
required
placeholder="Confirmez votre nouveau mot de passe"
v-model="passwordConfirm"
/>
</div>
<button type="submit" class="button is-primary mt-10" :disabled="loading">
<span v-if="!loading">Mettre à jour</span>
<i class="icon-spin animate-spin" v-if="loading"></i>
</button>
</form>
</div>
</main>
<script>
Vue.createApp({
data() {
return {
email: '<%= user.email %>',
username: '<%= user.username %>',
oldPassword: '',
password: '',
passwordConfirm: '',
loading: false,
}
},
methods: {
async updateProfil(event) {
// try {
// if ( this.password !== this.passwordConfirm ) {
// throw "La confirnation du mot de passe ne correspond pas";
// }
// } catch(err) {
// event.preventDefault();
// showToastr(err);
// }
},
}
}).mount('#app');
</script>

View file

@ -6,6 +6,7 @@
<a :href="shareLink" v-if="isPublicCollection" target="_blank"> <a :href="shareLink" v-if="isPublicCollection" target="_blank">
<i class="icon-share"></i> Voir ma collection partagée <i class="icon-share"></i> Voir ma collection partagée
</a> </a>
<div class="filters"> <div class="filters">
<div class="field"> <div class="field">
<label for="artist">Artiste</label> <label for="artist">Artiste</label>
@ -43,6 +44,50 @@
</select> </select>
</div> </div>
</div> </div>
<div class="filters" v-if="moreFilters">
<div class="field">
<label for="format">Année</label>
<select id="format" v-model="year" @change="changeFilter">
<option value="">Toutes</option>
<%
for (let i = 0; i < page.years.length; i += 1 ) {
__append(`<option value="${page.years[i]}">${page.years[i]}</option>`);
}
%>
</select>
</div>
<div class="field">
<label for="genre">Genre</label>
<select id="genre" v-model="genre" @change="changeFilter">
<option value="">Tous</option>
<%
for (let i = 0; i < page.genres.length; i += 1 ) {
__append(`<option value="${page.genres[i]}">${page.genres[i]}</option>`);
}
%>
</select>
</div>
<div class="field">
<label for="style">Style</label>
<select id="style" v-model="style" @change="changeFilter">
<option value="">Tous</option>
<%
for (let i = 0; i < page.styles.length; i += 1 ) {
__append(`<option value="${page.styles[i]}">${page.styles[i]}</option>`);
}
%>
</select>
</div>
</div>
<span @click="showMoreFilters" class="showMoreFilters">
<template v-if="!moreFilters">Voir plus de filtres</template>
<template v-if="moreFilters">Voir moins de filtres</template>
<i class="icon-left-open down" v-if="!moreFilters"></i>
<i class="icon-left-open up" v-if="moreFilters"></i>
</span>
<div class="grid grid-cols-1 md:grid-cols-2 list hover"> <div class="grid grid-cols-1 md:grid-cols-2 list hover">
<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">
@ -154,6 +199,7 @@
data() { data() {
return { return {
loading: false, loading: false,
moreFilters: false,
items: [], items: [],
total: 0, total: 0,
page: 1, page: 1,
@ -161,6 +207,9 @@
limit: 16, limit: 16,
artist: '', artist: '',
format: '', format: '',
year: '',
genre: '',
style: '',
sortOrder: 'artists_sort-asc', sortOrder: 'artists_sort-asc',
sort: 'artists_sort', sort: 'artists_sort',
order: 'asc', order: 'asc',
@ -185,6 +234,15 @@
if ( this.format ) { if ( this.format ) {
url += `&format=${this.format}`; url += `&format=${this.format}`;
} }
if ( this.year ) {
url += `&year=${this.year}`;
}
if ( this.genre ) {
url += `&genre=${this.genre.replace('&', '%26')}`;
}
if ( this.style ) {
url += `&style=${this.style.replace('&', '%26')}`;
}
axios.get(url) axios.get(url)
.then( response => { .then( response => {
@ -232,6 +290,9 @@
this.fetch(); this.fetch();
}, },
showMoreFilters() {
this.moreFilters = !this.moreFilters;
},
toggleModal() { toggleModal() {
this.showModalDelete = !this.showModalDelete; this.showModalDelete = !this.showModalDelete;
}, },
@ -273,7 +334,7 @@
.finally(() => { .finally(() => {
this.toggleModalShare(); this.toggleModalShare();
}); });
} },
} }
}).mount('#app'); }).mount('#app');
</script> </script>