Compare commits
72 commits
Author | SHA1 | Date | |
---|---|---|---|
|
30bd3ebdf9 | ||
|
5a7d9d707f | ||
|
041e24e26f | ||
|
71c120564a | ||
|
1a9728fce6 | ||
|
2eb22bb3d6 | ||
|
abcbd0f8f7 | ||
|
f73d4a3093 | ||
|
0a2d5029b5 | ||
|
fcb527aa5e | ||
|
c79f1c5a74 | ||
|
960f53ab54 | ||
|
6994170a04 | ||
|
8e0947ed4b | ||
|
736a0afa44 | ||
|
209ba0f5f0 | ||
77de7d54ca | |||
00bb8647e1 | |||
c32b182151 | |||
85752c537d | |||
3b3a4cf779 | |||
1931bd9eda | |||
7b525d3e43 | |||
81c61a0529 | |||
e01dbd5c31 | |||
205474a701 | |||
e28f382c6c | |||
3626b074bd | |||
4ea7b42d52 | |||
fd0a9df724 | |||
97b8bab2f4 | |||
2f988798df | |||
15eb2c2dad | |||
6862afda5c | |||
4109186a47 | |||
ec5e43889f | |||
c2ff54ecf2 | |||
bfdb19eec1 | |||
1df39410c3 | |||
e0f227af08 | |||
13209a9b1d | |||
b630e73c79 | |||
fbeb1a67c5 | |||
c743f0d3a4 | |||
68004646f1 | |||
55a9656c42 | |||
2389d7d731 | |||
4c442edf21 | |||
50f01805d4 | |||
663eb586cf | |||
c1b01ea4c0 | |||
fe3ed3e91f | |||
8822056c1f | |||
dff1d2baf0 | |||
d446735450 | |||
9fe49eca27 | |||
a7e41949dc | |||
a56db99a81 | |||
1d59ee3b71 | |||
e01f01337c | |||
980586d8eb | |||
8f9e902587 | |||
a74c67e241 | |||
eac7c1aa84 | |||
748edc9cc4 | |||
d03394bee7 | |||
4da4dd9423 | |||
6454f5f8d6 | |||
bc3bb3b554 | |||
cc25b83b2e | |||
06752ebcec | |||
6d0405d129 |
60 changed files with 20599 additions and 1761 deletions
79
.eslintrc.js
79
.eslintrc.js
|
@ -1,36 +1,49 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2020: true,
|
||||
node: true,
|
||||
jquery: true,
|
||||
},
|
||||
extends: ['airbnb-base', 'prettier'],
|
||||
plugins: ['prettier'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 11,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': ['error'],
|
||||
'no-underscore-dangle': [
|
||||
'error',
|
||||
{
|
||||
allow: ['_id', 'artists_sort', 'type_'],
|
||||
},
|
||||
],
|
||||
'camelcase': [
|
||||
'error',
|
||||
{
|
||||
allow: ['artists_sort',]
|
||||
},
|
||||
],
|
||||
},
|
||||
ignorePatterns: ['public/libs/**/*.js', 'public/js/main.js', 'dist/**'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
excludedFiles: '*.ejs',
|
||||
env: {
|
||||
browser: true,
|
||||
es2020: true,
|
||||
node: true,
|
||||
jquery: true,
|
||||
},
|
||||
extends: ["airbnb-base", "prettier"],
|
||||
plugins: ["prettier"],
|
||||
parserOptions: {
|
||||
ecmaVersion: 11,
|
||||
sourceType: "module",
|
||||
},
|
||||
rules: {
|
||||
"prettier/prettier": ["error"],
|
||||
"no-underscore-dangle": [
|
||||
"error",
|
||||
{
|
||||
allow: ["_id", "artists_sort", "type_"],
|
||||
},
|
||||
],
|
||||
camelcase: [
|
||||
"error",
|
||||
{
|
||||
allow: [
|
||||
"artists_sort",
|
||||
"access_token",
|
||||
"api_url",
|
||||
"media_ids",
|
||||
"release_id",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
ignorePatterns: ["public/libs/**/*.js", "public/js/main.js", "dist/**"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
excludedFiles: "*.ejs",
|
||||
},
|
||||
],
|
||||
globals: {
|
||||
Vue: true,
|
||||
axios: true,
|
||||
showToastr: true,
|
||||
protocol: true,
|
||||
host: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -121,6 +121,6 @@ dist
|
|||
dist
|
||||
yarn.lock
|
||||
public/css
|
||||
public/css
|
||||
public/js
|
||||
docker-compose.yml
|
||||
dump
|
||||
|
|
|
@ -3,7 +3,7 @@ version: "2.4"
|
|||
services:
|
||||
musictopus-www:
|
||||
container_name: musictopus-www
|
||||
image: "node:16"
|
||||
image: "node:18"
|
||||
restart: always
|
||||
user: "node"
|
||||
working_dir: /home/node/app
|
||||
|
|
|
@ -3,7 +3,7 @@ version: "2.4"
|
|||
services:
|
||||
musictopus-www:
|
||||
container_name: musictopus-www
|
||||
image: "node:16"
|
||||
image: "node:18"
|
||||
restart: always
|
||||
user: "node"
|
||||
working_dir: /home/node/app
|
||||
|
|
46
gulpfile.js
Normal file
46
gulpfile.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
const { parallel, src, dest } = require("gulp");
|
||||
|
||||
const sourcemaps = require("gulp-sourcemaps");
|
||||
const concat = require("gulp-concat");
|
||||
|
||||
const gulp = require("gulp");
|
||||
const uglify = require("gulp-uglify");
|
||||
const babel = require("gulp-babel");
|
||||
|
||||
const sourceJs = "javascripts/**/*.js";
|
||||
const sourceRemoteJS = [
|
||||
"./node_modules/vue/dist/vue.global.prod.js",
|
||||
"./node_modules/axios/dist/axios.min.js",
|
||||
];
|
||||
|
||||
const destination = "public/js";
|
||||
|
||||
// TASKS ----------------------------------------------------------------------
|
||||
|
||||
const compileJs = function () {
|
||||
return gulp
|
||||
.src(sourceJs)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat("main.js"))
|
||||
.pipe(
|
||||
babel({
|
||||
presets: ["@babel/env"],
|
||||
})
|
||||
)
|
||||
.pipe(uglify())
|
||||
.pipe(sourcemaps.write("."))
|
||||
.pipe(gulp.dest(destination));
|
||||
};
|
||||
const compileRemoteJs = function () {
|
||||
return gulp
|
||||
.src(sourceRemoteJS)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat("libs.js"))
|
||||
.pipe(sourcemaps.write("."))
|
||||
.pipe(gulp.dest(destination));
|
||||
};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// COMMANDS -------------------------------------------------------------------
|
||||
exports.default = parallel(compileJs, compileRemoteJs);
|
||||
// ----------------------------------------------------------------------------
|
193
javascripts/ajouter-un-album.js
Normal file
193
javascripts/ajouter-un-album.js
Normal file
|
@ -0,0 +1,193 @@
|
|||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
// eslint-disable-next-line no-undef
|
||||
share: canPublish,
|
||||
q: "",
|
||||
year: "",
|
||||
country: "",
|
||||
format: "",
|
||||
loading: false,
|
||||
items: [],
|
||||
details: {},
|
||||
modalIsVisible: false,
|
||||
submitting: 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: {
|
||||
search(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.loading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
let url = `/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}`;
|
||||
}
|
||||
|
||||
return axios
|
||||
.get(url)
|
||||
.then((response) => {
|
||||
const { results } = response.data;
|
||||
const items = [];
|
||||
|
||||
for (let i = 0; i < results.length; i += 1) {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
thumb,
|
||||
year,
|
||||
country,
|
||||
format,
|
||||
genre,
|
||||
style,
|
||||
inCollection,
|
||||
} = results[i];
|
||||
items.push({
|
||||
id,
|
||||
title,
|
||||
thumb,
|
||||
year,
|
||||
country,
|
||||
format,
|
||||
genre,
|
||||
style,
|
||||
inCollection,
|
||||
});
|
||||
}
|
||||
|
||||
this.items = items;
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(
|
||||
err.response?.data?.message ||
|
||||
"Aucun résultat trouvé :/"
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
toggleModal() {
|
||||
this.modalIsVisible = !this.modalIsVisible;
|
||||
},
|
||||
loadDetails(discogsId) {
|
||||
axios
|
||||
.get(`/api/v1/search/${discogsId}`)
|
||||
.then((response) => {
|
||||
const { data } = response;
|
||||
|
||||
this.details = data;
|
||||
this.toggleModal();
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(
|
||||
err.response?.data?.message ||
|
||||
"Impossible de charger les détails de cet album"
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
add() {
|
||||
if (this.submitting) {
|
||||
return true;
|
||||
}
|
||||
this.submitting = true;
|
||||
|
||||
return axios
|
||||
.post("/api/v1/albums", {
|
||||
album: this.details,
|
||||
share: this.share,
|
||||
})
|
||||
.then(() => {
|
||||
window.location.href = "/ma-collection";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.submitting = false;
|
||||
showToastr(
|
||||
err.response?.data?.message ||
|
||||
"Impossible d'ajouter cet album pour le moment…"
|
||||
);
|
||||
});
|
||||
},
|
||||
orderedItems(items) {
|
||||
return items.sort();
|
||||
},
|
||||
},
|
||||
}).mount("#ajouter-album");
|
244
javascripts/collection.js
Normal file
244
javascripts/collection.js
Normal file
|
@ -0,0 +1,244 @@
|
|||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
moreFilters: false,
|
||||
items: [],
|
||||
total: 0,
|
||||
// eslint-disable-next-line no-undef
|
||||
page: query.page || 1,
|
||||
totalPages: 1,
|
||||
limit: 16,
|
||||
artist: "",
|
||||
format: "",
|
||||
year: "",
|
||||
genre: "",
|
||||
style: "",
|
||||
sortOrder: "artists_sort-asc",
|
||||
sort: "artists_sort",
|
||||
order: "asc",
|
||||
itemId: null,
|
||||
showModalDelete: false,
|
||||
showModalShare: false,
|
||||
// eslint-disable-next-line no-undef
|
||||
shareLink: `/collection/${userId}`,
|
||||
// eslint-disable-next-line no-undef
|
||||
isPublicCollection,
|
||||
// eslint-disable-next-line no-undef
|
||||
userId,
|
||||
// eslint-disable-next-line no-undef
|
||||
vueType,
|
||||
// eslint-disable-next-line no-undef
|
||||
query,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
formatParams(param) {
|
||||
return param.replace("&", "%26").replace("+", "%2B");
|
||||
},
|
||||
fetch() {
|
||||
this.loading = true;
|
||||
this.total = 0;
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const entries = urlParams.entries();
|
||||
|
||||
const sortOrder = {
|
||||
sort: "artists_sort",
|
||||
order: "asc",
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const entry of entries) {
|
||||
const [key, value] = entry;
|
||||
switch (key) {
|
||||
case "artists_sort":
|
||||
this.artist = value;
|
||||
break;
|
||||
default:
|
||||
if (["order", "sort"].indexOf(key) !== -1) {
|
||||
sortOrder[key] = value;
|
||||
}
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
this.sortOrder = `${sortOrder.sort}-${sortOrder.order}`;
|
||||
|
||||
let url = `/api/v1/albums?page=${this.page}&limit=${this.limit}&sort=${this.sort}&order=${this.order}`;
|
||||
if (this.artist) {
|
||||
url += `&artist=${this.formatParams(this.artist)}`;
|
||||
}
|
||||
if (this.format) {
|
||||
url += `&format=${this.formatParams(this.format)}`;
|
||||
}
|
||||
if (this.year) {
|
||||
url += `&year=${this.year}`;
|
||||
}
|
||||
if (this.genre) {
|
||||
url += `&genre=${this.formatParams(this.genre)}`;
|
||||
}
|
||||
if (this.style) {
|
||||
url += `&style=${this.formatParams(this.style)}`;
|
||||
}
|
||||
// INFO: Cas d'une collection partagée
|
||||
if (this.vueType === "public" && this.userId) {
|
||||
url += `&userId=${this.userId}`;
|
||||
}
|
||||
|
||||
axios
|
||||
.get(url)
|
||||
.then((response) => {
|
||||
this.items = response.data.rows;
|
||||
this.total = response.data.count || 0;
|
||||
this.totalPages =
|
||||
parseInt(response.data.count / this.limit, 10) +
|
||||
(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.formatParams(this.artist)}`;
|
||||
}
|
||||
if (this.format) {
|
||||
url += `&format=${this.formatParams(this.format)}`;
|
||||
}
|
||||
if (this.year) {
|
||||
url += `&year=${this.year}`;
|
||||
}
|
||||
if (this.genre) {
|
||||
url += `&genre=${this.formatParams(this.genre)}`;
|
||||
}
|
||||
if (this.style) {
|
||||
url += `&style=${this.formatParams(this.style)}`;
|
||||
}
|
||||
|
||||
window.location.href = url;
|
||||
},
|
||||
next(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page += 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
previous(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page -= 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
goTo(page) {
|
||||
this.page = page;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
changeSort() {
|
||||
const [sort, order] = this.sortOrder.split("-");
|
||||
this.sort = sort;
|
||||
this.order = order;
|
||||
this.page = 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
changeFilter() {
|
||||
this.page = 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
showMoreFilters() {
|
||||
this.moreFilters = !this.moreFilters;
|
||||
},
|
||||
toggleModal() {
|
||||
this.showModalDelete = !this.showModalDelete;
|
||||
},
|
||||
toggleModalShare() {
|
||||
this.showModalShare = !this.showModalShare;
|
||||
},
|
||||
showConfirmDelete(itemId) {
|
||||
this.itemId = itemId;
|
||||
this.toggleModal();
|
||||
},
|
||||
deleteItem() {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (vueType !== "private") {
|
||||
return false;
|
||||
}
|
||||
return 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() {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (vueType !== "private") {
|
||||
return false;
|
||||
}
|
||||
return 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();
|
||||
});
|
||||
},
|
||||
renderAlbumTitle(item) {
|
||||
let render = "";
|
||||
|
||||
for (let i = 0; i < item.artists.length; i += 1) {
|
||||
const { name, join } = item.artists[i];
|
||||
render += `${name} ${join ? `${join} ` : ""}`;
|
||||
}
|
||||
|
||||
render += `- ${item.title}`;
|
||||
|
||||
return render;
|
||||
},
|
||||
},
|
||||
}).mount("#collection");
|
43
javascripts/conctact.js
Normal file
43
javascripts/conctact.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
// eslint-disable-next-line no-undef
|
||||
if (typeof contactMethod !== "undefined" && contactMethod === "smtp") {
|
||||
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;
|
||||
|
||||
return 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("#contact");
|
||||
}
|
65
javascripts/main.js
Normal file
65
javascripts/main.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
const { protocol, host } = window.location;
|
||||
|
||||
let timeout = null;
|
||||
|
||||
/**
|
||||
* Fonction permettant d'afficher un message dans un toastr
|
||||
* @param {String} message
|
||||
*/
|
||||
function showToastr(message, success = false) {
|
||||
const x = document.getElementById("toastr");
|
||||
if (message) {
|
||||
x.getElementsByTagName("SPAN")[0].innerHTML = message;
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
x.classList.remove("show");
|
||||
}
|
||||
|
||||
x.classList.remove("success");
|
||||
x.classList.remove("error");
|
||||
if (success) {
|
||||
x.classList.add("success");
|
||||
} else {
|
||||
x.classList.add("error");
|
||||
}
|
||||
|
||||
x.classList.add("show");
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
x.classList.remove("show");
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction permettant de masquer le toastr
|
||||
*/
|
||||
function hideToastr() {
|
||||
const x = document.getElementById("toastr");
|
||||
|
||||
x.className = x.className.replace("show", "");
|
||||
x.getElementsByTagName("SPAN")[0].innerHTML = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensemble d'actions effectuées au chargement de la page
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const $navbarBurgers = Array.prototype.slice.call(
|
||||
document.querySelectorAll(".navbar-burger"),
|
||||
0
|
||||
);
|
||||
if ($navbarBurgers.length > 0) {
|
||||
$navbarBurgers.forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
const { target } = el.dataset;
|
||||
const $target = document.getElementById(target);
|
||||
|
||||
el.classList.toggle("is-active");
|
||||
$target.classList.toggle("is-active");
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
103
javascripts/mon-compte/index.js
Normal file
103
javascripts/mon-compte/index.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
|
||||
if (typeof email !== "undefined" && typeof username !== "undefined") {
|
||||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
// eslint-disable-next-line no-undef
|
||||
email,
|
||||
// eslint-disable-next-line no-undef
|
||||
username,
|
||||
oldPassword: "",
|
||||
password: "",
|
||||
passwordConfirm: "",
|
||||
// eslint-disable-next-line no-undef
|
||||
mastodon: mastodon || {
|
||||
publish: false,
|
||||
url: "",
|
||||
token: "",
|
||||
message:
|
||||
"Je viens d'ajouter {artist} - {album} à ma collection !",
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
errors: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async testMastodon() {
|
||||
const { url, token } = this.formData.mastodon;
|
||||
|
||||
if (!url) {
|
||||
this.errors.push("emptyUrl");
|
||||
}
|
||||
if (!token) {
|
||||
this.errors.push("emptyToken");
|
||||
}
|
||||
|
||||
if (this.errors.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(`/api/v1/mastodon`, { url, token });
|
||||
|
||||
showToastr("Configuration valide !", true);
|
||||
} catch (err) {
|
||||
showToastr(
|
||||
err.response?.data?.message ||
|
||||
"Impossible de tester cette configuration",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async updateProfil() {
|
||||
this.errors = [];
|
||||
const { oldPassword, password, passwordConfirm, mastodon } =
|
||||
this.formData;
|
||||
|
||||
if (password && !oldPassword) {
|
||||
this.errors.push("emptyPassword");
|
||||
}
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
this.errors.push("passwordsDiffer");
|
||||
}
|
||||
|
||||
if (this.errors.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const data = {
|
||||
mastodon,
|
||||
};
|
||||
|
||||
if (password) {
|
||||
data.password = password;
|
||||
data.oldPassword = oldPassword;
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.patch(`/api/v1/me`, data);
|
||||
|
||||
showToastr("Profil mis à jour", true);
|
||||
} catch (err) {
|
||||
showToastr(
|
||||
err.response?.data?.message ||
|
||||
"Impossible de mettre à jour votre profil"
|
||||
);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}).mount("#mon-compte");
|
||||
}
|
248
javascripts/mon-compte/ma-collection/details.js
Normal file
248
javascripts/mon-compte/ma-collection/details.js
Normal file
|
@ -0,0 +1,248 @@
|
|||
if (typeof item !== "undefined") {
|
||||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
// eslint-disable-next-line no-undef
|
||||
item,
|
||||
// eslint-disable-next-line no-undef
|
||||
canShareItem,
|
||||
tracklist: [],
|
||||
identifiers: [],
|
||||
modalIsVisible: false,
|
||||
identifiersMode: "preview",
|
||||
identifiersPreviewLength: 16,
|
||||
preview: null,
|
||||
index: null,
|
||||
showModalDelete: false,
|
||||
showModalShare: false,
|
||||
shareMessage: "",
|
||||
shareMessageTransformed: "",
|
||||
shareMessageLength: 0,
|
||||
shareSubmiting: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.setTrackList();
|
||||
this.setIdentifiers();
|
||||
|
||||
window.addEventListener("keydown", this.changeImage);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener("keydown", this.changeImage);
|
||||
},
|
||||
watch: {
|
||||
shareMessage(message) {
|
||||
const video =
|
||||
this.item.videos && this.item.videos.length > 0
|
||||
? this.item.videos[0].uri
|
||||
: "";
|
||||
|
||||
this.shareMessageTransformed = message
|
||||
.replaceAll("{artist}", this.item.artists[0].name)
|
||||
.replaceAll("{format}", this.item.formats[0].name)
|
||||
.replaceAll("{year}", this.item.year)
|
||||
.replaceAll("{video}", video)
|
||||
.replaceAll("{album}", this.item.title);
|
||||
|
||||
this.shareMessageLength = this.shareMessageTransformed.replace(
|
||||
video,
|
||||
new Array(36).join("#")
|
||||
).length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setIdentifiers() {
|
||||
this.identifiers = [];
|
||||
|
||||
const max =
|
||||
this.identifiersMode === "preview" &&
|
||||
this.item.identifiers.length > this.identifiersPreviewLength
|
||||
? this.identifiersPreviewLength
|
||||
: this.item.identifiers.length;
|
||||
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
this.identifiers.push(this.item.identifiers[i]);
|
||||
}
|
||||
},
|
||||
setTrackList() {
|
||||
this.tracklist = [];
|
||||
let subTrack = {
|
||||
type: null,
|
||||
title: null,
|
||||
tracks: [],
|
||||
};
|
||||
for (let i = 0; i < this.item.tracklist.length; i += 1) {
|
||||
const {
|
||||
type_,
|
||||
title,
|
||||
position,
|
||||
duration,
|
||||
artists,
|
||||
extraartists,
|
||||
} = this.item.tracklist[i];
|
||||
|
||||
if (type_ === "heading") {
|
||||
if (subTrack.type) {
|
||||
this.tracklist.push(subTrack);
|
||||
subTrack = {
|
||||
type: null,
|
||||
title: null,
|
||||
tracks: [],
|
||||
};
|
||||
}
|
||||
|
||||
subTrack.type = type_;
|
||||
subTrack.title = title;
|
||||
} else {
|
||||
subTrack.tracks.push({
|
||||
title,
|
||||
position,
|
||||
duration,
|
||||
extraartists,
|
||||
artists,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.tracklist.push(subTrack);
|
||||
},
|
||||
setImage() {
|
||||
this.preview = this.item.images[this.index].uri;
|
||||
},
|
||||
showGallery(event) {
|
||||
const item =
|
||||
event.target.tagName === "IMG"
|
||||
? event.target.parentElement
|
||||
: event.target;
|
||||
|
||||
const { index } = item.dataset;
|
||||
|
||||
this.index = Number(index);
|
||||
this.modalIsVisible = true;
|
||||
|
||||
this.setImage();
|
||||
},
|
||||
toggleModal() {
|
||||
this.modalIsVisible = !this.modalIsVisible;
|
||||
},
|
||||
previous() {
|
||||
this.index =
|
||||
this.index > 0
|
||||
? this.index - 1
|
||||
: this.item.images.length - 1;
|
||||
this.setImage();
|
||||
},
|
||||
next() {
|
||||
this.index =
|
||||
this.index + 1 === this.item.images.length
|
||||
? 0
|
||||
: this.index + 1;
|
||||
this.setImage();
|
||||
},
|
||||
changeImage(event) {
|
||||
const direction = event.code;
|
||||
|
||||
if (
|
||||
this.modalIsVisible &&
|
||||
["ArrowRight", "ArrowLeft", "Escape"].indexOf(direction) !==
|
||||
-1
|
||||
) {
|
||||
switch (direction) {
|
||||
case "ArrowRight":
|
||||
return this.next();
|
||||
case "ArrowLeft":
|
||||
return this.previous();
|
||||
default:
|
||||
this.modalIsVisible = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
showAllIdentifiers() {
|
||||
this.identifiersMode = "all";
|
||||
this.setIdentifiers();
|
||||
},
|
||||
showLessIdentifiers() {
|
||||
this.identifiersMode = "preview";
|
||||
this.setIdentifiers();
|
||||
|
||||
document
|
||||
.querySelector("#identifiers")
|
||||
.scrollIntoView({ behavior: "smooth" });
|
||||
},
|
||||
showConfirmDelete() {
|
||||
this.toggleModalDelete();
|
||||
},
|
||||
toggleModalDelete() {
|
||||
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;
|
||||
|
||||
this.setTrackList();
|
||||
this.setIdentifiers();
|
||||
this.showLessIdentifiers();
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(
|
||||
err.response?.data?.message ||
|
||||
"Impossible de mettre à jour cet album",
|
||||
false
|
||||
);
|
||||
});
|
||||
},
|
||||
deleteItem() {
|
||||
axios
|
||||
.delete(`/api/v1/albums/${this.item._id}`)
|
||||
.then(() => {
|
||||
window.location.href = "/ma-collection";
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(
|
||||
err.response?.data?.message ||
|
||||
"Impossible de supprimer cet album"
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.toggleModalDelete();
|
||||
});
|
||||
},
|
||||
goToArtist() {
|
||||
return "";
|
||||
},
|
||||
shareAlbum() {
|
||||
if (this.shareSubmiting) {
|
||||
return false;
|
||||
}
|
||||
this.shareSubmiting = true;
|
||||
axios
|
||||
.post(`/api/v1/albums/${this.item._id}/share`, {
|
||||
message: this.shareMessageTransformed,
|
||||
})
|
||||
.then(() => {
|
||||
showToastr("Album partagé", true);
|
||||
this.shareMessage = "";
|
||||
this.showModalShare = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(
|
||||
err.response?.data?.message ||
|
||||
"Impossible de partager cet album",
|
||||
false
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.shareSubmiting = false;
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}).mount("#ma-collection-details");
|
||||
}
|
16
javascripts/mon-compte/ma-collection/exporter.js
Normal file
16
javascripts/mon-compte/ma-collection/exporter.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
format: "xml",
|
||||
};
|
||||
},
|
||||
created() {},
|
||||
destroyed() {},
|
||||
methods: {
|
||||
exportCollection(event) {
|
||||
event.preventDefault();
|
||||
|
||||
window.open(`/api/v1/albums?exportFormat=${this.format}`, "_blank");
|
||||
},
|
||||
},
|
||||
}).mount("#exporter");
|
106
javascripts/mon-compte/ma-collection/importer.js
Normal file
106
javascripts/mon-compte/ma-collection/importer.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
file: "",
|
||||
content: [],
|
||||
parsed: false,
|
||||
imported: 0,
|
||||
disabled: true,
|
||||
state: "default",
|
||||
};
|
||||
},
|
||||
created() {},
|
||||
destroyed() {},
|
||||
methods: {
|
||||
handleFileUpload(event) {
|
||||
const { files } = event.target;
|
||||
const [csv] = files;
|
||||
|
||||
this.file = csv;
|
||||
|
||||
this.file = csv;
|
||||
// this.parseFile();
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (content) => {
|
||||
this.content = [];
|
||||
this.state = "parse";
|
||||
const lines = content.target.result.split(/\r\n|\n/);
|
||||
for (let line = 1; line < lines.length - 1; line += 1) {
|
||||
this.parseLine(lines[0], lines[line]);
|
||||
}
|
||||
|
||||
this.state = "default";
|
||||
this.disabled = false;
|
||||
};
|
||||
|
||||
reader.readAsText(csv);
|
||||
},
|
||||
parseLine(header, line) {
|
||||
const row = {};
|
||||
let currentHeaderIndex = 0;
|
||||
|
||||
let separant = ",";
|
||||
let value = "";
|
||||
for (let i = 0; i < line.length; i += 1) {
|
||||
const char = line[i];
|
||||
|
||||
if (char !== separant) {
|
||||
if (char === '"') {
|
||||
separant = '"';
|
||||
} else {
|
||||
value += char;
|
||||
}
|
||||
} else if (char === '"') {
|
||||
separant = ",";
|
||||
} else {
|
||||
row[header.split(",")[currentHeaderIndex]] = value;
|
||||
currentHeaderIndex += 1;
|
||||
value = "";
|
||||
}
|
||||
}
|
||||
this.content.push(row);
|
||||
},
|
||||
async addOne(index) {
|
||||
const { Artist, Title, release_id } = this.content[index];
|
||||
|
||||
try {
|
||||
const res = await axios.get(
|
||||
`/api/v1/albums?discogsId=${release_id}`
|
||||
);
|
||||
|
||||
if (res.status === 204) {
|
||||
await axios.post("/api/v1/albums", {
|
||||
discogsId: release_id,
|
||||
share: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.imported += 1;
|
||||
if (this.content.length > index + 1) {
|
||||
await this.addOne(index + 1);
|
||||
}
|
||||
} catch (err) {
|
||||
showToastr(
|
||||
`Impossible d'ajouter l'album ${Title} de ${Artist}`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async importCollection(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.disabled = true;
|
||||
this.state = "submit";
|
||||
this.imported = 0;
|
||||
|
||||
const imported = await this.addOne(0);
|
||||
|
||||
this.disabled = false;
|
||||
this.state = imported ? "done" : "default";
|
||||
},
|
||||
},
|
||||
}).mount("#importer");
|
71
javascripts/theme.js
Normal file
71
javascripts/theme.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Fonction permettant de sauvegarder dans le stockage local le choix du thème
|
||||
* @param {String} scheme
|
||||
*/
|
||||
function saveColorScheme(scheme) {
|
||||
localStorage.setItem("theme", scheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction permettant de changer le thème du site
|
||||
* @param {String} scheme
|
||||
*/
|
||||
function setColorScheme(scheme) {
|
||||
document.documentElement.setAttribute("data-theme", scheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction permettant de récupérer le thème du système
|
||||
* @return {String}
|
||||
*/
|
||||
function getPreferredColorScheme() {
|
||||
if (window.matchMedia) {
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
return "dark";
|
||||
}
|
||||
return "light";
|
||||
}
|
||||
return "light";
|
||||
}
|
||||
|
||||
// INFO: On place un event sur le bouton
|
||||
const toggleSwitch = document.querySelector(
|
||||
'.theme-switch input[type="checkbox"]'
|
||||
);
|
||||
|
||||
/**
|
||||
* Event permettant de détecter les changements de thème du système
|
||||
*/
|
||||
if (window.matchMedia) {
|
||||
const colorSchemeQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
colorSchemeQuery.addEventListener("change", () => {
|
||||
const selectedColorScheme = localStorage.getItem("theme") || "system";
|
||||
|
||||
if (selectedColorScheme === "system") {
|
||||
const preferedColorScheme = getPreferredColorScheme();
|
||||
setColorScheme(preferedColorScheme);
|
||||
|
||||
toggleSwitch.checked = preferedColorScheme === "dark";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const currentTheme = localStorage.getItem("theme") || getPreferredColorScheme();
|
||||
|
||||
// INFO: Au chargement de la page on détecte le thème à charger
|
||||
setColorScheme(currentTheme);
|
||||
|
||||
toggleSwitch.checked = currentTheme === "dark";
|
||||
|
||||
toggleSwitch.addEventListener(
|
||||
"change",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const scheme = e.target.checked ? "dark" : "light";
|
||||
|
||||
saveColorScheme(scheme);
|
||||
setColorScheme(scheme);
|
||||
},
|
||||
false
|
||||
);
|
18175
package-lock.json
generated
Normal file
18175
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
@ -4,16 +4,17 @@
|
|||
"description": "Simple application to manage your CD/Vinyl collection",
|
||||
"scripts": {
|
||||
"start": "node ./dist/bin/www",
|
||||
"run:all": "npm-run-all build sass start",
|
||||
"watch": "nodemon -e js,scss",
|
||||
"run:all": "npm-run-all build sass uglify start",
|
||||
"watch": "npx nodemon -e js,scss",
|
||||
"sass": "npx sass sass/index.scss public/css/main.css -s compressed --color",
|
||||
"uglify": "npx gulp",
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "babel ./src --out-dir dist --copy-files",
|
||||
"build": "npx babel ./src --out-dir dist --copy-files",
|
||||
"test": "jest",
|
||||
"prepare": "husky install"
|
||||
"prepare": "npx husky install"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16.x",
|
||||
"node": "16.x || 18.x",
|
||||
"yarn": "1.x"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -23,7 +24,7 @@
|
|||
"author": {
|
||||
"name": "Damien Broqua",
|
||||
"email": "contact@darkou.fr",
|
||||
"url": "https://www.darkou.fr"
|
||||
"url": "https://www.darkou.link"
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
|
@ -38,10 +39,11 @@
|
|||
"prettier": "^2.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.490.0",
|
||||
"@aws-sdk/lib-storage": "^3.490.0",
|
||||
"@babel/cli": "^7.17.0",
|
||||
"@babel/core": "^7.17.2",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"aws-sdk": "^2.1110.0",
|
||||
"axios": "^0.26.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-flash": "^0.1.1",
|
||||
|
@ -55,8 +57,14 @@
|
|||
"excel4node": "^1.7.2",
|
||||
"express": "^4.17.2",
|
||||
"express-session": "^1.17.2",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"joi": "^17.6.0",
|
||||
"knacss": "^8.0.4",
|
||||
"mastodon": "^1.2.2",
|
||||
"mongoose": "^6.2.1",
|
||||
"mongoose-unique-validator": "^3.0.0",
|
||||
"nodemailer": "^6.7.8",
|
||||
|
@ -72,10 +80,11 @@
|
|||
"vue": "^3.2.31"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"exec": "yarn run:all",
|
||||
"exec": "npm run run:all",
|
||||
"watch": [
|
||||
"src/*",
|
||||
"sass/*"
|
||||
"sass/*",
|
||||
"javascripts/*"
|
||||
],
|
||||
"ignore": [
|
||||
"**/__tests__/**",
|
||||
|
|
|
@ -1,138 +0,0 @@
|
|||
/**
|
||||
* Fonction permettant d'afficher un message dans un toastr
|
||||
* @param {String} message
|
||||
*/
|
||||
function showToastr(message, success = false) {
|
||||
let x = document.getElementById("toastr");
|
||||
if ( message ) {
|
||||
x.getElementsByTagName("SPAN")[0].innerHTML = message;
|
||||
}
|
||||
|
||||
x.className = `${x.className} show`.replace("sucess", "");
|
||||
if ( success ) {
|
||||
x.className = `${x.className} success`;
|
||||
}
|
||||
setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fonction permettant de masquer le toastr
|
||||
*/
|
||||
function hideToastr() {
|
||||
let x = document.getElementById("toastr");
|
||||
|
||||
x.className = x.className.replace("show", "");
|
||||
x.getElementsByTagName("SPAN")[0].innerHTML = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction permettant de récupérer la valeur d'un cookie
|
||||
* @param {String} cname
|
||||
* @param {String} defaultValue
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
function getCookie(cname, defaultValue = 'false') {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for(let i = 0; i < ca.length; i+=1) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction permettant de créer un cookie
|
||||
* @param {String} cname
|
||||
* @param {String} cvalue
|
||||
* @param {Number} exdays
|
||||
*/
|
||||
function setCookie(cname, cvalue, exdays = 30) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + (exdays*24*60*60*1000));
|
||||
let expires = "expires="+ d.toUTCString();
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction de (dé)charger le thème accessible
|
||||
* @param {String} value
|
||||
*/
|
||||
function setAriaTheme(value) {
|
||||
let body = document.body;
|
||||
if ( value === 'true' ) {
|
||||
let classesString = body.className || "";
|
||||
if (classesString.indexOf("is-accessible") === -1) {
|
||||
body.classList.add("is-accessible");
|
||||
}
|
||||
} else {
|
||||
body.classList.remove("is-accessible");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction de (dé)charger le thème accessible
|
||||
*/
|
||||
function switchAriaTheme() {
|
||||
let body = document.body;
|
||||
|
||||
body.classList.toggle("is-accessible");
|
||||
|
||||
setCookie('ariatheme', body.classList.contains("is-accessible"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction permettant de switcher de thème clair/sombre
|
||||
* @param {Object} e
|
||||
*/
|
||||
function switchTheme(e) {
|
||||
const theme = e.target.checked ? 'dark' : 'light';
|
||||
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
setCookie('theme', theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensemble d'actions effectuées au chargement de la page
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||
if ($navbarBurgers.length > 0) {
|
||||
$navbarBurgers.forEach( el => {
|
||||
el.addEventListener('click', () => {
|
||||
const target = el.dataset.target;
|
||||
const $target = document.getElementById(target);
|
||||
|
||||
el.classList.toggle('is-active');
|
||||
$target.classList.toggle('is-active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const switchAriaThemeBtn = document.querySelector("#switchAriaTheme");
|
||||
if ( switchAriaThemeBtn ) {
|
||||
switchAriaThemeBtn.addEventListener("click", switchAriaTheme);
|
||||
}
|
||||
setAriaTheme(getCookie('ariatheme'));
|
||||
|
||||
const toggleSwitch = document.querySelector('.theme-switch input[type="checkbox"]');
|
||||
if ( toggleSwitch ) {
|
||||
toggleSwitch.addEventListener('change', switchTheme, false);
|
||||
}
|
||||
|
||||
let currentThemeIsDark = getCookie('theme');
|
||||
if ( currentThemeIsDark === 'false' && window.matchMedia ) {
|
||||
currentThemeIsDark = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
switchTheme({target: {checked: currentThemeIsDark === 'dark'}});
|
||||
if ( toggleSwitch) {
|
||||
toggleSwitch.checked = currentThemeIsDark === 'dark';
|
||||
}
|
||||
});
|
|
@ -7,10 +7,18 @@
|
|||
.list {
|
||||
margin-top: 2rem;
|
||||
|
||||
.item{
|
||||
.item {
|
||||
img {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.in-collection {
|
||||
opacity: 0.6;
|
||||
|
||||
small {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,10 +22,12 @@ $nord15: #b48ead;
|
|||
|
||||
$primary-color: $nord8;
|
||||
$danger-color: $nord11;
|
||||
$error-color: $nord12;
|
||||
$warning-color: $nord13;
|
||||
$success-color: $nord14;
|
||||
$primary-color-hl: darken($primary-color, $hoverAmount);
|
||||
$danger-color-hl: darken($danger-color, $hoverAmount);
|
||||
$error-color-hl: darken($error-color, $hoverAmount);
|
||||
$warning-color-hl: darken($warning-color, $hoverAmount);
|
||||
$success-color-hl: darken($success-color, $hoverAmount);
|
||||
|
||||
|
@ -35,6 +37,9 @@ $button-alternate-color: #01103C;
|
|||
$pagination-border-color: $nord3;
|
||||
$pagination-hover-color: rgb(115, 151, 186);
|
||||
|
||||
$close-background: rgba(10,10,10,.6);
|
||||
$close-background-dark: rgba(240,240,240,.6);
|
||||
|
||||
:root {
|
||||
--default-color: #{$white};
|
||||
--bg-color: #{darken($white, 5%)};
|
||||
|
@ -56,6 +61,8 @@ $pagination-hover-color: rgb(115, 151, 186);
|
|||
|
||||
--button-link-text-color: #2C364A;
|
||||
|
||||
--close-background: #{$close-background};
|
||||
|
||||
--loader-img: url('/img/loading-light.gif');
|
||||
|
||||
--nord0: #{$nord0};
|
||||
|
@ -97,5 +104,7 @@ $pagination-hover-color: rgb(115, 151, 186);
|
|||
|
||||
--button-link-text-color: #{$white};
|
||||
|
||||
--close-background: #{$nord3};
|
||||
|
||||
--loader-img: url('/img/loading-dark.gif');
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
.error {
|
||||
min-height: calc(100vh - 3.25rem - 100px);
|
||||
padding-top: 4rem;
|
||||
main {
|
||||
&.error {
|
||||
min-height: calc(100vh - 3.25rem - 100px);
|
||||
padding-top: 4rem;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
margin: 2rem auto;
|
||||
|
||||
.header {
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&.info {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -24,9 +24,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 800;
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
|
@ -34,13 +31,12 @@
|
|||
max-width: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--input-color);
|
||||
border: 1px solid transparent !important;
|
||||
border: 1px solid var(--input-active-color) !important;
|
||||
color: var(--input-font-color);
|
||||
@include transition() {}
|
||||
|
||||
&:focus-visible {
|
||||
outline: unset;
|
||||
border-color: var(--input-active-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,19 +7,10 @@ html {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 3.5rem;
|
||||
font-family: 'open_sansregular';
|
||||
font-weight: 400;
|
||||
font-family: 'lucioleregular';
|
||||
min-height: 100vh;
|
||||
color: var(--font-color);
|
||||
@include transition() {}
|
||||
|
||||
&.is-accessible {
|
||||
font-family: 'lucioleregular';
|
||||
|
||||
.text-justify {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@include transition();
|
||||
|
||||
footer.footer {
|
||||
margin-top: auto;
|
||||
|
|
|
@ -1,28 +1,4 @@
|
|||
// @use '../node_modules/knacss/sass/knacss.scss';
|
||||
|
||||
// NOYAU
|
||||
@import "../node_modules/knacss/sass/abstracts/variables-sass";
|
||||
@import "../node_modules/knacss/sass/abstracts/mixins-sass";
|
||||
|
||||
@import "../node_modules/knacss/sass/base/reset-base";
|
||||
@import "../node_modules/knacss/sass/base/reset-accessibility";
|
||||
@import "../node_modules/knacss/sass/base/reset-forms";
|
||||
@import "../node_modules/knacss/sass/base/reset-print";
|
||||
@import "../node_modules/knacss/sass/base/layout";
|
||||
|
||||
// UTILITAIRES
|
||||
@import "../node_modules/knacss/sass/utils/utils-global";
|
||||
@import "../node_modules/knacss/sass/utils/utils-font-sizes";
|
||||
@import "../node_modules/knacss/sass/utils/utils-spacers";
|
||||
@import "../node_modules/knacss/sass/utils/grillade";
|
||||
|
||||
// COMPOSANTS (à ajouter au besoin)
|
||||
// @import "../node_modules/knacss/sass/components/button";
|
||||
// @import "components/burger";
|
||||
// @import "../node_modules/knacss/sass/components/checkbox";
|
||||
@import "../node_modules/knacss/sass/components/radio";
|
||||
// @import "../node_modules/knacss/sass/components/select";
|
||||
// @import "components/quote";
|
||||
@import '../node_modules/knacss/sass/knacss.scss';
|
||||
|
||||
// SPÉCIFIQUE AU SITE
|
||||
@import './fonts';
|
||||
|
@ -42,6 +18,7 @@
|
|||
@import './loader';
|
||||
|
||||
@import './error';
|
||||
@import './messages.scss';
|
||||
@import './500';
|
||||
@import './home';
|
||||
@import './ajouter-un-album';
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
}
|
||||
|
||||
.title {
|
||||
font-weight: 800;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,33 +45,44 @@
|
|||
|
||||
.modal {
|
||||
button.close {
|
||||
height: 36px;
|
||||
max-height: 36px;
|
||||
max-width: 36px;
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
width: 36px;
|
||||
height: 42px;
|
||||
max-height: 42px;
|
||||
max-width: 42px;
|
||||
min-height: 42px;
|
||||
min-width: 42px;
|
||||
width: 42px;
|
||||
position: absolute;
|
||||
background-color: rgba(10,10,10,.6);
|
||||
background-color: var(--close-background);
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel {
|
||||
display: grid;
|
||||
grid-template-columns: auto 80vw auto;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
|
||||
&.previous {
|
||||
left: 12px;
|
||||
}
|
||||
&.next {
|
||||
right: 12px;
|
||||
}
|
||||
i {
|
||||
font-size: 2rem;
|
||||
font-size: 1rem;
|
||||
color: $nord4;
|
||||
|
||||
@include respond-to("small-up") {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
9
sass/messages.scss
Normal file
9
sass/messages.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
.message {
|
||||
margin: 8px 0;
|
||||
padding: 0;
|
||||
font-size: 0.8rem;
|
||||
|
||||
&.error {
|
||||
color: $error-color-hl;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
z-index: 40;
|
||||
z-index: 2;
|
||||
|
||||
&.is-visible {
|
||||
display: flex;
|
||||
|
@ -84,6 +84,11 @@
|
|||
width: 1200;
|
||||
}
|
||||
|
||||
&.for-image {
|
||||
display: initial;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
align-items: center;
|
||||
|
@ -116,10 +121,25 @@
|
|||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
justify-content: end;
|
||||
align-items: baseline;
|
||||
|
||||
.field {
|
||||
flex-direction: row;
|
||||
|
||||
padding: 6px;
|
||||
span {
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.button:not(:last-child) {
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +1,25 @@
|
|||
.navbar {
|
||||
min-height: 3.25rem;
|
||||
min-height: 3.5rem;
|
||||
background-color: var(--navbar-color);
|
||||
box-shadow: rgba(216, 222, 233, 0.15) 0px 5px 10px 0px;
|
||||
color: rgba(0,0,0,.7);
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
@include transition() {}
|
||||
|
||||
@include respond-to("medium-up") {
|
||||
min-height: 3.25rem;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.container {
|
||||
max-width: 1330px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
|
@ -33,7 +37,6 @@
|
|||
word-break: break-word;
|
||||
color: var(--font-color);
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.125;
|
||||
margin-left: .5rem !important;
|
||||
@include transition() {}
|
||||
|
@ -128,7 +131,6 @@
|
|||
min-width: 100%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -279,7 +281,6 @@
|
|||
min-width: 100%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
z-index: 20;
|
||||
|
||||
.navbar-item {
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -3,16 +3,19 @@
|
|||
min-width: 250px;
|
||||
max-width: 360px;
|
||||
position: fixed;
|
||||
z-index: 31;
|
||||
z-index: 10;
|
||||
right: 30px;
|
||||
top: 30px;
|
||||
font-size: 17px;
|
||||
|
||||
padding: 1.25rem 2.5rem 1.25rem 1.5rem;
|
||||
background-color: $danger-color;
|
||||
color: $button-alternate-color;
|
||||
border-radius: 6px;
|
||||
|
||||
&.error {
|
||||
background-color: $danger-color;
|
||||
color: $button-alternate-color;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: $success-color;
|
||||
color: $button-font-color;
|
||||
|
|
13
src/app.js
13
src/app.js
|
@ -22,11 +22,13 @@ import importJobsRouter from "./routes/jobs";
|
|||
|
||||
import importAlbumRouterApiV1 from "./routes/api/v1/albums";
|
||||
import importSearchRouterApiV1 from "./routes/api/v1/search";
|
||||
import importMastodonRouterApiV1 from "./routes/api/v1/mastodon";
|
||||
import importMeRouterApiV1 from "./routes/api/v1/me";
|
||||
import importContactRouterApiV1 from "./routes/api/v1/contact";
|
||||
|
||||
passportConfig(passport);
|
||||
|
||||
mongoose.set("strictQuery", false);
|
||||
mongoose
|
||||
.connect(mongoDbUri, { useNewUrlParser: true, useUnifiedTopology: true })
|
||||
.catch(() => {
|
||||
|
@ -35,7 +37,7 @@ mongoose
|
|||
|
||||
const sess = {
|
||||
cookie: {
|
||||
maxAge: 86400000,
|
||||
maxAge: 604800000, // INFO: 7 jours
|
||||
},
|
||||
secret,
|
||||
saveUninitialized: false,
|
||||
|
@ -75,14 +77,6 @@ app.set("views", path.join(__dirname, "../views"));
|
|||
app.set("view engine", "ejs");
|
||||
|
||||
app.use(express.static(path.join(__dirname, "../public")));
|
||||
app.use(
|
||||
"/libs/vue",
|
||||
express.static(path.join(__dirname, "../node_modules/vue/dist"))
|
||||
);
|
||||
app.use(
|
||||
"/libs/axios",
|
||||
express.static(path.join(__dirname, "../node_modules/axios/dist"))
|
||||
);
|
||||
|
||||
app.use("/", indexRouter);
|
||||
app.use("/mon-compte", monCompteRouter);
|
||||
|
@ -91,6 +85,7 @@ app.use("/collection", collectionRouter);
|
|||
app.use("/jobs", importJobsRouter);
|
||||
app.use("/api/v1/albums", importAlbumRouterApiV1);
|
||||
app.use("/api/v1/search", importSearchRouterApiV1);
|
||||
app.use("/api/v1/mastodon", importMastodonRouterApiV1);
|
||||
app.use("/api/v1/me", importMeRouterApiV1);
|
||||
app.use("/api/v1/contact", importContactRouterApiV1);
|
||||
|
||||
|
|
|
@ -33,6 +33,11 @@ export const getAlbumDetails = async (id) => {
|
|||
|
||||
const res = await dis.getRelease(id);
|
||||
|
||||
if (res.released && res.released.includes("-00")) {
|
||||
const [year, month] = res.released.split("-");
|
||||
res.released = new Date(year, parseInt(month, 10) - 1);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import AWS from "aws-sdk";
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
import { Upload } from "@aws-sdk/lib-storage";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import axios from "axios";
|
||||
|
@ -10,13 +11,9 @@ import {
|
|||
s3BaseFolder,
|
||||
s3Endpoint,
|
||||
s3Bucket,
|
||||
s3Signature,
|
||||
// s3Signature,
|
||||
} from "../config";
|
||||
|
||||
AWS.config.update({
|
||||
accessKeyId: awsAccessKeyId,
|
||||
secretAccessKey: awsSecretAccessKey,
|
||||
});
|
||||
/**
|
||||
* Fonction permettant de stocker un fichier local sur S3
|
||||
* @param {String} filename
|
||||
|
@ -27,23 +24,28 @@ AWS.config.update({
|
|||
*/
|
||||
export const uploadFromFile = async (filename, file, deleteFile = false) => {
|
||||
const data = await fs.readFileSync(file);
|
||||
|
||||
const base64data = Buffer.from(data, "binary");
|
||||
const dest = path.join(s3BaseFolder, filename);
|
||||
|
||||
const s3 = new AWS.S3({
|
||||
endpoint: s3Endpoint,
|
||||
signatureVersion: s3Signature,
|
||||
});
|
||||
|
||||
await s3
|
||||
.putObject({
|
||||
const multipartUpload = new Upload({
|
||||
client: new S3Client({
|
||||
region: "fr-par",
|
||||
endpoint: `https://${s3Endpoint}`,
|
||||
credentials: {
|
||||
accessKeyId: awsAccessKeyId,
|
||||
secretAccessKey: awsSecretAccessKey,
|
||||
},
|
||||
}),
|
||||
params: {
|
||||
Bucket: s3Bucket,
|
||||
Key: dest,
|
||||
Body: base64data,
|
||||
ACL: "public-read",
|
||||
})
|
||||
.promise();
|
||||
endpoint: s3Endpoint,
|
||||
},
|
||||
});
|
||||
|
||||
await multipartUpload.done();
|
||||
|
||||
if (deleteFile) {
|
||||
fs.unlinkSync(file);
|
||||
|
@ -62,11 +64,15 @@ export const uploadFromUrl = async (url) => {
|
|||
const filename = `${uuid()}.jpg`;
|
||||
const file = `/tmp/${filename}`;
|
||||
|
||||
const { data } = await axios.get(url, { responseType: "arraybuffer" });
|
||||
const { data } = await axios.get(url, {
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0",
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
|
||||
fs.writeFileSync(file, data);
|
||||
|
||||
return uploadFromFile(filename, file, true);
|
||||
|
||||
// return s3Object;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { format as formatDate } from "date-fns";
|
||||
import fs from "fs";
|
||||
|
||||
import Mastodon from "mastodon";
|
||||
import { v4 } from "uuid";
|
||||
import axios from "axios";
|
||||
import Pages from "./Pages";
|
||||
import Export from "./Export";
|
||||
|
||||
|
@ -21,9 +25,21 @@ class Albums extends Pages {
|
|||
*/
|
||||
static async postAddOne(req) {
|
||||
const { body, user } = req;
|
||||
const { share, discogsId } = body;
|
||||
|
||||
let albumDetails = body.album;
|
||||
if (discogsId) {
|
||||
albumDetails = await getAlbumDetails(discogsId);
|
||||
body.id = discogsId;
|
||||
}
|
||||
|
||||
if (!albumDetails) {
|
||||
throw new ErrorEvent(406, "Aucun album à ajouter");
|
||||
}
|
||||
|
||||
const data = {
|
||||
...body,
|
||||
discogsId: body.id,
|
||||
...albumDetails,
|
||||
discogsId: albumDetails.id,
|
||||
User: user._id,
|
||||
};
|
||||
data.released = data.released
|
||||
|
@ -39,11 +55,88 @@ class Albums extends Pages {
|
|||
model: "Albums",
|
||||
id: album._id,
|
||||
};
|
||||
|
||||
const job = new JobsModel(jobData);
|
||||
|
||||
job.save();
|
||||
|
||||
try {
|
||||
const User = await UsersModel.findOne({ _id: user._id });
|
||||
|
||||
const { mastodon: mastodonConfig } = User;
|
||||
|
||||
const { publish, token, url, message } = mastodonConfig;
|
||||
|
||||
if (share && publish && url && token) {
|
||||
const M = new Mastodon({
|
||||
access_token: token,
|
||||
api_url: url,
|
||||
});
|
||||
|
||||
const video =
|
||||
data.videos && data.videos.length > 0
|
||||
? data.videos[0].uri
|
||||
: "";
|
||||
|
||||
const status = `${(
|
||||
message ||
|
||||
"Je viens d'ajouter {artist} - {album} à ma collection !"
|
||||
)
|
||||
.replaceAll("{artist}", data.artists[0].name)
|
||||
.replaceAll("{format}", data.formats[0].name)
|
||||
.replaceAll("{year}", data.year)
|
||||
.replaceAll("{video}", video)
|
||||
.replaceAll("{album}", data.title)}
|
||||
|
||||
Publié automatiquement via #musictopus`;
|
||||
|
||||
const media_ids = [];
|
||||
|
||||
if (data.images.length > 0) {
|
||||
for (let i = 0; i < data.images.length; i += 1) {
|
||||
if (media_ids.length === 4) {
|
||||
break;
|
||||
}
|
||||
|
||||
const filename = `${v4()}.jpg`;
|
||||
const file = `/tmp/${filename}`;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: buff } = await axios.get(
|
||||
data.images[i].uri,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0",
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
);
|
||||
|
||||
fs.writeFileSync(file, buff);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: media } = await M.post("media", {
|
||||
file: fs.createReadStream(file),
|
||||
});
|
||||
|
||||
const { id } = media;
|
||||
|
||||
media_ids.push(id);
|
||||
|
||||
fs.unlinkSync(file);
|
||||
}
|
||||
}
|
||||
|
||||
await M.post("statuses", { status, media_ids });
|
||||
}
|
||||
} catch (err) {
|
||||
throw new ErrorEvent(
|
||||
500,
|
||||
"Mastodon",
|
||||
"Album ajouté à votre collection mais impossible de publier sur Mastodon"
|
||||
);
|
||||
}
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
|
@ -80,20 +173,22 @@ class Albums extends Pages {
|
|||
exportFormat = "json",
|
||||
sort = "artists_sort",
|
||||
order = "asc",
|
||||
artists_sort,
|
||||
artist,
|
||||
format,
|
||||
year,
|
||||
genre,
|
||||
style,
|
||||
userId: collectionUserId,
|
||||
discogsIds,
|
||||
discogsId,
|
||||
} = this.req.query;
|
||||
|
||||
let userId = this.req.user?._id;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (artists_sort) {
|
||||
where.artists_sort = artists_sort;
|
||||
if (artist) {
|
||||
where["artists.name"] = artist;
|
||||
}
|
||||
if (format) {
|
||||
where["formats.name"] = format;
|
||||
|
@ -135,6 +230,13 @@ class Albums extends Pages {
|
|||
userId = userIsSharingCollection._id;
|
||||
}
|
||||
|
||||
if (discogsIds) {
|
||||
where.discogsId = { $in: discogsIds };
|
||||
}
|
||||
if (discogsId) {
|
||||
where.discogsId = Number(discogsId);
|
||||
}
|
||||
|
||||
const count = await AlbumsModel.count({
|
||||
User: userId,
|
||||
...where,
|
||||
|
@ -183,6 +285,27 @@ class Albums extends Pages {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode permettant de récupérer le détails d'un album
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
async getOne() {
|
||||
const { itemId: _id } = this.req.params;
|
||||
const { _id: User } = this.req.user;
|
||||
const album = await AlbumsModel.findOne({
|
||||
_id,
|
||||
User,
|
||||
});
|
||||
|
||||
return {
|
||||
...album.toJSON(),
|
||||
released: album.released
|
||||
? formatDate(album.released, "MM/dd/yyyy")
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode permettant de mettre à jour un album
|
||||
*
|
||||
|
@ -191,10 +314,11 @@ class Albums extends Pages {
|
|||
async patchOne() {
|
||||
const { itemId: _id } = this.req.params;
|
||||
const { _id: User } = this.req.user;
|
||||
const album = await AlbumsModel.findOne({
|
||||
const query = {
|
||||
_id,
|
||||
User,
|
||||
});
|
||||
};
|
||||
const album = await AlbumsModel.findOne(query);
|
||||
|
||||
if (!album) {
|
||||
throw new ErrorEvent(
|
||||
|
@ -206,9 +330,9 @@ class Albums extends Pages {
|
|||
|
||||
const values = await getAlbumDetails(album.discogsId);
|
||||
|
||||
await album.updateOne(values);
|
||||
await AlbumsModel.findOneAndUpdate(query, values, { new: true });
|
||||
|
||||
return album;
|
||||
return this.getOne();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -217,7 +341,7 @@ class Albums extends Pages {
|
|||
*/
|
||||
async deleteOne() {
|
||||
const res = await AlbumsModel.findOneAndDelete({
|
||||
user: this.req.user._id,
|
||||
User: this.req.user._id,
|
||||
_id: this.req.params.itemId,
|
||||
});
|
||||
|
||||
|
@ -232,12 +356,89 @@ class Albums extends Pages {
|
|||
);
|
||||
}
|
||||
|
||||
async shareOne() {
|
||||
const { message: status } = this.req.body;
|
||||
const { itemId: _id } = this.req.params;
|
||||
const { _id: User } = this.req.user;
|
||||
const query = {
|
||||
_id,
|
||||
User,
|
||||
};
|
||||
|
||||
const album = await AlbumsModel.findOne(query);
|
||||
|
||||
if (!album) {
|
||||
throw new ErrorEvent(
|
||||
404,
|
||||
"Mise à jour",
|
||||
"Impossible de trouver cet album"
|
||||
);
|
||||
}
|
||||
|
||||
const { mastodon: mastodonConfig } = this.req.user;
|
||||
const { publish, token, url } = mastodonConfig;
|
||||
|
||||
if (publish && url && token) {
|
||||
const M = new Mastodon({
|
||||
access_token: token,
|
||||
api_url: url,
|
||||
});
|
||||
|
||||
const media_ids = [];
|
||||
|
||||
if (album.images.length > 0) {
|
||||
for (let i = 0; i < album.images.length; i += 1) {
|
||||
if (media_ids.length === 4) {
|
||||
break;
|
||||
}
|
||||
|
||||
const filename = `${v4()}.jpg`;
|
||||
const file = `/tmp/${filename}`;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: buff } = await axios.get(
|
||||
album.images[i].uri,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0",
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
);
|
||||
|
||||
fs.writeFileSync(file, buff);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: media } = await M.post("media", {
|
||||
file: fs.createReadStream(file),
|
||||
});
|
||||
|
||||
const { id } = media;
|
||||
|
||||
media_ids.push(id);
|
||||
|
||||
fs.unlinkSync(file);
|
||||
}
|
||||
}
|
||||
|
||||
await M.post("statuses", { status, media_ids });
|
||||
} else {
|
||||
throw new ErrorEvent(
|
||||
406,
|
||||
`Vous n'avez pas configuré vos options de partage sur votre compte`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode permettant de créer la page "ma-collection"
|
||||
*/
|
||||
async loadMyCollection() {
|
||||
const artists = await Albums.getAllDistincts(
|
||||
"artists_sort",
|
||||
"artists.name",
|
||||
this.req.user._id
|
||||
);
|
||||
const formats = await Albums.getAllDistincts(
|
||||
|
@ -266,19 +467,7 @@ class Albums extends Pages {
|
|||
* Méthode permettant d'afficher le détails d'un album
|
||||
*/
|
||||
async loadItem() {
|
||||
const { itemId: _id } = this.req.params;
|
||||
const { _id: User } = this.req.user;
|
||||
const album = await AlbumsModel.findOne({
|
||||
_id,
|
||||
User,
|
||||
});
|
||||
|
||||
const item = {
|
||||
...album.toJSON(),
|
||||
released: album.released
|
||||
? formatDate(album.released, "MM/dd/yyyy")
|
||||
: null,
|
||||
};
|
||||
const item = await this.getOne();
|
||||
|
||||
this.setPageContent("item", item);
|
||||
this.setPageTitle(
|
||||
|
@ -286,6 +475,31 @@ class Albums extends Pages {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode permettant de choisir un album de manière aléatoire dans la collection d'un utilisateur
|
||||
*/
|
||||
async onAir() {
|
||||
const { _id: User } = this.req.user;
|
||||
const count = await AlbumsModel.count({
|
||||
User,
|
||||
});
|
||||
|
||||
const items = await AlbumsModel.find(
|
||||
{
|
||||
User,
|
||||
},
|
||||
[],
|
||||
{
|
||||
skip: Math.floor(Math.random() * (count + 1)),
|
||||
limit: 1,
|
||||
}
|
||||
);
|
||||
|
||||
this.req.params.itemId = items[0]._id;
|
||||
|
||||
await this.loadItem();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode permettant de créer la page "collection/:userId"
|
||||
*/
|
||||
|
@ -302,7 +516,7 @@ class Albums extends Pages {
|
|||
);
|
||||
}
|
||||
|
||||
const artists = await Albums.getAllDistincts("artists_sort", userId);
|
||||
const artists = await Albums.getAllDistincts("artists.name", userId);
|
||||
const formats = await Albums.getAllDistincts("formats.name", userId);
|
||||
const years = await Albums.getAllDistincts("year", userId);
|
||||
const genres = await Albums.getAllDistincts("genres", userId);
|
||||
|
|
|
@ -12,21 +12,47 @@ class Me extends Pages {
|
|||
* @return {Object}
|
||||
*/
|
||||
async patchMe() {
|
||||
const { body, user } = this.req;
|
||||
const { body } = this.req;
|
||||
const { _id } = this.req.user;
|
||||
|
||||
const schema = Joi.object({
|
||||
isPublicCollection: Joi.boolean(),
|
||||
oldPassword: Joi.string(),
|
||||
password: Joi.string(),
|
||||
passwordConfirm: Joi.ref("password"),
|
||||
mastodon: {
|
||||
publish: Joi.boolean(),
|
||||
url: Joi.string().uri().allow(null, ""),
|
||||
token: Joi.string().allow(null, ""),
|
||||
message: Joi.string().allow(null, ""),
|
||||
},
|
||||
});
|
||||
|
||||
const value = await schema.validateAsync(body);
|
||||
const update = await UsersModel.findByIdAndUpdate(
|
||||
user._id,
|
||||
{ $set: value },
|
||||
{ new: true }
|
||||
);
|
||||
const user = await UsersModel.findById(_id);
|
||||
|
||||
if (value.oldPassword) {
|
||||
if (!user.validPassword(value.oldPassword)) {
|
||||
throw new Error("Votre ancien mot de passe n'est pas valide");
|
||||
}
|
||||
}
|
||||
|
||||
if (value.mastodon !== undefined) {
|
||||
user.mastodon = value.mastodon;
|
||||
}
|
||||
|
||||
if (value.password) {
|
||||
user.salt = value.password;
|
||||
}
|
||||
|
||||
if (value.isPublicCollection !== undefined) {
|
||||
user.isPublicCollection = value.isPublicCollection;
|
||||
}
|
||||
|
||||
user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this.req.login(update, (err) => {
|
||||
this.req.login(user, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
@ -35,34 +61,7 @@ class Me extends Pages {
|
|||
});
|
||||
});
|
||||
|
||||
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");
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,12 @@ const UserSchema = new mongoose.Schema(
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
mastodon: {
|
||||
publish: Boolean,
|
||||
token: String,
|
||||
url: String,
|
||||
message: String,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
|
|
@ -68,4 +68,17 @@ router
|
|||
}
|
||||
});
|
||||
|
||||
router
|
||||
.route("/:itemId/share")
|
||||
.post(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
||||
try {
|
||||
const albums = new Albums(req);
|
||||
const data = await albums.shareOne();
|
||||
|
||||
sendResponse(req, res, data);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
28
src/routes/api/v1/mastodon.js
Normal file
28
src/routes/api/v1/mastodon.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import express from "express";
|
||||
import { ensureLoggedIn } from "connect-ensure-login";
|
||||
|
||||
import Mastodon from "mastodon";
|
||||
import { sendResponse } from "../../../libs/format";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const router = express.Router();
|
||||
|
||||
router.route("/").post(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
||||
try {
|
||||
const { url, token } = req.body;
|
||||
|
||||
const M = new Mastodon({
|
||||
access_token: token,
|
||||
api_url: url,
|
||||
});
|
||||
|
||||
const data = await M.post("statuses", {
|
||||
status: "Test d'intégration de Mastodon sur mon compte #musictopus 👌",
|
||||
});
|
||||
return sendResponse(req, res, data);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -3,6 +3,7 @@ import { ensureLoggedIn } from "connect-ensure-login";
|
|||
|
||||
import { sendResponse } from "../../../libs/format";
|
||||
import { searchSong, getAlbumDetails } from "../../../helpers";
|
||||
import Albums from "../../../middleware/Albums";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const router = express.Router();
|
||||
|
@ -16,6 +17,30 @@ router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
|||
req.query.country || null
|
||||
);
|
||||
|
||||
const discogsIds = [];
|
||||
const foundIds = [];
|
||||
|
||||
for (let i = 0; i < data.results.length; i += 1) {
|
||||
discogsIds.push(data.results[i].id);
|
||||
}
|
||||
|
||||
req.query.discogsIds = discogsIds;
|
||||
|
||||
const albums = new Albums(req);
|
||||
const myAlbums = await albums.getAll();
|
||||
|
||||
if (myAlbums.rows) {
|
||||
for (let i = 0; i < myAlbums.rows.length; i += 1) {
|
||||
foundIds.push(myAlbums.rows[i].discogsId);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.results.length; i += 1) {
|
||||
data.results[i].inCollection = foundIds.includes(
|
||||
data.results[i].id
|
||||
);
|
||||
}
|
||||
|
||||
sendResponse(req, res, data);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
|
|
@ -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/index");
|
||||
const page = new Albums(req, "collection");
|
||||
|
||||
await page.loadMyCollection();
|
||||
|
||||
|
@ -24,6 +24,20 @@ router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
router
|
||||
.route("/on-air")
|
||||
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
||||
try {
|
||||
const page = new Albums(req, "mon-compte/ma-collection/details");
|
||||
|
||||
await page.onAir();
|
||||
|
||||
render(res, page);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router
|
||||
.route("/exporter")
|
||||
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
||||
|
@ -32,6 +46,19 @@ router
|
|||
|
||||
page.setPageTitle("Exporter ma collection");
|
||||
|
||||
render(res, page);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router
|
||||
.route("/importer")
|
||||
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
||||
try {
|
||||
const page = new Albums(req, "mon-compte/ma-collection/importer");
|
||||
|
||||
page.setPageTitle("Importer une collection");
|
||||
|
||||
render(res, page);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
|
|
@ -8,28 +8,16 @@ 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");
|
||||
router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => {
|
||||
try {
|
||||
const page = new Me(req, "mon-compte/index");
|
||||
|
||||
page.setPageTitle("Mon compte");
|
||||
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");
|
||||
});
|
||||
render(res, page);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
133
views/components/album.ejs
Normal file
133
views/components/album.ejs
Normal file
|
@ -0,0 +1,133 @@
|
|||
<div class="grid md:grid-cols-3 gap-16">
|
||||
<div>
|
||||
<template v-for="album in tracklist">
|
||||
<strong v-if="album.title">{{album.title}}</strong>
|
||||
<ul>
|
||||
<li v-for="(track, index) in album.tracks" class="ml-4">
|
||||
{{track.position || (index+1)}} - {{ track.title }} <template v-if="track.duration">({{track.duration}})</template>
|
||||
<ul v-if="track.artists && track.artists.length > 0" class="sm-hidden">
|
||||
<li v-for="extra in track.artists" class=" ml-4">
|
||||
<small>{{extra.name}}</small>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="track.extraartists && track.extraartists.length > 0" class="sm-hidden">
|
||||
<li v-for="extra in track.extraartists" class=" ml-4">
|
||||
<small>{{extra.role}} : {{extra.name}}</small>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div>
|
||||
<strong>Genres</strong>
|
||||
<br />
|
||||
<template v-for="(genre, index) in item.genres">
|
||||
{{genre}}<template v-if="index < item.genres.length - 1">, </template>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Styles</strong>
|
||||
<br />
|
||||
<span v-for="(style, index) in item.styles">
|
||||
{{style}}<template v-if="index < item.styles.length - 1">, </template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid grid-cols-3 gap-10">
|
||||
<div>
|
||||
<strong>Pays</strong>
|
||||
<br />
|
||||
<span>{{item.country}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Année</strong>
|
||||
<br />
|
||||
<span>{{item.year}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Date de sortie</strong>
|
||||
<br />
|
||||
<span>{{item.released}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid gap-10">
|
||||
<div>
|
||||
<strong>Format<template v-if="item.formats.length > 1">s</template></strong>
|
||||
<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>
|
||||
</span>)
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||
<div>
|
||||
<strong id="identifiers">Code<template v-if="item.identifiers.length > 1">s</template> barre<template v-if="item.identifiers.length > 1">s</template></strong>
|
||||
<ol class="ml-4">
|
||||
<li v-for="identifier in identifiers">
|
||||
{{identifier.value}} ({{identifier.type}})
|
||||
</li>
|
||||
</ol>
|
||||
<template v-if="item.identifiers.length > identifiersPreviewLength">
|
||||
<button type="button" class="button is-link" v-if="identifiersMode === 'preview'" @click="showAllIdentifiers">
|
||||
Voir la suite
|
||||
</button>
|
||||
<button type="button" class="button is-link" v-if="identifiersMode === 'all'" @click="showLessIdentifiers">
|
||||
Voir moins
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Label<template v-if="item.labels.length > 1">s</template></strong>
|
||||
<ol class="ml-4">
|
||||
<li v-for="label in item.labels">
|
||||
{{label.name}} {{label.catno}}
|
||||
</li>
|
||||
</ol>
|
||||
<strong>Société<template v-if="item.companies.length > 1">s</template></strong>
|
||||
<ol class="ml-4">
|
||||
<li v-for="company in item.companies">
|
||||
<strong>{{company.entity_type_name}}</strong> {{company.name}}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid gap-10">
|
||||
<div>
|
||||
<strong>Note</strong>
|
||||
<div v-html="(item.notes || '').replaceAll('\n', '<br />')"></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid gap-10">
|
||||
<div>
|
||||
<strong>Vidéos</strong>
|
||||
<dl>
|
||||
<template v-for="video in item.videos">
|
||||
<dt>
|
||||
<a :href="video.uri" target="_blank" rel="noopener noreferrer">{{video.title}}</a>
|
||||
</dt>
|
||||
<dd>
|
||||
{{video.description}}
|
||||
</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
11
views/components/filters/artist.ejs
Normal file
11
views/components/filters/artist.ejs
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
11
views/components/filters/format.ejs
Normal file
11
views/components/filters/format.ejs
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
12
views/components/filters/genre.ejs
Normal file
12
views/components/filters/genre.ejs
Normal file
|
@ -0,0 +1,12 @@
|
|||
|
||||
<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>
|
18
views/components/filters/index.ejs
Normal file
18
views/components/filters/index.ejs
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="filters">
|
||||
<%- include('./artist') %>
|
||||
<%- include('./format') %>
|
||||
<%- include('./sort') %>
|
||||
</div>
|
||||
|
||||
<div class="filters" v-if="moreFilters">
|
||||
<%- include('./year') %>
|
||||
<%- include('./genre') %>
|
||||
<%- include('./style') %>
|
||||
</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>
|
15
views/components/filters/sort.ejs
Normal file
15
views/components/filters/sort.ejs
Normal file
|
@ -0,0 +1,15 @@
|
|||
<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 (1-9)</option>
|
||||
<option value="year-desc">Année (9-1)</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>
|
||||
<option value="createdAt-asc">Date d'ajout (1-9)</option>
|
||||
<option value="createdAt-desc">Date d'ajout (9-1)</option>
|
||||
</select>
|
||||
</div>
|
11
views/components/filters/style.ejs
Normal file
11
views/components/filters/style.ejs
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
11
views/components/filters/year.ejs
Normal file
11
views/components/filters/year.ejs
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
162
views/index.ejs
162
views/index.ejs
|
@ -16,10 +16,6 @@
|
|||
|
||||
<link href="/css/main.css" rel="stylesheet" />
|
||||
|
||||
<script src="/libs/axios/axios.min.js"></script>
|
||||
<script src="/libs/vue/vue.global.prod.js"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
|
||||
<% if ( config.matomoUrl ) { %>
|
||||
<!-- Matomo -->
|
||||
<script>
|
||||
|
@ -39,89 +35,94 @@
|
|||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar" aria-label="Navigation principale">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
<img src="/img/logo.png" alt="Logo MusicTopus">
|
||||
<span>MusicTopus</span>
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger" aria-label="Afficher le menu" aria-expanded="false" data-target="navbar">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbar" class="navbar-menu">
|
||||
<% if ( user ) { %>
|
||||
<div class="navbar-start">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<a class="button is-primary" href="/ajouter-un-album">
|
||||
<i class="icon-plus"></i>
|
||||
<span>
|
||||
Ajouter un album
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="navbar-end">
|
||||
<a class="navbar-item" href="/nous-contacter">
|
||||
Nous contacter
|
||||
<nav class="navbar">
|
||||
<nav class="navbar container" aria-label="Navigation principale">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
<img src="/img/logo.png" alt="Logo MusicTopus">
|
||||
<span>MusicTopus</span>
|
||||
</a>
|
||||
<% if ( user ) { %>
|
||||
<div class="navbar-item has-dropdown">
|
||||
<a class="navbar-link">
|
||||
<i class="icon-user"></i>
|
||||
<span>
|
||||
<%= user.username %>
|
||||
</span>
|
||||
</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>
|
||||
<a class="navbar-item" href="/ma-collection/exporter">
|
||||
Exporter ma collection
|
||||
</a>
|
||||
<a role="button" class="navbar-burger" aria-label="Afficher le menu" aria-expanded="false" data-target="navbar">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbar" class="navbar-menu">
|
||||
<% if ( user ) { %>
|
||||
<div class="navbar-start">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<a class="button is-primary" href="/ajouter-un-album">
|
||||
<i class="icon-plus"></i>
|
||||
<span>
|
||||
Ajouter un album
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="navbar-item apparence">
|
||||
<div class="theme-switch-wrapper">
|
||||
<label class="theme-switch" for="checkbox" aria-label="Passer du thème clair au thème sombre et inversement">
|
||||
<input type="checkbox" id="checkbox" />
|
||||
<div class="slider round"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<button type="button" class="button is-primary" id="switchAriaTheme" aria-label="Renforcer la visibilité de ce site" title="Renforcer la visibilité de ce site">
|
||||
<i class="icon-eye"></i>
|
||||
</button>
|
||||
<% if ( !user ) { %>
|
||||
<a class="button is-primary" href="/connexion">
|
||||
<strong>Connexion</strong>
|
||||
|
||||
<div class="navbar-end">
|
||||
<a class="navbar-item" href="/nous-contacter">
|
||||
Nous contacter
|
||||
</a>
|
||||
<% if ( user ) { %>
|
||||
<div class="navbar-item has-dropdown">
|
||||
<a class="navbar-link">
|
||||
<i class="icon-user"></i>
|
||||
<span>
|
||||
<%= user.username %>
|
||||
</span>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<a class="button is-danger" href="/se-deconnecter">
|
||||
Déconnexion
|
||||
</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>
|
||||
<a class="navbar-item" href="/ma-collection/on-air">
|
||||
On air
|
||||
</a>
|
||||
<a class="navbar-item" href="/ma-collection/exporter">
|
||||
Exporter ma collection
|
||||
</a>
|
||||
<a class="navbar-item" href="/ma-collection/importer">
|
||||
Importer une collection
|
||||
</a>
|
||||
<hr />
|
||||
<a class="navbar-item is-danger" href="/se-deconnecter">
|
||||
Déconnexion
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="navbar-item apparence">
|
||||
<div class="theme-switch-wrapper">
|
||||
<label class="theme-switch" for="checkbox" aria-label="Passer du thème clair au thème sombre et inversement">
|
||||
<input type="checkbox" id="checkbox" />
|
||||
<div class="slider round"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<% if ( !user ) { %>
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<a class="button is-primary" href="/connexion">
|
||||
<strong>Connexion</strong>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
<div id="toastr">
|
||||
|
@ -178,7 +179,7 @@
|
|||
|
||||
<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>.
|
||||
<strong title="Merci Brunus ! 😜">MusicTopus</strong> par <a href="https://www.darkou.link" 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>.
|
||||
|
@ -186,5 +187,8 @@
|
|||
Fait avec ❤️ à Bordeaux.
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script defer src="/js/libs.js"></script>
|
||||
<script defer src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<main class="layout-maxed ajouter-un-album" id="app">
|
||||
<main class="layout-maxed ajouter-un-album" id="ajouter-album">
|
||||
<h1>Ajouter un album</h1>
|
||||
<form @submit="search">
|
||||
<div class="grid sm:grid-cols-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="q">Nom de l'album ou code barre</label>
|
||||
<div class="field has-addons">
|
||||
|
@ -13,7 +13,7 @@
|
|||
</div>
|
||||
<div class="grid sm:grid-cols-3 gap-5">
|
||||
<div class="field">
|
||||
<label for="format">Trier par</label>
|
||||
<label for="format">Format</label>
|
||||
<select id="format" v-model="format">
|
||||
<option value="">Tous</option>
|
||||
<option v-for="format in orderedItems(formats)" :value="format">{{format}}</option>
|
||||
|
@ -34,8 +34,9 @@
|
|||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 list hover">
|
||||
<div class="item" v-if="!loading" v-for="item in items">
|
||||
<div v-if="!loading" v-for="item in items" class="item" :class="{'in-collection': item.inCollection}">
|
||||
<a @click="loadDetails(item.id)" class="title">{{ item.artists_sort }} {{ item.title }}</a>
|
||||
<small v-if="item.inCollection"> (Dans ma collection)</small>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4">
|
||||
<div>
|
||||
<img :src="item.thumb" :alt="item.title" @click="loadDetails(item.id)"/>
|
||||
|
@ -78,7 +79,7 @@
|
|||
<button aria-label="Fermer" class="close" @click="toggleModal"></button>
|
||||
</header>
|
||||
<section>
|
||||
<div class="grid grid-cols-2 gap-16">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-16">
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<img :src="details.thumb %>" :alt="`Miniature pour l'album ${details.title}`" />
|
||||
|
@ -86,75 +87,93 @@
|
|||
<img v-for="image in details.images" :src="image.uri150" :alt="`Miniature de type ${image.type}`" style="max-width: 60px;" />
|
||||
<hr />
|
||||
</div>
|
||||
<ol class="ml-4">
|
||||
<li v-for="track in details.tracklist">{{ track.title }} ({{track.duration}})</li>
|
||||
</ol>
|
||||
<ul class="is-unstyled">
|
||||
<li v-for="track in details.tracklist" :class="{'ml-4': track.type_ === 'track'}">
|
||||
<strong v-if="track.type_ === 'heading'">
|
||||
{{track.title}}
|
||||
</strong>
|
||||
<template v-else>
|
||||
{{ track.position }}
|
||||
{{ track.title }} <span v-if="track.duration">({{track.duration}})</span>
|
||||
<ul v-if="track.artists && track.artists.length > 0" class="sm-hidden">
|
||||
<li v-for="extra in track.artists" class=" ml-4">
|
||||
<small>{{extra.role}} : {{extra.name}}</small>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div>
|
||||
<div class="md:col-span-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||
<div class="grid grid-cols-1">
|
||||
<strong>Genres</strong>
|
||||
<br />
|
||||
<template v-for="(genre, index) in details.genres">
|
||||
{{genre}}<template v-if="index < details.genres.length - 1">, </template>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-1">
|
||||
<strong>Styles</strong>
|
||||
<br />
|
||||
<span v-for="(style, index) in details.styles">
|
||||
{{style}}<template v-if="index < details.styles.length - 1">, </template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid grid-cols-3 gap-10">
|
||||
<div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||
<div class="grid grid-cols-2 md:grid-cols-1">
|
||||
<strong>Pays</strong>
|
||||
<br />
|
||||
<span>{{details.country}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-1">
|
||||
<strong>Année</strong>
|
||||
<br />
|
||||
<span>{{details.year}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-1">
|
||||
<strong>Date de sortie</strong>
|
||||
<br />
|
||||
<span>{{details.released}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div class="grid grid-cols-1 gap-10">
|
||||
<div>
|
||||
<strong>Format</strong>
|
||||
<br />
|
||||
<span v-for="(format, index) in details.formats">
|
||||
{{format.name}}<template v-if="index < details.formats.length - 1">, </template>
|
||||
</span>
|
||||
<strong>Format<template v-if="details?.formats?.length > 1">s</template></strong>
|
||||
<ul class="ml-4">
|
||||
<li v-for="(format) in details.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>
|
||||
</span>)
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||
<div>
|
||||
<strong>Codes barres</strong>
|
||||
<ol>
|
||||
<strong>Code<template v-if="details?.identifiers?.length > 1">s</template> barre<template v-if="details?.identifiers?.length > 1">s</template></strong>
|
||||
<ol class="ml-4">
|
||||
<li v-for="identifier in details.identifiers">
|
||||
{{identifier.value}} ({{identifier.type}})
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Label</strong>
|
||||
<ol>
|
||||
<strong>Label<template v-if="details?.labels?.length > 1">s</template></strong>
|
||||
<ol class="ml-4">
|
||||
<li v-for="label in details.labels">
|
||||
{{label.name}}
|
||||
</li>
|
||||
</ol>
|
||||
<strong>Société</strong>
|
||||
<ol>
|
||||
<li v-for="company in details.companie">
|
||||
<strong>Société<template v-if="details?.companies?.length > 1">s</template></strong>
|
||||
<ol class="ml-4">
|
||||
<li v-for="company in details.companies">
|
||||
<strong>{{company.entity_type_name}}</strong>
|
||||
{{company.name}}
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -164,7 +183,15 @@
|
|||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<button class="button is-primary" @click="add">Ajouter</button>
|
||||
<% if ( user.mastodon && user.mastodon.publish ) { %>
|
||||
<div class="field">
|
||||
<label for="share">Partager sur le fédiverse</label>
|
||||
<span>
|
||||
<input type="checkbox" id="share" name="share" v-model="share">
|
||||
</span>
|
||||
</div>
|
||||
<% } %>
|
||||
<button :class="['button is-primary', submitting ? 'is-disabled' : '']" @click="add">Ajouter</button>
|
||||
<button class="button" @click="toggleModal">Annuler</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
@ -172,175 +199,5 @@
|
|||
</main>
|
||||
|
||||
<script>
|
||||
Vue.createApp({
|
||||
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: {
|
||||
search(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if ( this.loading ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
let url = `/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,
|
||||
} = response.data;
|
||||
let items = [];
|
||||
|
||||
for (let i = 0 ; i < results.length ; i += 1 ) {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
thumb,
|
||||
year,
|
||||
country,
|
||||
format,
|
||||
genre,
|
||||
style,
|
||||
} = results[i];
|
||||
items.push({
|
||||
id,
|
||||
title,
|
||||
thumb,
|
||||
year,
|
||||
country,
|
||||
format,
|
||||
genre,
|
||||
style,
|
||||
});
|
||||
}
|
||||
|
||||
this.items = items;
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(err.response?.data?.message || "Aucun résultat trouvé :/");
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
toggleModal() {
|
||||
this.modalIsVisible = !this.modalIsVisible;
|
||||
},
|
||||
loadDetails(discogsId) {
|
||||
axios.get(`/api/v1/search/${discogsId}`)
|
||||
.then( response => {
|
||||
const {
|
||||
data,
|
||||
} = response;
|
||||
|
||||
this.details = data;
|
||||
this.toggleModal();
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(err.response?.data?.message || "Impossible de charger les détails de cet album");
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
add() {
|
||||
axios.post('/api/v1/albums', this.details)
|
||||
.then(() => {
|
||||
window.location.href = '/ma-collection';
|
||||
})
|
||||
.catch((err) => {
|
||||
showToastr(err.response?.data?.message || "Impossible d'ajouter cet album pour le moment…");
|
||||
});
|
||||
},
|
||||
orderedItems(items) {
|
||||
return items.sort();
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
const canPublish = <%- (user.mastodon && user.mastodon.publish) || false %>;
|
||||
</script>
|
|
@ -1,103 +1,48 @@
|
|||
<main class="layout-maxed collection" id="app">
|
||||
<%
|
||||
const pageType = page.username ? 'public' : 'private';
|
||||
%>
|
||||
|
||||
<main class="layout-maxed collection" id="collection">
|
||||
<h1>
|
||||
Collection de <%= page.username %>
|
||||
<% if ( pageType === 'private' ) {
|
||||
__append('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>');
|
||||
} else {
|
||||
__append(`Collection de ${page.username}`);
|
||||
} %>
|
||||
</h1>
|
||||
<% if ( pageType === 'private' ) { %>
|
||||
<div>
|
||||
<a :href="shareLink" v-if="isPublicCollection" target="_blank">
|
||||
<i class="icon-share"></i> Voir ma collection partagée
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<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>
|
||||
<%- include('../components/filters/index') %>
|
||||
|
||||
<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 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">
|
||||
<div class="item" v-if="!loading" v-for="item in items">
|
||||
<span class="title">
|
||||
{{ item.artists_sort}} - {{ item.title }}
|
||||
<% if ( pageType === 'private' ) { %>
|
||||
<a :href="'/ma-collection/' + item._id">{{ renderAlbumTitle(item) }}</a>
|
||||
<i class="icon-trash" @click="showConfirmDelete(item._id)"></i>
|
||||
<% } else { %>
|
||||
{{ item.artists_sort}} - {{ item.title }}
|
||||
<% } %>
|
||||
</span>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4">
|
||||
<div>
|
||||
<img :src="item.thumb" :alt="item.title" />
|
||||
<% if ( pageType === 'private' ) { %>
|
||||
<a :href="'/ma-collection/' + item._id"><img :src="item.thumb" :alt="item.title" /></a>
|
||||
<% } else { %>
|
||||
<img :src="item.thumb" :alt="item.title" />
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="md:col-span-3">
|
||||
<span><strong>Année :</strong> {{ item.year }}</span>
|
||||
|
@ -129,157 +74,84 @@
|
|||
</div>
|
||||
<nav class="pagination" role="navigation" aria-label="Pagination">
|
||||
<ul class="pagination-list">
|
||||
<li v-if="page > 1">
|
||||
<a class="pagination-link" @click="goTo(1)" aria-label="Aller à la première page">«</a>
|
||||
</li>
|
||||
<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">
|
||||
<template v-if="p < 2 || p > (totalPages - 1) || (Number(page) - 1) <= p && Number(page) + 1 >= p">
|
||||
<li>
|
||||
<a class="pagination-link" :class="{'is-current': p === page}" @click="goTo(p)" aria-label="Aller à la page {{p}}">{{ p }}</a>
|
||||
<a class="pagination-link" :class="{'is-current': p === Number(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)">
|
||||
<template v-if="(Number(page) - 3 === p && Number(page) - 2 > 1) || (Number(page) + 2 === p && Number(page) + 2 < totalPages - 1)">
|
||||
<li>
|
||||
<a class="pagination-link is-disabled">…</a>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
<li v-if="page < totalPages">
|
||||
<a class="pagination-link" @click="goTo(totalPages)" aria-label="Aller à la derière page">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<% if ( pageType === 'private' ) { %>
|
||||
<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,
|
||||
moreFilters: false,
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
limit: 16,
|
||||
artist: '',
|
||||
format: '',
|
||||
year: '',
|
||||
genre: '',
|
||||
style: '',
|
||||
sortOrder: 'artists_sort-asc',
|
||||
sort: 'artists_sort',
|
||||
order: 'asc',
|
||||
userId: "<%= params.userId %>",
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
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')}`;
|
||||
}
|
||||
if ( this.format ) {
|
||||
url += `&format=${this.format.replace('&', '%26')}`;
|
||||
}
|
||||
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')}`;
|
||||
}
|
||||
|
||||
location.href = url;
|
||||
},
|
||||
next(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page += 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
previous(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page -= 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
goTo(page) {
|
||||
this.page = page;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
changeSort() {
|
||||
const [sort,order] = this.sortOrder.split('-');
|
||||
this.sort = sort;
|
||||
this.order = order;
|
||||
this.page = 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
changeFilter() {
|
||||
this.page = 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
showMoreFilters() {
|
||||
this.moreFilters = !this.moreFilters;
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
const vueType = "<%= pageType %>";
|
||||
const query = <%- JSON.stringify(query) %>;
|
||||
<% if ( pageType === 'private' ) { %>
|
||||
const isPublicCollection = <%= user.isPublicCollection ? 'true' : 'false' %>;
|
||||
const userId = "<%= user._id %>";
|
||||
<% } else { %>
|
||||
const userId = "<%= params.userId %>";
|
||||
const isPublicCollection = false;
|
||||
<% } %>
|
||||
</script>
|
||||
|
|
|
@ -497,9 +497,6 @@
|
|||
</div>
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<button type="button" class="button is-primary" id="switchAriaTheme" aria-label="Renforcer la visibilité de ce site" title="Renforcer la visibilité de ce site">
|
||||
<i class="icon-eye"></i>
|
||||
</button>
|
||||
<a class="button is-danger" href="/se-deconnecter">
|
||||
Déconnexion
|
||||
</a>
|
||||
|
|
|
@ -1,99 +1,143 @@
|
|||
<main class="layout-maxed collection" id="app">
|
||||
<main class="layout-maxed collection" id="mon-compte">
|
||||
<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"
|
||||
/>
|
||||
<form method="POST" @submit.prevent="updateProfil">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||
<div>
|
||||
<h2>Mes données personnelles</h2>
|
||||
<div>
|
||||
<div class="field">
|
||||
<label for="email">Adresse e-mail</label>
|
||||
<input
|
||||
type="email"
|
||||
readonly
|
||||
disabled
|
||||
name="email"
|
||||
id="email"
|
||||
v-model="formData.email"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="username">Nom d'utilisateur</label>
|
||||
<input
|
||||
type="string"
|
||||
readonly
|
||||
disabled
|
||||
name="username"
|
||||
id="username"
|
||||
v-model="formData.username"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="oldPassword">Mot de passe actuel</label>
|
||||
<input
|
||||
type="password"
|
||||
name="oldPassword"
|
||||
id="oldPassword"
|
||||
placeholder="Saisisssez votre mot de passe actuel"
|
||||
v-model="formData.oldPassword"
|
||||
/>
|
||||
<div class="message error" v-if="errors.includes('emptyPassword')">
|
||||
Pour changer votre mot de passe vous devez saisir votre mot de passe actuel
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">Nouveau mot de passe</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Saisisssez votre nouveau mot de passe"
|
||||
v-model="formData.password"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="passwordConfirm">Nouveau mot de passe (confirmation)</label>
|
||||
<input
|
||||
type="password"
|
||||
name="passwordConfirm"
|
||||
id="passwordConfirm"
|
||||
placeholder="Confirmez votre nouveau mot de passe"
|
||||
v-model="formData.passwordConfirm"
|
||||
/>
|
||||
<div class="message error" v-if="errors.includes('passwordsDiffer')">
|
||||
La confirmation ne correspond pas avec votre nouveau mot de passe
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<h2>Mon activité</h2>
|
||||
<div>
|
||||
<div class="field">
|
||||
<label for="mastodon.publish">Publier sur le fédiverse lorsque j'ajoute un album</label>
|
||||
<select id="format" v-model="formData.mastodon.publish">
|
||||
<option value="true">Oui</option>
|
||||
<option value="false">Non</option>
|
||||
</select>
|
||||
<!-- <input
|
||||
type="checkbox"
|
||||
name="mastodon.publish"
|
||||
id="mastodon.publish"
|
||||
v-model="mastodon.publish"
|
||||
/> -->
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="mastodon.url">Url de l'API de votre instance</label>
|
||||
<input
|
||||
type="text"
|
||||
name="mastodon.url"
|
||||
id="mastodon.url"
|
||||
v-model="formData.mastodon.url"
|
||||
placeholder="https://mastodon.social/api/v1/"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="mastodon.token">Jeton d'accès (droits nécessaires : write:media et write:statuses)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="mastodon.token"
|
||||
id="mastodon.token"
|
||||
v-model="formData.mastodon.token"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="mastodon.message">Message</label>
|
||||
<textarea
|
||||
name="mastodon.message"
|
||||
id="mastodon.message"
|
||||
v-model="formData.mastodon.message"
|
||||
></textarea>
|
||||
<small>
|
||||
Variables possibles :
|
||||
<ul>
|
||||
<li>{artist}, exemple : Iron Maiden</li>
|
||||
<li>{album}, exemple : Powerslave</li>
|
||||
<li>{format}, exemple : Cassette</li>
|
||||
<li>{year}, exemple: 1984</li>
|
||||
<li>{video}, exemple : https://www.youtube.com/watch?v=Qx0s8OqgBIw</li>
|
||||
</ul>
|
||||
</small>
|
||||
</div>
|
||||
<button type="button" class="button is-secondary mt-10" :disabled="loading" @click="testMastodon">
|
||||
<span>Tester</span>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
const email = '<%= user.email %>';
|
||||
const username = '<%= user.username %>';
|
||||
const mastodon = <%- JSON.stringify(user.mastodon || {publish: false, url: '', token: '', message: ''}) %>;
|
||||
</script>
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
<main class="layout-maxed ma-collection-details" id="app" v-cloak @keyup="changeImage">
|
||||
<main class="layout-maxed ma-collection-details" id="ma-collection-details" v-cloak @keyup="changeImage">
|
||||
|
||||
<h1>
|
||||
{{item.artists_sort}} - {{item.title}}
|
||||
<template v-for="artist in item.artists">
|
||||
<a :href="`/ma-collection?page=1&limit=16&sort=year&order=asc&artist=${artist.name}`">{{artist.name}}</a>
|
||||
<template v-if="artist.join"> {{artist.join}} </template>
|
||||
</template>
|
||||
- {{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>
|
||||
<i class="icon-share" title="Partager cet album sur le fédiverse" @click="showModalShare = true" v-if="canShareItem"></i>
|
||||
</h1>
|
||||
<div class="grid sm:grid-cols-3 gap-16">
|
||||
<div class="text-center">
|
||||
|
@ -16,148 +21,22 @@
|
|||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid md:grid-cols-3 gap-16">
|
||||
<div>
|
||||
<template v-for="album in tracklist">
|
||||
<strong v-if="album.title">{{album.title}}</strong>
|
||||
<ol class="ml-4">
|
||||
<li v-for="track in album.tracks">
|
||||
{{ track.title }} <template v-if="track.duration">({{track.duration}})</template>
|
||||
<ul v-if="track.extraartists && track.extraartists.length > 0" class="sm-hidden ml-4">
|
||||
<li v-for="extra in track.extraartists">
|
||||
<small>{{extra.role}} : {{extra.name}}</small>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div>
|
||||
<strong>Genres</strong>
|
||||
<br />
|
||||
<template v-for="(genre, index) in item.genres">
|
||||
{{genre}}<template v-if="index < item.genres.length - 1">, </template>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Styles</strong>
|
||||
<br />
|
||||
<span v-for="(style, index) in item.styles">
|
||||
{{style}}<template v-if="index < item.styles.length - 1">, </template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid grid-cols-3 gap-10">
|
||||
<div>
|
||||
<strong>Pays</strong>
|
||||
<br />
|
||||
<span>{{item.country}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Année</strong>
|
||||
<br />
|
||||
<span>{{item.year}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Date de sortie</strong>
|
||||
<br />
|
||||
<span>{{item.released}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid gap-10">
|
||||
<div>
|
||||
<strong>Format</strong>
|
||||
<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>
|
||||
</span>)
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div>
|
||||
<strong id="identifiers">Codes barres</strong>
|
||||
<ol class="ml-4">
|
||||
<li v-for="identifier in identifiers">
|
||||
{{identifier.value}} ({{identifier.type}})
|
||||
</li>
|
||||
</ol>
|
||||
<template v-if="item.identifiers.length > identifiersPreviewLength">
|
||||
<button type="button" class="button is-link" v-if="identifiersMode === 'preview'" @click="showAllIdentifiers">
|
||||
Voir la suite
|
||||
</button>
|
||||
<button type="button" class="button is-link" v-if="identifiersMode === 'all'" @click="showLessIdentifiers">
|
||||
Voir moins
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Label</strong>
|
||||
<br />
|
||||
<template v-for="label in item.labels">
|
||||
{{label.name}} {{label.catno}}
|
||||
<br />
|
||||
</template>
|
||||
<hr />
|
||||
<strong>Sociétés</strong>
|
||||
<br />
|
||||
<template v-for="company in item.companies">
|
||||
<strong>{{company.entity_type_name}}</strong> : {{company.name}}
|
||||
<br />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <hr />
|
||||
<div class="grid gap-10">
|
||||
<div>
|
||||
<strong>Note</strong>
|
||||
<br />
|
||||
<span>{{item.notes}}</span>
|
||||
</div>
|
||||
</div> -->
|
||||
<hr />
|
||||
<div class="grid gap-10">
|
||||
<div>
|
||||
<strong>Vidéos</strong>
|
||||
<dl>
|
||||
<template v-for="video in item.videos">
|
||||
<dt>
|
||||
<a :href="video.uri" target="_blank" rel="noopener noreferrer">{{video.title}}</a>
|
||||
</dt>
|
||||
<dd>
|
||||
{{video.description}}
|
||||
</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('../../../components/album.ejs') %>
|
||||
|
||||
<div class="modal" :class="{'is-visible': modalIsVisible}">
|
||||
<div class="modal-background"></div>
|
||||
<button type="button" aria-label="Fermer" class="close" @click="toggleModal"></button>
|
||||
<button type="button" aria-label="Image précédente" class="navigation previous" @click="previous" v-if="index > 0">
|
||||
<i class="icon-left-open"></i>
|
||||
</button>
|
||||
<button type="button" aria-label="Image suivante" class="navigation next" @click="next" v-if="index + 1 < item.images.length">
|
||||
<i class="icon-right-open"></i>
|
||||
</button>
|
||||
<div class="modal-card">
|
||||
<img :src="preview" />
|
||||
<div class="carousel">
|
||||
<button type="button" aria-label="Image précédente" class="navigation previous" @click="previous">
|
||||
<i class="icon-left-open"></i>
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<img :src="preview" />
|
||||
</div>
|
||||
<button type="button" aria-label="Image suivante" class="navigation next" @click="next">
|
||||
<i class="icon-right-open"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -170,164 +49,50 @@
|
|||
</section>
|
||||
<footer>
|
||||
<button class="button is-primary" @click="deleteItem">Supprimer</button>
|
||||
<button class="button" @click="toggleModal">Annuler</button>
|
||||
<button class="button" @click="toggleModalDelete">Annuler</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" :class="{'is-visible': showModalShare}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header>Partager un album sur le fédiverse</header>
|
||||
<section>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||
<div class="field">
|
||||
<label for="shareMessage">Message</label>
|
||||
<textarea
|
||||
name="shareMessage"
|
||||
id="shareMessage"
|
||||
v-model="shareMessage"
|
||||
rows="6"
|
||||
></textarea>
|
||||
Caractères utilisés : {{ shareMessageLength }}
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
Variables possibles :
|
||||
<ul>
|
||||
<li>{artist}, exemple : Iron Maiden</li>
|
||||
<li>{album}, exemple : Powerslave</li>
|
||||
<li>{format}, exemple : Cassette</li>
|
||||
<li>{year}, exemple: 1984</li>
|
||||
<li>{video}, exemple : https://www.youtube.com/watch?v=Qx0s8OqgBIw</li>
|
||||
</ul>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<button :class="['button is-primary', shareSubmiting ? 'is-disabled' : '']" @click="shareAlbum">Partager</button>
|
||||
<button class="button" @click="showModalShare=!showModalShare">Annuler</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
item: <%- JSON.stringify(page.item) %>,
|
||||
tracklist: [],
|
||||
identifiers: [],
|
||||
modalIsVisible: false,
|
||||
identifiersMode: 'preview',
|
||||
identifiersPreviewLength: 16,
|
||||
preview: null,
|
||||
index: null,
|
||||
showModalDelete: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setTrackList();
|
||||
this.setIdentifiers();
|
||||
|
||||
window.addEventListener("keydown", this.changeImage);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('keydown', this.changeImage);
|
||||
},
|
||||
methods: {
|
||||
setIdentifiers() {
|
||||
this.identifiers = [];
|
||||
|
||||
let max = this.identifiersMode == 'preview' && this.item.identifiers.length > this.identifiersPreviewLength ? this.identifiersPreviewLength : this.item.identifiers.length;
|
||||
|
||||
for ( let i = 0 ; i < max ; i += 1 ) {
|
||||
this.identifiers.push(this.item.identifiers[i]);
|
||||
}
|
||||
},
|
||||
setTrackList() {
|
||||
let subTrack = {
|
||||
type: null,
|
||||
title: null,
|
||||
tracks: [],
|
||||
};
|
||||
for (let i = 0 ; i < this.item.tracklist.length ; i += 1 ) {
|
||||
const {
|
||||
type_,
|
||||
title,
|
||||
position,
|
||||
duration,
|
||||
extraartists,
|
||||
} = this.item.tracklist[i];
|
||||
|
||||
if ( type_ === 'heading' ) {
|
||||
if ( subTrack.type ) {
|
||||
this.tracklist.push(subTrack);
|
||||
subTrack = {
|
||||
type: null,
|
||||
title: null,
|
||||
tracks: [],
|
||||
};
|
||||
}
|
||||
|
||||
subTrack.type = type_;
|
||||
subTrack.title = title;
|
||||
} else {
|
||||
subTrack.tracks.push({
|
||||
title,
|
||||
position,
|
||||
duration,
|
||||
extraartists
|
||||
});
|
||||
}
|
||||
}
|
||||
this.tracklist.push(subTrack);
|
||||
},
|
||||
setImage() {
|
||||
this.preview = this.item.images[this.index].uri;
|
||||
},
|
||||
showGallery(event) {
|
||||
const item = event.target.tagName === 'IMG' ? event.target.parentElement : event.target;
|
||||
|
||||
const {
|
||||
index,
|
||||
} = item.dataset;
|
||||
|
||||
this.index = Number(index);
|
||||
this.modalIsVisible = true;
|
||||
|
||||
this.setImage();
|
||||
},
|
||||
toggleModal() {
|
||||
this.modalIsVisible = !this.modalIsVisible;
|
||||
},
|
||||
previous() {
|
||||
this.index = this.index > 0 ? this.index - 1 : this.item.images.length -1;
|
||||
this.setImage();
|
||||
},
|
||||
next() {
|
||||
this.index = (this.index +1) === this.item.images.length ? 0 : this.index + 1;
|
||||
this.setImage();
|
||||
},
|
||||
changeImage(event) {
|
||||
const direction = event.code;
|
||||
|
||||
if ( this.modalIsVisible && ['ArrowRight', 'ArrowLeft', 'Escape'].indexOf(direction) !== -1 ) {
|
||||
switch (direction) {
|
||||
case 'ArrowRight':
|
||||
return this.next();
|
||||
case 'ArrowLeft':
|
||||
return this.previous();
|
||||
default:
|
||||
this.modalIsVisible = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
showAllIdentifiers() {
|
||||
this.identifiersMode = 'all';
|
||||
this.setIdentifiers();
|
||||
},
|
||||
showLessIdentifiers() {
|
||||
this.identifiersMode = 'preview';
|
||||
this.setIdentifiers();
|
||||
|
||||
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');
|
||||
const item = <%- JSON.stringify(page.item) %>;
|
||||
const canShareItem = <%= user.mastodon?.publish || false %>;
|
||||
</script>
|
|
@ -1,4 +1,4 @@
|
|||
<main class="layout-maxed" id="app">
|
||||
<main class="layout-maxed" id="exporter">
|
||||
<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 :
|
||||
|
@ -44,25 +44,4 @@
|
|||
Exporter
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
format: 'xml',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
exportCollection(event) {
|
||||
event.preventDefault();
|
||||
|
||||
window.open(`/api/v1/albums?exportFormat=${this.format}`, '_blank');
|
||||
}
|
||||
},
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</main>
|
43
views/pages/mon-compte/ma-collection/importer.ejs
Normal file
43
views/pages/mon-compte/ma-collection/importer.ejs
Normal file
|
@ -0,0 +1,43 @@
|
|||
<main class="layout-maxed" id="importer">
|
||||
<h1>Importer une collection</h1>
|
||||
<p>
|
||||
Il est actuellement possible d'importer une collection provenant de discogs.
|
||||
<br />
|
||||
Vous devez dans un premier temps vous rendre sur la page <a href="https://www.discogs.com/fr/users/export" target="_blank" rel="noopener noreferrer">Exporter</a> de discogs.
|
||||
<br />
|
||||
Une fois exporter vous recevrez un mail de Discogs avec un lien de téléchargement. Une fois le fichier .zip téléchargé vous devez en extraire le fichier .csv afin de l'importer dans MusicTopus.
|
||||
</p>
|
||||
<p>
|
||||
D'autres formats d'imports seront ajoutés par la suite, comme l'import entre 2 instances MusicTopus.
|
||||
</p>
|
||||
<div class="flash info">
|
||||
<div class="header">
|
||||
Information
|
||||
</div>
|
||||
<div class="body">
|
||||
Si un album est déjà présent en base celui-ci sera ignoré.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit="importCollection">
|
||||
|
||||
<div class="field">
|
||||
<label for="file">Fichier .csv</label>
|
||||
<input type="file" name="file" id="file" @change="handleFileUpload( $event )" accept=".csv">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span>
|
||||
Albums à impoter : <strong>{{content.length}}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button is-primary my-16" :disabled="disabled">
|
||||
<i v-if="['parse', 'submit'].includes(state)" class="icon-spin animate-spin"></i>
|
||||
<span v-if="state === 'default'">Importer</span>
|
||||
<span v-if="state === 'parse'">Analyse en cours...</span>
|
||||
<span v-if="state === 'submit'">Importation en cours... ({{imported}}/{{content.length}})</span>
|
||||
<span v-if="state === 'done'">Importatation terminée</span>
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
|
@ -1,380 +0,0 @@
|
|||
<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="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="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>
|
||||
<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,
|
||||
moreFilters: false,
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
limit: 16,
|
||||
artist: '',
|
||||
format: '',
|
||||
year: '',
|
||||
genre: '',
|
||||
style: '',
|
||||
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;
|
||||
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')}`;
|
||||
}
|
||||
if ( this.format ) {
|
||||
url += `&format=${this.format.replace('&', '%26')}`;
|
||||
}
|
||||
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')}`;
|
||||
}
|
||||
|
||||
location.href = url;
|
||||
},
|
||||
next(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page += 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
previous(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.page -= 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
goTo(page) {
|
||||
this.page = page;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
changeSort() {
|
||||
const [sort,order] = this.sortOrder.split('-');
|
||||
this.sort = sort;
|
||||
this.order = order;
|
||||
this.page = 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
changeFilter() {
|
||||
this.page = 1;
|
||||
|
||||
this.changeUrl();
|
||||
},
|
||||
showMoreFilters() {
|
||||
this.moreFilters = !this.moreFilters;
|
||||
},
|
||||
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>
|
|
@ -1,4 +1,4 @@
|
|||
<section class="box" id="app">
|
||||
<section class="box" id="contact">
|
||||
<h1>Nous contacter</h1>
|
||||
<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">
|
||||
|
@ -34,47 +34,6 @@
|
|||
</form>
|
||||
</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>
|
||||
<% } %>
|
||||
<script>
|
||||
const contactMethod = '<%= config.mailMethod %>';
|
||||
</script>
|
Loading…
Reference in a new issue