Some changes in structure + add album

This commit is contained in:
Damien Broqua 2022-02-15 11:03:20 +01:00
parent 3ebdc9c06a
commit f08e70eb7c
36 changed files with 883 additions and 165 deletions

View file

@ -16,7 +16,13 @@ module.exports = {
'no-underscore-dangle': [ 'no-underscore-dangle': [
'error', 'error',
{ {
allow: ['_id'], allow: ['_id', 'artists_sort'],
},
],
'camelcase': [
'error',
{
allow: ['artists_sort',]
}, },
], ],
}, },

1
.gitignore vendored
View file

@ -120,3 +120,4 @@ dist
dist dist
yarn.lock yarn.lock
public/css

View file

@ -1,2 +1,33 @@
# nodecdtheque # nodecdtheque
NodeCDThèque (non temporaire faute de mieux haha) est une application Web permettant de lister votre collection de CD ou vinyles.
## Prérequis
Pour fonctionner vous devez avoir `docker` d'installer sur votre serveur.
## Installation
### Préparer la terre
Après avoir cloné le projet il faudra créer un fichier d'environnement nommé `.env` et situé à la racine de projet.
Contenu du fichier :
```
NODE_ENV=development
DISCOGS_TOKEN=***
```
Pour obtenir un token Discogs vous devez simplement vous inscrire sur [Discogs](https://www.discogs.com). Une fois connecté à votre espace Discogs vous pourrez obtenir un token en allant dans le sous-menu [Développeur](https://www.discogs.com/settings/developers).
### Semer
Vous pouvez maintenant simplement lancer la commande suivante afin de démarrer le projet en tant que service :
```
docker-compose up -d
```
### Pailler
Une fois le serveur démarré vous pourrez accéder à l'interface web via [http://127.0.0.1:3001](http://127.0.0.1:3001). (ou tout autre port si vous avez défini `PORT` dans le fichier `.env`).

View file

@ -19,7 +19,8 @@ services:
depends_on: depends_on:
- nodecdtheque-db - nodecdtheque-db
environment: environment:
NODE_ENV: "development" NODE_ENV: ${NODE_ENV}
DISCOGS_TOKEN: ${DISCOGS_TOKEN}
networks: networks:
- nodecdtheque - nodecdtheque
nodecdtheque-db: nodecdtheque-db:

View file

@ -4,8 +4,9 @@
"description": "Simple application to manage your CD/Vinyl collection", "description": "Simple application to manage your CD/Vinyl collection",
"scripts": { "scripts": {
"start": "node ./dist/bin/www", "start": "node ./dist/bin/www",
"dev": "npm-run-all build start", "dev": "npm-run-all build sass start",
"watch": "nodemon -e js,ejs", "watch": "nodemon",
"sass": "npx sass sass/*.scss public/css/main.css -s compressed",
"prebuild": "rimraf dist", "prebuild": "rimraf dist",
"build": "babel ./src --out-dir dist --copy-files", "build": "babel ./src --out-dir dist --copy-files",
"test": "jest", "test": "jest",
@ -42,20 +43,26 @@
"rimraf": "^3.0.2" "rimraf": "^3.0.2"
}, },
"dependencies": { "dependencies": {
"axios": "^0.26.0",
"connect-ensure-login": "^0.1.1", "connect-ensure-login": "^0.1.1",
"connect-flash": "^0.1.1", "connect-flash": "^0.1.1",
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"debug": "^4.3.3", "debug": "^4.3.3",
"disconnect": "^1.2.2",
"ejs": "^3.1.6", "ejs": "^3.1.6",
"express": "^4.17.2", "express": "^4.17.2",
"express-session": "^1.17.2", "express-session": "^1.17.2",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"mdbootstrap": "^4.20.0", "mdb-ui-kit": "^3.10.2",
"moment": "^2.29.1",
"mongoose": "^6.2.1", "mongoose": "^6.2.1",
"mongoose-unique-validator": "^3.0.0", "mongoose-unique-validator": "^3.0.0",
"passport": "^0.5.2", "passport": "^0.5.2",
"passport-local": "^1.0.0" "passport-http": "^0.3.0",
"passport-local": "^1.0.0",
"sass": "^1.49.7",
"vue": "^3.2.31"
}, },
"nodemonConfig": { "nodemonConfig": {
"exec": "npm run dev", "exec": "npm run dev",

View file

@ -0,0 +1 @@
#toastr{visibility:hidden;min-width:250px;margin-left:-125px;background-color:#9c3030;color:#fff;text-align:left;border-radius:2px;padding:16px;position:fixed;z-index:1;right:30px;top:30px;font-size:17px}#toastr.show{visibility:visible;animation:toastrFadein .5s,toastrFadeout .5s 2.5s}@keyframes toastrFadein{from{top:0;opacity:0}to{top:30px;opacity:1}}@keyframes toastrFadeout{from{top:30px;opacity:1}to{top:0;opacity:0}}/*# sourceMappingURL=main.css.map */

View file

42
sass/toast.scss Normal file
View file

@ -0,0 +1,42 @@
#toastr {
visibility: hidden;
min-width: 250px;
margin-left: -125px;
background-color: rgb(156, 48, 48);
color: #fff;
text-align: left;
border-radius: 2px;
padding: 16px;
position: fixed;
z-index: 1;
right: 30px;
top: 30px;
font-size: 17px;
&.show {
visibility: visible;
animation: toastrFadein 0.5s, toastrFadeout 0.5s 2.5s;
}
}
@keyframes toastrFadein {
from {
top: 0;
opacity: 0;
}
to {
top: 30px;
opacity: 1;
}
}
@keyframes toastrFadeout {
from {
top: 30px;
opacity: 1;
}
to {
top: 0;
opacity: 0;
}
}

View file

@ -9,10 +9,14 @@ import MongoStore from "connect-mongo";
import config, { env, mongoDbUri, secret } from "./config"; import config, { env, mongoDbUri, secret } from "./config";
import indexRouter from "./routes/index"; import indexRouter from "./routes";
import addAlbumRouter from "./routes/addAlbum";
import importRouterApiV1 from "./routes/api/v1";
import importAlbumRouterApiV1 from "./routes/api/v1/albums";
// Mongoose schema init // Mongoose schema init
require("./models/users"); require("./models/users");
require("./models/albums");
require("./libs/passport")(passport); require("./libs/passport")(passport);
@ -60,7 +64,7 @@ if (["production"].indexOf(env) !== -1) {
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());
app.set("views", path.join(__dirname, "views")); app.set("views", path.join(__dirname, "../views"));
app.set("view engine", "ejs"); app.set("view engine", "ejs");
app.use(express.static(path.join(__dirname, "../public"))); app.use(express.static(path.join(__dirname, "../public")));
@ -69,45 +73,65 @@ app.use(
express.static(path.join(__dirname, "../node_modules/jquery/dist/")) express.static(path.join(__dirname, "../node_modules/jquery/dist/"))
); );
app.use( app.use(
"/libs/mdbootstrap", "/libs/mdb-ui-kit/css",
express.static(path.join(__dirname, "../node_modules/mdbootstrap")) express.static(path.join(__dirname, "../node_modules/mdb-ui-kit/css"))
);
app.use(
"/libs/mdb-ui-kit/js",
express.static(path.join(__dirname, "../node_modules/mdb-ui-kit/js"))
);
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("/", indexRouter);
app.use("/ajouter-un-album", addAlbumRouter);
app.use("/api/v1", importRouterApiV1);
app.use("/api/v1/albums", importAlbumRouterApiV1);
// Handle 404 // Handle 404
app.use((req, res) => { app.use((req, res) => {
if (req.xhr) { if (req.xhr || req.rawHeaders.indexOf("application/json") !== -1) {
res.status(404).send({ message: "404: Not found" }); res.status(404).send({ message: "404: Not found" });
} else { } else {
res.status(404).render("error", { res.status(404).render("index", {
page: { title: `404: Cette page n'existe pas.` }, page: { title: `404: Cette page n'existe pas.` },
errorCode: 404, errorCode: 404,
viewname: "error",
user: req.user || null, user: req.user || null,
config, config,
session: req.session || null, session: req.session || null,
flashInfo: null, flashInfo: null,
query: null, query: null,
params: null, params: null,
error: null,
}); });
} }
}); });
// Handle 500 // Handle 500
app.use((error, req, res, next) => { app.use((error, req, res, next) => {
if (req.xhr) { if (req.xhr || req.rawHeaders.indexOf("application/json") !== -1) {
res.status(error.errorCode || 500).send({ message: error.message }); const { message, errorCode, date } = error;
res.status(error.errorCode || 500).send({ message, errorCode, date });
} else { } else {
res.status(500); res.status(error.errorCode || 500);
res.render("error", { res.render("index", {
page: { title: "500: Oups… le serveur a crashé !", error }, page: { title: "500: Oups… le serveur a crashé !", error },
errorCode: error.errorCode || 500, errorCode: error.errorCode || 500,
viewname: "error",
user: req.user || null, user: req.user || null,
config, config,
session: req.session || null, session: req.session || null,
flashInfo: null, flashInfo: null,
query: null, query: null,
params: null, params: null,
error: null,
}); });
next(); next();

View file

@ -3,4 +3,5 @@ module.exports = {
port: parseInt(process.env.PORT || "3001", 10), port: parseInt(process.env.PORT || "3001", 10),
mongoDbUri: process.env.MONGODB_URI || "mongodb://nodecdtheque-db/cdtheque", mongoDbUri: process.env.MONGODB_URI || "mongodb://nodecdtheque-db/cdtheque",
secret: process.env.SECRET || "waemaeMe5ahc6ce1chaeKohKa6Io8Eik", secret: process.env.SECRET || "waemaeMe5ahc6ce1chaeKohKa6Io8Eik",
discogsToken: process.env.DISCOGS_TOKEN,
}; };

View file

@ -1,3 +1,25 @@
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import { Client as Discogs } from "disconnect";
import { discogsToken } from "../config";
export const getBaseUrl = (req) => `${req.protocol}://${req.get("host")}`; export const getBaseUrl = (req) => `${req.protocol}://${req.get("host")}`;
export const searchSong = async (q) => {
const dis = new Discogs({ userToken: discogsToken }).database();
const res = await dis.search({
q,
type: "release",
});
return res;
};
export const getAlbumDetails = async (id) => {
const dis = new Discogs({ userToken: discogsToken }).database();
const res = await dis.getRelease(id);
return res;
};

21
src/libs/error.js Normal file
View file

@ -0,0 +1,21 @@
/**
* Classe permettant la gestion des erreurs personilisées
*/
class ErrorEvent extends Error {
/**
* @param {Number} errorCode
* @param {Mixed} ...params
*/
constructor(errorCode, ...params) {
super(...params);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ErrorEvent);
}
this.errorCode = parseInt(errorCode, 10);
this.date = new Date();
}
}
export default ErrorEvent;

View file

@ -1,3 +1,39 @@
/**
* Fonction permettant de formater une réponse basée sur la méthode utilisée
* @param {Object} req
* @param {Object} res
* @param {Object} data
*
* @return {Object}
*/
export const sendResponse = (req, res, data) => {
let status = 200;
const path = req.route.path.split("/");
switch (req.method) {
case "GET":
// INFO: On regarde de quel type de get il s'agit (liste ou item)
if (path[path.length - 1].indexOf(":") === 0) {
// INFO: Cas d'un item
status = !data ? 404 : 200;
} else {
// INFO: Cas d'une liste
status =
data.rows === undefined || data.rows.length > 0 ? 200 : 204;
}
return res.status(status).json(data).end();
case "PATCH":
return res.status(200).json(data).end();
case "DELETE":
return res.status(200).json(data).end();
case "POST":
return res.status(201).json(data).end();
default:
return res.status(500).json({ message: "Not implemented" });
}
};
export default (res, page) => { export default (res, page) => {
res.status(200).render("index", page.render()); res.status(200).render("index", page.render());
}; };

View file

@ -1,6 +1,7 @@
/* eslint-disable func-names */ /* eslint-disable func-names */
const mongoose = require("mongoose"); const mongoose = require("mongoose");
const LocalStrategy = require("passport-local").Strategy; const LocalStrategy = require("passport-local").Strategy;
const { BasicStrategy } = require("passport-http");
const Users = mongoose.model("Users"); const Users = mongoose.model("Users");
@ -36,4 +37,22 @@ module.exports = function (passport) {
} }
) )
); );
passport.use(
"basic",
new BasicStrategy((email, password, done) => {
Users.findOne({ email })
.then((user) => {
if (!user || !user.validPassword(password)) {
return done(
null,
false,
"Oops! Identifiants incorrects"
);
}
return done(null, user);
})
.catch(done);
})
);
}; };

85
src/middleware/Albums.js Normal file
View file

@ -0,0 +1,85 @@
import moment from "moment";
import Pages from "./Pages";
import { getAlbumDetails } from "../helpers";
import AlbumsModel from "../models/albums";
/**
* Classe permettant la gestion des albums d'un utilisateur
*/
class Albums extends Pages {
async getFormAddOne() {
const data = await getAlbumDetails(this.req.params.discogsId);
const {
id, // Integer
year, // - Integer
uri, // String
artists, // - Array<Object>
artists_sort, // String
labels, // - Array<Object>
series, // Array
companies, // - Array<Object>
formats, // - Array<Object>
title, // - String
country, // - String
released, // - Date
notes, // - String
identifiers, // - Array<Object>
videos, // - Array<Object>
genres, // - Array<String>
styles, // - Array<String>
tracklist, // - Array<Object>
extraartists, // - Array<Object>
images, // - Array<Object
thumb, // - String
} = data;
this.pageContent.page.values = "test";
this.setPageContent("values", {
id,
year,
uri,
artists,
artists_sort,
labels,
series,
companies,
formats,
title,
country,
released,
notes,
identifiers,
videos,
genres,
styles,
tracklist,
extraartists,
images,
thumb,
});
return true;
}
static async postAddOne(req) {
const { body, user } = req;
const data = {
...body,
discogsId: body.id,
User: user._id,
};
data.released = moment(data.released.replace("-00", "-01"));
delete data.id;
const album = new AlbumsModel(data);
return album.save();
}
}
export default Albums;

View file

@ -34,6 +34,10 @@ class Pages {
} }
} }
setPageContent(field, value) {
this.pageContent.page[field] = value;
}
/** /**
* Rendu de la page * Rendu de la page
* @return {Object} * @return {Object}
@ -46,7 +50,7 @@ class Pages {
this.pageContent.params = this.req.params; this.pageContent.params = this.req.params;
this.pageContent.user = this.user; this.pageContent.user = this.user;
this.pageContent.config = config; this.pageContent.config = config;
this.pageContent.getBaseUrl = getBaseUrl(); this.pageContent.getBaseUrl = getBaseUrl(this.req);
if (this.req.session.flash && this.req.session.flash.error) { if (this.req.session.flash && this.req.session.flash.error) {
// eslint-disable-next-line prefer-destructuring // eslint-disable-next-line prefer-destructuring

36
src/models/albums.js Normal file
View file

@ -0,0 +1,36 @@
import mongoose from "mongoose";
const { Schema } = mongoose;
const AlbumSchema = new mongoose.Schema(
{
User: {
type: Schema.Types.ObjectId,
ref: "Users",
},
discogsId: Number,
year: Number,
released: Date,
uri: String,
artists: Array,
artists_sort: String,
labels: Array,
series: Array,
companies: Array,
formats: Array,
title: String,
country: String,
notes: String,
identifiers: Array,
videos: Array,
genres: Array,
styles: Array,
tracklist: Array,
extraartists: Array,
images: Array,
thumb: String,
},
{ timestamps: true }
);
export default mongoose.model("Albums", AlbumSchema);

35
src/routes/addAlbum.js Normal file
View file

@ -0,0 +1,35 @@
import express from "express";
import { ensureLoggedIn } from "connect-ensure-login";
import Pages from "../middleware/Albums";
import render from "../libs/format";
// eslint-disable-next-line new-cap
const router = express.Router();
router.route("/").get(ensureLoggedIn("/connexion"), (req, res, next) => {
try {
const page = new Pages(req, "ajouter-un-album/search");
render(res, page);
} catch (err) {
next(err);
}
});
router
.route("/:discogsId")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const page = new Pages(req, "ajouter-un-album/form");
await page.getFormAddOne();
render(res, page);
} catch (err) {
next(err);
}
});
export default router;

View file

@ -0,0 +1,20 @@
import express from "express";
import { ensureLoggedIn } from "connect-ensure-login";
import { sendResponse } from "../../../libs/format";
import Albums from "../../../middleware/Albums";
// eslint-disable-next-line new-cap
const router = express.Router();
router.route("/").post(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const data = await Albums.postAddOne(req);
sendResponse(req, res, data);
} catch (err) {
next(err);
}
});
export default router;

View file

@ -0,0 +1,22 @@
import express from "express";
import { ensureLoggedIn } from "connect-ensure-login";
import { sendResponse } from "../../../libs/format";
import { searchSong } from "../../../helpers";
// eslint-disable-next-line new-cap
const router = express.Router();
router
.route("/search")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const data = await searchSong(req.query.q);
sendResponse(req, res, data);
} catch (err) {
next(err);
}
});
export default router;

View file

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<%- include('partials/head', {page: page, user: user}); %>
<body class="error">
<%- include('partials/header'); %>
<main class="mt-4">
<div class="container">
<section class="px-md-5 mx-md-5 dark-grey-text mb-4">
<h1><%= page.title %></h1>
<% if ( errorCode && errorCode === 404 ) { %>
<img src="/img/404.svg" alt="Erreur 404" style="max-height: 400px;" />
<% } %>
<p class="lead">
<%= page.error %>
</p>
</section>
</div>
</main>
<%- include('partials/footer', {page: page, user: user, blog: null}); %>
</body>
</html>

View file

@ -1,27 +0,0 @@
<!doctype html>
<html lang="fr">
<%- include('partials/head'); %>
<body>
<%- include('partials/header'); %>
<% if ( page.failureFlash ) {%>
<div class="alert alert-danger" role="alert">
<%= page.failureFlash %>
</div>
<% } %>
<%
if (error && error.length > 0) {
for( let i = 0 ; i < error.length ; i += 1 ) {
%>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<%
}
}
%>
<%- include(viewname) %>
<%- include('partials/footer'); %>
</body>
</html>

View file

@ -1,24 +0,0 @@
<div class="container">
<div class="d-flex justify-content-center">
<div class="p-2">
<form class="text-center border border-light p-5" method="POST">
<img class="mb-4" src="/img/logo.png" alt="DarKou">
<p class="h4 mb-4">Connexion</p>
<div class="md-form">
<input type="email" id="email" name="email" class="form-control" required>
<label for="email">Adresse e-mail</label>
</div>
<div class="md-form">
<input type="password" id="password" name="password" class="form-control" required>
<label for="password">Mot de passe</label>
</div>
<button class="btn btn-primary btn-block my-4" type="submit">Connexion</button>
<p>Pas encore inscrit ? <a href="/inscription">Inscrivez-vous</a></p>
</form>
</div>
</div>
</div>

View file

@ -1,29 +0,0 @@
<div class="container">
<div class="d-flex justify-content-center">
<div class="p-2">
<form class="text-center border border-light p-5" method="POST">
<img class="mb-4" src="/img/logo.png" alt="DarKou">
<p class="h4 mb-4">Inscription</p>
<div class="md-form">
<input type="text" id="username" name="username" class="form-control" required>
<label for="username">Nom d'utilisateur</label>
</div>
<div class="md-form">
<input type="email" id="email" name="email" class="form-control" required>
<label for="email">Adresse e-mail</label>
</div>
<div class="md-form">
<input type="password" id="password" name="password" class="form-control" required>
<label for="password">Mot de passe</label>
</div>
<button class="btn btn-primary btn-block my-4" type="submit">Inscription</button>
<p>Déjà inscrit ? <a href="/connexion">Connectez-vous</a></p>
</form>
</div>
</div>
</div>

View file

@ -1,5 +0,0 @@
<script type="text/javascript" src="/libs/mdbootstrap/js/jquery.min.js"></script>
<script type="text/javascript" src="/libs/mdbootstrap/js/popper.min.js"></script>
<script type="text/javascript" src="/libs/mdbootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/libs/mdbootstrap/js/mdb.min.js"></script>
<script type="text/javascript" src="/js/main.js"></script>

View file

@ -1,17 +0,0 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><% if (page.title) { %><%= page.title %> <% } else { %> DarKou - Ma CDThèque <% } %></title>
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.11.2/css/all.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap">
<link rel="stylesheet" href="/libs/mdbootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="/libs/mdbootstrap/css/mdb.min.css">
<link rel="stylesheet" href="/libs/mdbootstrap/css/style.css">
<link rel="stylesheet" href="/css/main.css" />
</head>

View file

@ -1,24 +0,0 @@
<nav class="navbar navbar-expand-md navbar-dark primary-color sticky-top">
<a class="navbar-brand" href="/">CDThèque</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<% if ( user ) { %>
<div class="navbar-collapse collapse w-100 order-1 dual-collapse2">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/upload">Ajouter une image</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Mon compte</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="/gallery">Mes images</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/se-deconnecter">Déconnexion</a>
</div>
</li>
</ul>
</div>
<% } %>
</nav>

11
views/error.ejs Normal file
View file

@ -0,0 +1,11 @@
<div class="container">
<section class="px-md-5 mx-md-5 dark-grey-text mb-4">
<h1><%= page.title %></h1>
<% if ( errorCode && errorCode === 404 ) { %>
<img src="/img/404.svg" alt="Erreur 404" style="max-height: 400px;" />
<% } %>
<pre>
<%= page.error %>
</pre>
</section>
</div>

37
views/index.ejs Normal file
View file

@ -0,0 +1,37 @@
<!doctype html>
<html lang="fr">
<%- include('partials/head'); %>
<body>
<header>
<%- include('partials/header'); %>
</header>
<div id="toastr"></div>
<main class="mt-4 mb-5">
<% if ( page.failureFlash ) {%>
<div class="alert alert-danger" role="alert">
<%= page.failureFlash %>
</div>
<% } %>
<%
if (error && error.length > 0) {
for( let i = 0 ; i < error.length ; i += 1 ) {
%>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<%
}
}
%>
<%- include(viewname) %>
</main>
<footer class="bg-light text-lg-start">
<%- include('partials/footer'); %>
</footer>
</body>
</html>

View file

@ -0,0 +1,144 @@
<div class="container-fluid" id="app">
<form class="bg-white rounded shadow-5-strong p-5" method="POST" @submit="add">
<div class="row">
<div class="col-12 col-sm-3 p-2 text-center">
<img src="<%= page.values.thumb %>" alt="Miniature" />
<hr />
<img v-for="image in album.images" :src="image.uri150" alt="Miniature" style="max-width: 60px;" />
<hr />
<ol>
<li v-for="track in album.tracklist">{{ track.title }} ({{track.duration}})</li>
</ol>
</div>
<div class="col-12 col-sm-9 p-2">
<div class="row">
<div class="col-12 p-2">
<div class="form-outline mb-4">
<input type="text" id="title" name="title" class="form-control" v-model="album.title" disabled />
<label class="form-label" for="artists">Titre</label>
</div>
<div class="form-outline mb-4" v-for="artist in album.artists">
<input type="text" id="artists" name="artists" class="form-control" v-model="artist.name" disabled />
<label class="form-label" for="artists">Artiste</label>
</div>
</div>
<hr />
<div class="col-12 col-sm-6 p-2" v-for="genre in album.genres">
<div class="form-outline mb-4">
<input type="text" id="genres" name="genres" class="form-control" v-model="genre" disabled />
<label class="form-label" for="company">Genre</label>
</div>
</div>
<div class="col-12 col-sm-6 p-2" v-for="style in album.styles">
<div class="form-outline mb-4">
<input type="text" id="style" name="style" class="form-control" v-model="style" disabled />
<label class="form-label" for="company">Style</label>
</div>
</div>
<hr />
<div class="col-12 col-sm-6 p-2">
<div class="form-outline mb-4">
<input type="text" id="year" name="year" class="form-control" v-model="album.year" disabled />
<label class="form-label" for="year">Année</label>
</div>
</div>
<div class="col-12 col-sm-6 p-2">
<div class="form-outline mb-4">
<input type="text" id="released" name="released" class="form-control" v-model="album.released" disabled />
<label class="form-label" for="released">Date de sortie</label>
</div>
</div>
<hr />
<div class="col-12 col-sm-6 p-2">
<div class="form-outline mb-4">
<input type="text" id="country" name="country" class="form-control" v-model="album.country" disabled />
<label class="form-label" for="country">Pays</label>
</div>
</div>
<hr />
<div class="col-12 col-sm-6 p-2">
<ol>
<li v-for="identifier in album.identifiers">
{{identifier.value}} ({{identifier.type}})
</li>
</ol>
</div>
<hr />
<div class="col-12 p-2">
<div class="form-outline mb-4">
<textarea id="notes" id="notes" name="notes" class="form-control" v-model="album.notes" disabled></textarea>
<label class="form-label" for="country">Notes</label>
</div>
</div>
<hr />
<div class="col-12 col-sm-6 p-2" v-for="format in album.formats">
<div class="form-outline mb-4">
<input type="text" id="format" name="format" class="form-control" v-model="format.name" disabled />
<label class="form-label" for="format">Format</label>
</div>
</div>
<hr />
<div class="col-12 col-sm-6 p-2" v-for="label in album.labels">
<div class="form-outline mb-4">
<input type="text" id="label" name="label" class="form-control" v-model="label.name" disabled />
<label class="form-label" for="label">Label</label>
</div>
</div>
<hr />
<div class="col-12 col-sm-6 p-2" v-for="company in album.companies">
<div class="form-outline mb-4">
<input type="text" id="company" name="company" class="form-control" v-model="company.name" disabled />
<label class="form-label" for="company">Société</label>
</div>
</div>
<hr />
<div class="col-12 col-sm-6 p-2">
<ol>
<li v-for="video in album.videos">
<a :href="video.uri" target="_blank">{{video.title}}</a>
</li>
</ol>
</div>
<hr />
<div class="col-12 col-sm-6 p-2">
<ol>
<li v-for="extraartist in album.extraartists">
{{extraartist.name}} ({{extraartist.role}})
</li>
</ol>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Ajouter</button>
</form>
</div>
<script>
const defaultValues = <%- JSON.stringify(page.values) %>;
Vue.createApp({
data() {
return {
album: defaultValues,
}
},
methods: {
add(event) {
event.preventDefault();
axios.post('/api/v1/albums', this.album)
.then(() => {
window.location.href = '/ma-collection';
})
.catch((err) => {
console.log('ERR:', err.response);
showToastr(err.response?.data?.message || "Impossible d'ajouter ce album pour le moment…");
});
}
}
}).mount('#app')
</script>

View file

@ -0,0 +1,105 @@
<div class="container-fluid" id="app">
<form @submit="search">
<div class="input-group mb-3">
<div class="form-outline">
<input v-model="q" type="search" id="q" class="form-control" />
<label class="form-label" for="q">Nom de l'album ou code barre</label>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
</button>
</div>
</form>
<table class="table table-striped table-hover table-sm align-middle">
<thead>
<tr>
<th>Pochette</th>
<th>Titre</th>
<th>Pays</th>
<th>Année</th>
<th>Format</th>
<th>Genres</th>
<th>Styles</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items">
<td>
<img :src="item.thumb" :alt="item.title" style="max-width: 120px;"/>
</td>
<td>
<a :href="'/ajouter-un-album/' + item.id">{{ item.title }}</a>
</td>
<td>{{ item.year }}</td>
<td>{{ item.country }}</td>
<td>
<ul>
<li v-for="format in item.format">{{ format }}</li>
</ul>
</td>
<td>
<ul>
<li v-for="genre in item.genre">{{ genre }}</li>
</ul>
</td>
<td>
<ul>
<li v-for="style in item.style">{{ style }}</li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
<script>
Vue.createApp({
data() {
return {
q: '',
items: [],
}
},
methods: {
search(event) {
event.preventDefault();
axios.get(`/api/v1/search?q=${this.q}`)
.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 => {
console.log('err:', err);
});
}
}
}).mount('#app')
</script>

30
views/pages/connexion.ejs Normal file
View file

@ -0,0 +1,30 @@
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-5 col-md-8">
<form class="bg-white rounded shadow-5-strong p-5" method="POST">
<div class="text-center">
<img class="mb-4" src="/img/logo.png" alt="DarKou">
</div>
<h4>Connexion</h4>
<div class="form-outline mb-4">
<input type="email" id="email" name="email" class="form-control" />
<label class="form-label" for="email">Adresse e-mail</label>
</div>
<div class="form-outline mb-4">
<input type="password" id="password" name="password" class="form-control" />
<label class="form-label" for="password">Mot de passe</label>
</div>
<div class="row mb-4">
<div class="col text-end">
<p>Pas encore inscrit ? <a href="/inscription">Inscrivez-vous</a></p>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Connexion</button>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,35 @@
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-5 col-md-8">
<form class="bg-white rounded shadow-5-strong p-5" method="POST">
<div class="text-center">
<img class="mb-4" src="/img/logo.png" alt="DarKou">
</div>
<h4>Inscription</h4>
<div class="form-outline mb-4">
<input type="text" id="username" name="username" class="form-control" />
<label class="form-label" for="username">Nom d'utilisateur</label>
</div>
<div class="form-outline mb-4">
<input type="email" id="email" name="email" class="form-control" />
<label class="form-label" for="email">Adresse e-mail</label>
</div>
<div class="form-outline mb-4">
<input type="password" id="password" name="password" class="form-control" />
<label class="form-label" for="password">Mot de passe</label>
</div>
<div class="row mb-4">
<div class="col text-end">
<p>Déjà inscrit ? <a href="/connexion">Connectez-vous</a></p>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Inscription</button>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,2 @@
<script type="text/javascript" src="/libs/mdb-ui-kit/js/mdb.min.js"></script>

39
views/partials/head.ejs Normal file
View file

@ -0,0 +1,39 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><% if (page.title) { %><%= page.title %> <% } else { %> DarKou - Ma CDThèque <% } %></title>
<link rel="icon" type="image/png" href="/favicon.png" />
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
rel="stylesheet"
/>
<link
href="/libs/mdb-ui-kit/css/mdb.min.css"
rel="stylesheet"
/>
<link href="/css/main.css" rel="stylesheet" />
<script type="text/javascript" src="/libs/jquery/jquery.min.js"></script>
<script src="/libs/axios/axios.min.js"></script>
<script src="/libs/vue/vue.global.prod.js"></script>
<script>
function showToastr(message) {
var x = document.getElementById("toastr");
if ( message ) {
x.innerHTML = message;
}
x.className = "show";
setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000);
};
</script>
</head>

49
views/partials/header.ejs Normal file
View file

@ -0,0 +1,49 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<button
class="navbar-toggler"
type="button"
data-mdb-toggle="collapse"
data-mdb-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<a class="navbar-brand mt-2 mt-lg-0" href="/">
Ma CDThèque
</a>
</div>
<% if ( user ) { %>
<div class="d-flex align-items-center">
<a class="text-reset me-3" href="/ajouter-un-album">
Ajouter un album
</a>
<div class="dropdown">
<a
class="dropdown-toggle d-flex align-items-center hidden-arrow"
href="#"
id="navbarDropdownMenuAvatar"
role="button"
data-mdb-toggle="dropdown"
aria-expanded="false"
>
Mon compte
</a>
<ul
class="dropdown-menu dropdown-menu-end"
aria-labelledby="navbarDropdownMenuAvatar"
>
<li><a class="dropdown-item" href="/ma-collection">Ma collection</a></li>
<li><hr class="dropdown-divider" /></li>
<li><a class="dropdown-item" href="/se-deconnecter">Déconnexion</a></li>
</ul>
</div>
</div>
<% } %>
</div>
</nav>