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)
PORT # Port sur lequel éxécuter le serveur (par défaut 3001)
MONGODB_URI # Url du serveur mongo (par défaut mongodb://musictopus-db/musictopus)
SECRET # Hash utilisé pour pour sauvegardé les dessions (par défaut waemaeMe5ahc6ce1chaeKohKa6Io8Eik)
PORT # Port sur lequel éxécuter le serveur (3001 par défaut)
MONGODB_URI # Url du serveur mongo (mongodb://musictopus-db/musictopus par défaut)
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)
FORMSPREE_ID # Id du formulaire formspree pour la page "nous-contacter"
MATOMO_URL # Url vers l'instance matomo (exemple: https://analytics.darkou.fr/)
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_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_SIGNATURE # Version de la signature AWS (s3v4 pour scaleway par exemple)
S3_BASEFOLDER # Nom du sous dossier dans lequel seront mis les pochettes des albums
S3_BUCKET # Nom du bucket
JOBS_HEADER_KEY # Nom du header utilisé pour l'identification des tâches cron (par exemple musictopus)
JOBS_HEADER_VALUE # Valeur de la clé
S3_ENDPOINT # Url de l'instance aws (s3.fr-par.scw.cloud par défaut)
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 (dev par défaut)
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 (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)
```
## Contributeurs

View File

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

View File

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

View File

@ -47,6 +47,8 @@
"connect-flash": "^0.1.1",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"date-fns": "^2.28.0",
"date-fns-tz": "^1.3.3",
"debug": "^4.3.3",
"disconnect": "^1.2.2",
"ejs": "^3.1.6",
@ -55,8 +57,6 @@
"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",

0
public/robots.txt Normal file
View File

View File

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

View File

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

View File

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

View File

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

View File

@ -17,4 +17,6 @@ module.exports = {
jobsHeaderKey: process.env.JOBS_HEADER_KEY || "musictopus",
jobsHeaderValue:
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 Export from "./Export";
@ -26,7 +26,7 @@ class Albums extends Pages {
User: user._id,
};
data.released = data.released
? moment(data.released.replace("-00", "-01"))
? new Date(data.released.replace("-00", "-01"))
: null;
delete data.id;
@ -81,6 +81,9 @@ class Albums extends Pages {
order = "asc",
artists_sort,
format,
year,
genre,
style,
userId: collectionUserId,
} = this.req.query;
@ -94,11 +97,20 @@ class Albums extends Pages {
if (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) {
throw new ErrorEvent(
401,
"Cette collection n'est pas publique",
"Collection",
"Cette collection n'est pas publique"
);
}
@ -114,7 +126,7 @@ class Albums extends Pages {
) {
throw new ErrorEvent(
401,
"Cette collection n'est pas publique",
"Collection",
"Cette collection n'est pas publique"
);
}
@ -199,9 +211,21 @@ class Albums extends Pages {
"formats.name",
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("formats", formats);
this.setPageContent("years", years);
this.setPageContent("genres", genres);
this.setPageContent("styles", styles);
this.setPageTitle("Ma collection");
}
@ -211,11 +235,16 @@ class Albums extends Pages {
async loadItem() {
const { itemId: _id } = this.req.params;
const { _id: User } = this.req.user;
const item = await AlbumsModel.findOne({
const album = await AlbumsModel.findOne({
_id,
User,
});
const item = {
...album.toJSON(),
released: formatDate(album.released, "MM/dd/yyyy"),
};
this.setPageContent("item", item);
this.setPageTitle(
`Détails de l'album ${item.title} de ${item.artists_sort}`
@ -233,17 +262,24 @@ class Albums extends Pages {
if (!user || !user.isPublicCollection) {
throw new ErrorEvent(
401,
"Collection non partagée",
"Cet utilisateur ne souhaite pas partager sa collection"
);
}
const artists = await Albums.getAllDistincts("artists_sort", 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.setPageTitle(`Collection publique de ${user.username}`);
this.setPageContent("artists", artists);
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";
class Export {
@ -132,7 +133,9 @@ class Export {
}
if (released) {
ws.cell(currentRow, 7)
.date(momenttz.tz(released, "Europe/Paris").hour(12))
.date(
setHours(utcToZonedTime(released, "Europe/Paris"), 12)
)
.style({ numberFormat: "dd/mm/yyyy" });
}
ws.cell(currentRow, 8).string(format).style(style);

View File

@ -1,15 +1,12 @@
import Joi from "joi";
import UsersModel from "../models/users";
import Pages from "./Pages";
/**
* Classe permettant la gestion de l'utilisateur connecté
*/
class Me {
constructor(req) {
this.req = req;
}
class Me extends Pages {
/**
* Méthode permettant de modifier le profil d'un utilisateur
* @return {Object}
@ -40,6 +37,33 @@ class Me {
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;

View File

@ -52,21 +52,20 @@ class Pages {
*/
render() {
this.pageContent.session = this.req.session;
this.pageContent.flashInfo = this.req.flash("info");
this.pageContent.error = this.req.flash("error") || null;
this.pageContent.flash = {
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.params = this.req.params;
this.pageContent.user = this.user;
this.pageContent.config = config;
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;
}
}

View File

@ -7,6 +7,8 @@ import Auth from "../middleware/Auth";
import render from "../libs/format";
import { registrationOpen } from "../config";
// eslint-disable-next-line new-cap
const router = express.Router();
@ -59,11 +61,33 @@ router
}
);
router
.route("/inscription")
.get((req, res, next) => {
if (registrationOpen) {
router
.route("/inscription")
.get((req, res, next) => {
try {
const page = new Pages(req, "inscription/index");
page.setPageTitle("Inscription");
render(res, page);
} catch (err) {
next(err);
}
})
.post(async (req, res) => {
try {
await Auth.register(req);
res.redirect("/");
} catch (err) {
res.redirect("/inscription");
}
});
} else {
router.route("/inscription").get((req, res, next) => {
try {
const page = new Pages(req, "inscription");
const page = new Pages(req, "inscription/desactivee");
page.setPageTitle("Inscription");
@ -71,16 +95,8 @@ router
} catch (err) {
next(err);
}
})
.post(async (req, res) => {
try {
await Auth.register(req);
res.redirect("/");
} catch (err) {
res.redirect("/inscription");
}
});
}
router
.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">
<h1><%= page.title %></h1>
<% if ( errorCode && errorCode === 404 ) { %>
<p class="text-center">
<img src="/img/404.svg" alt="Erreur 404" style="max-height: 400px;" />
</p>
<% } %>
<% if ( process.env.NODE_ENV !== 'production' ) { %>
<div>
<pre><%= page.error %></pre>

View File

@ -83,6 +83,10 @@
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="/mon-compte">
Mon compte
</a>
<hr />
<a class="navbar-item" href="/ma-collection">
Ma collection
</a>
@ -125,40 +129,61 @@
<span></span>
</div>
<% if ( page.failureFlash || (error && error.length > 0 ) ) {%>
<div class="flash">
<% if ( page.failureFlash ) {%>
<div class="header">
Erreur
<%
if ( flash.error.length > 0 ) {
for ( let i = 0 ; i < flash.error.length ; i += 1 ) {
%>
<div class="flash">
<div class="header">
Erreur
</div>
<div class="body">
<%= flash.error[i].replace('Error: ', '') %>
</div>
</div>
<div class="body">
<%= page.failureFlash %>
<%
}
}
if ( flash.info.length > 0 ) {
for ( let i = 0 ; i < flash.info.length ; i += 1 ) {
%>
<div class="flash info">
<div class="header">
Information
</div>
<div class="body">
<%= flash.info[i] %>
</div>
</div>
<% } %>
<%
if (error && error.length > 0) {
for( let i = 0 ; i < error.length ; i += 1 ) {
%>
<div class="header">
Erreur
</div>
<div class="body">
<%= error %>
</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>
<%
}
}
%>
<%- include(viewname) %>
<footer class="footer layout-hero">
<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>.
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.
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>
</footer>
</body>

View File

@ -239,9 +239,9 @@
window.location.href = '/ma-collection';
})
.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');
</script>
</script>

View File

@ -40,6 +40,50 @@
</select>
</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="item" v-if="!loading" v-for="item in items">
<span class="title">
@ -105,6 +149,7 @@
data() {
return {
loading: false,
moreFilters: false,
items: [],
total: 0,
page: 1,
@ -112,6 +157,9 @@
limit: 16,
artist: '',
format: '',
year: '',
genre: '',
style: '',
sortOrder: 'artists_sort-asc',
sort: 'artists_sort',
order: 'asc',
@ -132,6 +180,15 @@
if ( 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)
.then( response => {
@ -179,6 +236,9 @@
this.fetch();
},
showMoreFilters() {
this.moreFilters = !this.moreFilters;
}
}
}).mount('#app');
</script>

View File

@ -274,6 +274,22 @@
Ceci est une erreur
</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>
&lt;div class="flash"&gt;
&lt;div class="header"&gt;

View File

@ -12,9 +12,11 @@
<input type="password" name="password" id="password" placeholder="********">
</div>
<% if ( config.registrationOpen === true ) { %>
<div class="text-right mt-10">
<p>Pas encore inscrit ? <a href="/inscription">Inscrivez-vous</a></p>
</div>
<% } %>
<button type="submit" class="button is-primary">Connexion</button>
</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">
<i class="icon-share"></i> Voir ma collection partagée
</a>
<div class="filters">
<div class="field">
<label for="artist">Artiste</label>
@ -43,6 +44,50 @@
</select>
</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="item" v-if="!loading" v-for="item in items">
<span class="title">
@ -154,6 +199,7 @@
data() {
return {
loading: false,
moreFilters: false,
items: [],
total: 0,
page: 1,
@ -161,6 +207,9 @@
limit: 16,
artist: '',
format: '',
year: '',
genre: '',
style: '',
sortOrder: 'artists_sort-asc',
sort: 'artists_sort',
order: 'asc',
@ -185,6 +234,15 @@
if ( 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)
.then( response => {
@ -232,6 +290,9 @@
this.fetch();
},
showMoreFilters() {
this.moreFilters = !this.moreFilters;
},
toggleModal() {
this.showModalDelete = !this.showModalDelete;
},
@ -273,7 +334,7 @@
.finally(() => {
this.toggleModalShare();
});
}
},
}
}).mount('#app');
</script>