add all project files

This commit is contained in:
Julien Aldon
2026-01-08 10:57:31 +01:00
parent 2873f6fd1b
commit 9766e18fd5
40 changed files with 22638 additions and 0 deletions

17
front/Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM node:19.1-alpine AS build
WORKDIR /app
COPY front/package.json front/package-lock.json /app/
RUN npm install
COPY front/ .
RUN npm run build
FROM nginx:latest
COPY --from=build /app/dist /srv/www/bookshelf
RUN rm /etc/nginx/conf.d/default.conf
COPY --from=build /app/nginx/default.conf /etc/nginx/conf.d/default.conf

24
front/README.md Normal file
View File

@@ -0,0 +1,24 @@
# front
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
front/babel.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

1
front/dist/css/app.f7195e87.css vendored Normal file
View File

@@ -0,0 +1 @@
.red{background-color:#de1656;color:#fff}.red:hover{background-color:#961656}.green{background-color:#16de56}.green:hover{background-color:#169656}header{width:60.5vw}.header,header{display:flex}body{margin:0}.button{border:none;padding:.2rem;margin:.3rem}.view{display:flex;flex-direction:column;align-items:center;margin-top:5rem}.router{flex:1 1 auto;background-color:#de1656;border:none;color:#fff;text-align:center;text-decoration:none;display:inline-block;font-size:16px;transition:background-color .5s ease-in}.router:focus,.router:hover{background-color:#961656}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50}nav a{padding:1rem;margin:0}table{table-layout:fixed}.button-panel{display:flex;flex-flow:row}tr:nth-child(odd){background-color:hsla(0,0%,4%,.1)}tr{text-align:left}tr:hover{background-color:hsla(0,0%,4%,.1)}h1{flex:1 1 auto;text-align:left}td{overflow:hidden;text-overflow:ellipsis;word-wrap:break-word}.AddButton{border:none;margin-right:.2rem;justify-content:flex-end;align-self:center}.AddButton,.edit{height:2rem;width:2rem}.AddForm{display:flex;flex-direction:column;overflow:hidden;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-name:lineInsertedIn;animation-name:lineInsertedIn;transition:height .3s}.AddForm input{margin:1rem}.AddForm .button{align-self:center;align-self:flex-end}.AddForm label{align-self:flex-start}th[data-v-aa4cb3dc]{text-align:left}.elipsis[data-v-6b7294ba]{font-size:3rem}.page[data-v-6b7294ba]{border:none;padding:1rem;margin:.2rem}footer[data-v-6b7294ba]{display:flex;padding:.2rem;align-items:center}.searchBox[data-v-559755d8]{display:flex}.search[data-v-559755d8]{display:flex;min-width:95%;margin-top:1rem;margin-bottom:1rem}.searchButton[data-v-559755d8]{color:hsla(0,0%,4%,.8);margin-left:-1.1rem;width:1rem;height:1rem;align-self:center;align-items:center;font-size:.6rem;border:none}select[data-v-1faa4947]{max-width:14.5rem}.dropdown[data-v-1faa4947]{background-color:transparent}i[data-v-1faa4947]:hover{color:#de1656}.dropdown-content[data-v-1faa4947]{position:absolute;background-color:#f8f8f8;min-width:14em;max-width:15rem;max-height:15rem;margin-top:.5rem;border:1px solid #de1656;box-shadow:0 -8px 34px 0 rgba(0,0,0,.05);overflow:auto;z-index:1}.dropdown-input[data-v-1faa4947]{width:14em;margin-right:1em}.dropdown-item[data-v-1faa4947]{color:#000;line-height:1em;text-decoration:none;padding:.5em;display:block;cursor:pointer}main[data-v-68f1643b]{max-width:60.5vw}td input[data-v-68f1643b]{width:14.5rem}@-webkit-keyframes lineInsertedIn-68f1643b{0%{height:0}to{height:23rem}}@keyframes lineInsertedIn-68f1643b{0%{height:0}to{height:23rem}}a[data-v-f3fce040]{text-decoration:none}h2[data-v-f3fce040],p[data-v-f3fce040]{text-align:left}a[data-v-31c3b6b4]{text-decoration:none}h2[data-v-31c3b6b4],p[data-v-31c3b6b4]{text-align:left}main[data-v-590e11e2]{width:80vw}table[data-v-590e11e2]{margin-left:auto;margin-right:auto}.elipsis[data-v-590e11e2]{font-size:3rem}.page[data-v-590e11e2]{border:none;padding:1rem;margin:.2rem}footer[data-v-590e11e2]{display:flex;padding:.2rem;align-items:center}button[data-v-3cf27bac]{margin-top:.5rem}label[data-v-3cf27bac]{margin:.2rem;text-align:left}input[data-v-3cf27bac]{border:none}input[data-v-3cf27bac],input[data-v-3cf27bac]:hover{background-color:rgba(0,0,0,.1)}main[data-v-3cf27bac]{width:20rem;align-self:center;align-content:center}div[data-v-3cf27bac],main[data-v-3cf27bac]{display:flex;flex-direction:column}

BIN
front/dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
front/dist/index.html vendored Normal file
View File

@@ -0,0 +1 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><script src="https://kit.fontawesome.com/08db035529.js" crossorigin="anonymous"></script><title>Aldon's Home</title><script defer="defer" src="/js/chunk-vendors.6bc46727.js"></script><script defer="defer" src="/js/app.10597b7c.js"></script><link href="/css/app.f7195e87.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but Aldon's Home doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

2
front/dist/js/app.10597b7c.js vendored Normal file

File diff suppressed because one or more lines are too long

1
front/dist/js/app.10597b7c.js.map vendored Normal file

File diff suppressed because one or more lines are too long

14
front/dist/js/chunk-vendors.6bc46727.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

19
front/jsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

26
front/nginx/default.conf Normal file
View File

@@ -0,0 +1,26 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost;
root /srv/www/bookshelf;
index index.html;
location / {
try_files $uri /index.html;
}
location /api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://backend;
}
}
upstream backend {
server back:8000;
}

19672
front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
front/package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "front",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"vue": "^3.2.13",
"vue-router": "^4.0.14",
"vuex": "^4.0.2"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

BIN
front/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

17
front/public/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script src="https://kit.fontawesome.com/08db035529.js" crossorigin="anonymous"></script>
<title>Aldon's Home</title>
</head>
<body>
<noscript>
<strong>We're sorry but Aldon's Home doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
</body>
</html>

192
front/src/App.vue Normal file
View File

@@ -0,0 +1,192 @@
<template>
<div>
<nav class="header">
<router-link class="router" to="/">Home</router-link>
<router-link class="router" to="/about">About</router-link>
<router-link class="router" to="/films">Film library</router-link>
<router-link class="router" to="/books">Book library</router-link>
<router-link v-if="!$store.state.token" class="router" to="/login">Login</router-link>
<a class="router" v-else @click="logout">Log out</a>
</nav>
<router-view class="view"/>
</div>
</template>
<script>
import { routerLink, routerView} from 'vue-router';
export default {
name: 'App',
components: {
routerLink,
routerView
},
beforeMount() {
let token = localStorage.getItem('token')
if (token) {
this.$store.state.token = localStorage.getItem('token')
}
else {
this.$store.state.token = ""
this.$router.push('/login')
}
this.$store.dispatch('getBooks', {page: 0, search: "", order: ""})
this.$store.dispatch('getFilms', {page: 0, search: "", order: ""})
},
methods: {
logout() {
this.$store.dispatch('logout')
this.$router.push('/login')
}
}
}
</script>
<style>
.red {
background-color: rgba(222, 22, 86);
color: #fff;
}
.red:hover {
background-color: rgba(150, 22, 86);
}
.green {
background-color: rgba(22, 222, 86);
}
.green:hover {
background-color: rgba(22, 150, 86);
}
header {
width: 60.5vw;
display: flex;
}
.header {
display: flex;
/* width: 100%; */
}
body {
margin: 0px;
}
.button {
border: none;
padding: 0.2rem;
margin: 0.3rem;
}
.view {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 5rem;
}
.router {
flex: 1 1 auto;
background-color: rgba(222, 22, 86);
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
transition: background-color 0.5s ease-in;
}
.router:focus {
background-color: rgba(150, 22, 86);
}
.router:hover {
background-color: rgba(150, 22, 86);
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav a {
padding: 1rem;
margin: 0px;
}
table {
table-layout: fixed;
}
.button-panel {
display: flex;
flex-flow: row;
}
tr:nth-child(2n+1) {
background-color: rgba(11, 11, 11, 0.1);
}
tr {
text-align: left;
}
tr:hover {
background-color: rgba(11, 11, 11, 0.1);
}
h1 {
flex: 1 1 auto;
text-align: left;
}
td {
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
}
.AddButton {
border: none;
height: 2rem;
width: 2rem;
margin-right: 0.2rem;
justify-content: flex-end;
align-self: center;
}
.edit {
width: 2rem;
height: 2rem;
}
.AddForm {
display: flex;
flex-direction: column;
overflow: hidden;
animation-duration: 0.3s;
animation-name: lineInsertedIn;
transition: height 0.3s;
}
.AddForm input {
margin: 1rem;
}
.AddForm .button {
align-self: center;
/* max-width: 5rem;
min-width: 5rem; */
align-self: flex-end
}
.AddForm label {
align-self: flex-start;
}
</style>

BIN
front/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,109 @@
<template>
<div>
<input
class="dropdown-input"
@focus="showOption"
@blur="hideOption"
v-model="inputVal" />
<button
class="dropdown button"
@blur="hideOption"
@click="toggleOption" >
<i class="fa-solid fa-angle-down" ></i>
</button>
<div class="dropdown-content" v-show="showOptions">
<div
class="dropdown-item"
v-for="val in vals"
@mousedown="selectValue(val)"
:key="val">{{ val }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
field: String,
values: Array,
inputValue: String,
},
data() {
return {
inputVal: "",
showOptions: false,
selected: "",
vals: [],
}
},
mounted() {
this.inputVal = this.inputValue;
},
watch: {
inputVal: function() {
if (!this.inputVal)
return
this.vals = this.values.filter(word => word.toLowerCase().includes(this.inputVal.toLowerCase(), 0))
}
},
methods: {
selectValue(value) {
this.selected = value;
this.inputVal = value;
this.$emit('selected', this.selected);
},
toggleOption() {
this.showOptions = !this.showOptions
},
showOption() {
this.showOptions = true;
},
hideOption() {
this.showOptions = false;
this.selected = this.inputVal;
this.$emit('selected', this.selected);
}
}
}
</script>
<style scoped>
select {
max-width: 14.5rem;
}
.dropdown {
background-color: rgba(0, 0, 0, 0);
}
i:hover {
color: rgba(222, 22, 86, 1);
}
.dropdown-content {
position: absolute;
background-color: #f8f8F8;
min-width: 14em;
max-width: 15rem;
max-height: 15rem;
margin-top: .5rem;
border: 1px solid rgba(222, 22, 86, 1);
/* border-top-color: rgba(0,0,0,0); */
box-shadow: 0px -8px 34px 0px rgba(0,0,0,0.05);
overflow: auto;
z-index: 1;
}
.dropdown-input {
width: 14em;
margin-right: 1em;
}
.dropdown-item {
color: black;
line-height: 1em;
text-decoration: none;
padding: 0.5em;
display: block;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<div>
<footer v-if="$store.getters.getBookPageNb <= 5">
<button class="page" v-for="index in $store.getters.getBookCurrentPage + 1" :key="index">{{ index }}</button>
</footer>
<footer v-else>
<button @click="$store.dispatch('fetchPageBooks', {page: 0, nooption: false})" class="page"> {{ '<<' }} </button>
<button
class="page"
:class="{ 'red': $store.getters.getBookCurrentPage - Math.max(0, Math.min($store.getters.getBookCurrentPage - 2, $store.getters.getBookPageNb - 5)) === index}"
v-for="(n, index) in (Math.max(5, Math.min($store.getters.getBookCurrentPage + 2, $store.getters.getBookPageNb)) - Math.max(0, Math.min($store.getters.getBookCurrentPage - 2, $store.getters.getBookPageNb - 5)))"
:key="index"
@click="$store.dispatch('fetchPageBooks', {page: Math.max(0, Math.min($store.getters.getBookCurrentPage - 2, $store.getters.getBookPageNb - 5)) + index, nooption: false})">
{{ Math.max(0, Math.min($store.getters.getBookCurrentPage - 2, $store.getters.getBookPageNb - 5)) + index }}
</button>
<p v-if="$store.getters.getBookCurrentPage !== $store.getters.getBookPageNb" class="elipsis">...</p>
<button v-if="$store.getters.getBookCurrentPage !== $store.getters.getBookPageNb" class="page" @click="$store.dispatch('fetchPageBooks', {page: $store.getters.getBookPageNb, nooption: false})">{{ $store.getters.getBookPageNb }}</button>
<button @click="$store.dispatch('fetchPageBooks', {page: $store.getters.getBookPageNb, nooption: false})" class="page"> >> </button>
</footer>
</div>
</template>
<style scoped>
.elipsis {
font-size: 3rem
}
.page {
border: none;
padding: 1rem;
margin: 0.2rem;
}
footer {
display: flex;
padding: 0.2rem;
align-items: center;
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="searchBox">
<input class="search" type="text" v-model="search"/>
<button class="searchButton" @click="clearSearch" >x</button>
</div>
</template>
<script>
export default {
props: {
page: String,
},
watch: {
search: function() {
this.$emit('changeSearch', this.search);
this.$store.dispatch('fetchPage'+this.page, {page: 0, nooption: false, search: this.search, order: this.order})
}
},
methods: {
clearSearch() {
this.$store.dispatch('fetchPage'+this.page, {page: 0, nooption: true})
this.search = ""
},
},
data() {
return {
search: "",
}
}
}
</script>
<style scoped>
.searchBox {
display: flex;
}
.search {
display: flex;
min-width: 95%;
margin-top: 1rem;
margin-bottom: 1rem;
}
.searchButton {
color: rgba(11, 11, 11, 0.8);
margin-left: -1.1rem;
width: 1rem;
height: 1rem;
align-self: center;
align-items: center;
font-size: 0.6rem;
border: none;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<table>
<tr>
<th v-bind:style="{ 'min-width': this.width }" v-for="head in headings" :key="head">{{ head }}</th>
</tr>
<slot></slot>
</table>
</template>
<script>
export default {
name: "TableView",
props: {
headings: Array,
width: String
},
};
</script>
<style scoped>
th {
text-align: left;
}
</style>

6
front/src/config.js Normal file
View File

@@ -0,0 +1,6 @@
const ROOT_FQDN = 'https://bookshelf.aldon.fr/api';
// const ROOT_FQDN = 'http://localhost/api';
export {
ROOT_FQDN
};

19
front/src/main.js Normal file
View File

@@ -0,0 +1,19 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')
// new Vue({
// router,
// store,
// el: '#app',
// components: { App },
// render: h => h(App)
// })

28
front/src/router/index.js Normal file
View File

@@ -0,0 +1,28 @@
import { createRouter, createWebHistory } from 'vue-router';
import TheBooks from '../views/TheBooks.vue';
import TheAbout from '../views/TheAbout.vue';
import TheHome from '../views/TheHome.vue';
import TheFilms from '../views/TheFilms.vue';
import TheLogin from '../views/TheLogin.vue';
import store from '../store';
const routes = [
{ name: 'TheHome', path: '/', component: TheHome },
{ name: 'TheFilms', path: '/films', component: TheFilms },
{ name: 'TheBooks', path: '/books', component: TheBooks },
{ name: 'TheAbout', path: '/about', component: TheAbout },
{ name: 'Login', path: '/login', component: TheLogin },
]
const router = new createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
router.beforeEach(async (to) => {
if (!store.state.token && to.name !== 'Login') {
return { name: 'Login', query: {redirect: to.path} }
}
})
export default router;

389
front/src/store/index.js Normal file
View File

@@ -0,0 +1,389 @@
import { createStore } from 'vuex';
import { ROOT_FQDN } from '@/config';
const store = createStore({
state() {
return {
books: [
],
films: [
],
token: "",
bookPageNb: 0,
bookCurrentPage: 0,
filmPageNb: 0,
filmCurrentPage: 0,
}
},
mutations: {
setBooks(state, books) {
state.books = books.result
},
setFilms(state, films) {
state.films = films
},
logout(state) {
state.token = ""
},
addToken(state, token) {
state.token = token
},
removeBook(state, id) {
let index = state.books.findIndex(book => book.biblio_Index === id)
state.books.splice(index, 1)
},
editBook(state, payload) {
let index = state.books.findIndex(book => book.biblio_Index === payload.id)
state.books.splice(index, 1, payload.newBook)
},
addBook(state, book) {
state.books.push(book);
},
removeFilm(state, id) {
let index = state.films.findIndex(film => film.Number === id)
state.films.splice(index, 1)
},
editFilm(state, payload) {
let index = state.films.findIndex(film => film.Number === payload.id)
state.films.splice(index, 1, payload.newfilm)
},
addFilm(state, film) {
state.films.push(film);
},
setBookPageNb(state, nb) {
state.bookPageNb = nb
},
setBookCurrentPage(state, page) {
state.bookCurrentPage = page
},
setFilmCurrentPage(state, page) {
state.filmCurrentPage = page
}
},
getters: {
getBooks(state) {
return state.books;
},
getBookCurrentPage(state) {
return state.bookCurrentPage
},
getBookPageNb(state) {
return state.bookPageNb
},
getFilms(state) {
return state.films;
},
getFilmCurrentPage(state) {
return state.filmCurrentPage
},
getFilmPageNb(state) {
return state.filmPageNb
},
},
actions: {
logout(ctx) {
ctx.commit('logout')
localStorage.setItem('token', '')
},
async login(ctx, info) {
let username = info.username
let password = info.password
const data = new FormData()
data.append('username', username)
data.append('password', password)
return await fetch(ROOT_FQDN + '/token', {
method: 'POST',
body: data
}).then((response) => {
if (response.ok) {
return response.json()
} else {
return null
}
}).then(res => {
if (res !== null) {
ctx.commit('addToken', res.access_token)
localStorage.setItem('token', res.access_token)
ctx.dispatch('getBooks', {page: 0, search: "", order:""})
ctx.dispatch('getFilms', {page: 0, search: "", order:""})
}
})
},
async removeBook({dispatch, commit, state}, id) {
await fetch(ROOT_FQDN+'/book/'+id, {
method: 'DELETE',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
},
}).then((response) => {
if (response.ok) {
commit('setBookPageNb', response.headers.get('x-nbpage'))
return response.json()
} else
return null
}).then(res => {
if (res === null)
{
dispatch('logout')
return null
}
return res
})
commit('removeBook', id)
},
async editBook({dispatch, commit, state}, payload) {
let data = {
title: payload.newBook.title,
author: payload.newBook.author,
type: payload.newBook.type,
editor: payload.newBook.editor
}
await fetch(ROOT_FQDN+'/book/'+payload.id, {
method: 'PUT',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}).then(res => {
if (res.ok) {
return res.json()
} else
return null
}).then((res) => {
if (res === null)
dispatch('logout')
dispatch('getBooks', {page: 0, search: payload.search, order:""})
})
commit('editBook', payload)
},
async addBook({dispatch, commit, state}, book) {
let data = {
title: book.Titre,
author: book.Auteur,
type: book.Type,
editor: book.Editeur
}
await fetch(ROOT_FQDN+'/books', {
method: 'POST',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}).then((response) => {
commit('setBookPageNb', response.headers.get('x-nbpage'))
if (response.ok)
return response.json()
else
return null
}).then(res => {
if (res === null)
{
dispatch('logout')
return null
}
return res
})
commit('addBook', book)
},
async getBooks({dispatch, commit, state}, payload) {
let test = payload.page ? "?page=" + payload.page : ""
if (payload.page)
test += '&'
else
test += '?'
test += payload.search ? "search="+payload.search : ""
await fetch(ROOT_FQDN+'/books'+test, {
method: 'GET',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
}
}).then((response) => {
commit('setBookPageNb', response.headers.get('x-nbpage'))
if (response.ok)
return response.json()
else
return null
}).then(res => {
if (res === null)
{
dispatch('logout')
return null
}
commit('setBooks', res)
return res
})
},
async removeFilm({dispatch, commit, state}, id) {
await fetch(ROOT_FQDN+'/film/'+id, {
method: 'DELETE',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
},
}).then((response) => {
if (response.ok) {
commit('setFilmPageNb', response.headers.get('x-nbpage'))
return response.json()
} else
return null
}).then(res => {
if (res === null)
{
dispatch('logout')
return null
}
return res
})
commit('removeFilm', id)
},
async editFilm({dispatch, commit, state}, payload) {
let data = {
title: payload.newfilm.Title,
actors: payload.newfilm.Actors,
director: payload.newfilm.Director,
type: payload.newfilm.Type,
producer: payload.newfilm.Producer,
length: payload.newfilm.Length
}
await fetch(ROOT_FQDN+'/film/'+payload.id, {
method: 'PUT',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}).then(res => {
if (res.ok) {
return res.json()
} else
return null
}).then((res) => {
if (res === null)
dispatch('logout')
dispatch('getFilms', {page: 0, search:"", order:""})
})
commit('editFilm', payload)
},
async addFilm({dispatch, commit, state}, film) {
let data = {
title: film.Title,
actors: film.Actors,
director: film.Director,
type: film.Type,
producer: film.Producer,
length: film.Length
}
await fetch(ROOT_FQDN+'/films', {
method: 'POST',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}).then((response) => {
commit('setFilmPageNb', response.headers.get('x-nbpage'))
if (response.ok)
return response.json()
else
return null
}).then(res => {
if (res === null)
{
dispatch('logout')
return null
}
return res
})
commit('addFilm', film)
},
async getFilms({dispatch, commit, state}, payload) {
let test = payload.page ? "?page=" + payload.page : ""
if (payload.page)
test += '&'
else
test += '?'
test += payload.search ? "search="+payload.search : ""
await fetch(ROOT_FQDN+'/films'+test, {
method: 'GET',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
}
}).then((response) => {
if (response.ok)
return response.json()
else
return null
}).then(res => {
commit('setFilms', res)
if (res === null) {
dispatch('logout');
return null
}
return res
})
},
async fetchPageBooks({dispatch, commit}, payload) {
commit('setBookCurrentPage', payload.page)
if (payload.nooption) {
dispatch('getBooks', {page: payload.page, search: "", order: ""})
} else {
dispatch('getBooks', {page: payload.page, search: payload.search, order: payload.sort})
}
},
async fetchPageFilms({dispatch, commit}, payload) {
commit('setFilmCurrentPage', payload.page)
if (payload.nooption) {
dispatch('getFilms', {page: payload.page, search: "", order: ""})
} else {
dispatch('getFilms', {page: payload.page, search: payload.search, order: payload.sort})
}
},
async getBookField({dispatch, state}, field) {
return await fetch(ROOT_FQDN+'/books/' + field, {
method: 'GET',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
}
}).then((response) => {
if (response.ok)
return response.json()
else
return null
}).then(res => {
if (res === null) {
dispatch('logout');
return null
}
return res
})
},
async getFilmField({dispatch, state}, field) {
return await fetch(ROOT_FQDN+'/films/' + field, {
method: 'GET',
headers: {
"Authorization": 'Bearer ' + state.token,
"Content-Type": "application/json"
}
}).then((response) => {
if (response.ok)
return response.json()
else
return null
}).then(res => {
if (res === null) {
dispatch('logout');
return null
}
return res
})
}
}
})
export default store;

View File

@@ -0,0 +1,40 @@
<template>
<div class="about">
<header>
<h1>About</h1>
</header>
<main>
<h2>Home Library</h2>
<p>
</p>
<p>
Made with <a href="https://vuejs.org/"><b>VueJS</b></a> and <a href="https://fastapi.tiangolo.com/"><b>FastAPI</b></a> by <a href="https://julien.aldon.fr/"><b><em>Julien Aldon</em></b></a>
</p>
</main>
<footer>
</footer>
</div>
</template>
<script>
export default {
data() {
return {
}
}
}
</script>
<style scoped>
a {
text-decoration: none;
}
p {
text-align: left;
}
h2 {
text-align: left;
}
</style>

View File

@@ -0,0 +1,206 @@
<template>
<div>
<header>
<h1>Books</h1>
<button v-if="!addMode" class="AddButton" v-show="!addMode" @click="toggleEditMode"><i class="fas fa-plus"></i></button>
<button v-else class="AddButton red" @click="addMode=!addMode">X</button>
</header>
<main>
<form ref="form" class="AddForm collapsed" v-on:submit.prevent="addElem" v-if="addMode">
<label for="author">Author</label>
<input id="author" v-model="formAdd.author" placeholder="Author"/>
<label for="title">Title</label>
<input id="title" v-model="formAdd.title" placeholder="Title"/>
<label for="editor">Editor</label>
<input id="editor" v-model="formAdd.editor" placeholder="Editor"/>
<label for="type">Type</label>
<input id="type" v-model="formAdd.type" placeholder="Type"/>
<button class="button edit">
<i class="fa-solid fa-arrow-right"></i>
</button>
</form>
<SearchHeader page="Books" @changeSearch="(s) => {editSearch(s)}"/>
<TableView ref="table" :headings="headings" width="15rem">
<tr v-for="book in this.$store.getters.getBooks" :key="book">
<td v-if="editRow === book.biblio_Index">
<DropDown
:values="this.bookFields.authors"
field="Auteur"
:inputValue="book.Auteur"
@selected="(n) => {formEdit.author = n}"
/>
</td>
<td v-else>{{ book.Auteur }}</td>
<td v-if="editRow === book.biblio_Index">
<DropDown
:values="this.bookFields.titles"
field="Titre"
:inputValue="book.Titre"
@selected="(n) => {formEdit.title = n}"
/>
</td>
<td v-else>{{ book.Titre }}</td>
<td v-if="editRow === book.biblio_Index">
<DropDown
:values="this.bookFields.editors"
field="Editeur"
:inputValue="book.Editeur"
@selected="(n) => {formEdit.editor = n}"
/>
</td>
<td v-else>{{ book.Editeur }}</td>
<td v-if="editRow === book.biblio_Index">
<DropDown
:values="this.bookFields.types"
field="Type"
:inputValue="book.Type"
@selected="(n) => {formEdit.type = n}"
/>
</td>
<td v-else>{{ book.Type }}</td>
<div class="button-panel">
<button class="button edit" @click="editElem(book.biblio_Index)">
<i v-if="editRow === book.biblio_Index" class="fa fa-check" aria-hidden="true"></i>
<i v-else class="fa fa-pencil-square-o" aria-hidden="true"></i>
</button>
<button class="button edit" @click="removeElem(book.biblio_Index)"><i class="fa fa-trash-o" aria-hidden="true"></i></button>
</div>
</tr>
</TableView>
</main>
<PaginationFooter/>
</div>
</template>
<script>
import TableView from '../components/TableView.vue';
import PaginationFooter from '../components/PaginationFooter.vue';
import SearchHeader from '../components/SearchHeader.vue';
import DropDown from '../components/DropDown.vue';
export default {
components: {
TableView,
PaginationFooter,
SearchHeader,
DropDown
},
data() {
return {
headings: ['Author', 'Title', 'Editor', 'Type'],
editMode: false,
editRow: undefined,
addMode: false,
formEdit: {},
formAdd: {},
search: "",
sort: "",
bookFields: {},
}
},
mounted() {
this.$store.dispatch('getBookField', 'Auteur').then((res) => {
if (res === null) {
this.$store.dispatch('logout');
return;
}
this.bookFields.authors = [...res];
})
this.$store.dispatch('getBookField', 'Titre').then((res) => {
if (res === null) {
this.$store.dispatch('logout');
return;
}
this.bookFields.titles = [...res];
})
this.$store.dispatch('getBookField', 'Editeur').then((res) => {
if (res === null) {
this.$store.dispatch('logout');
return;
}
this.bookFields.editors = [...res];
})
this.$store.dispatch('getBookField', 'Type').then((res) => {
if (res === null) {
this.$store.dispatch('logout');
return;
}
this.bookFields.types = [...res];
})
},
methods: {
removeElem(id) {
this.$store.dispatch('removeBook', id)
},
toggleEditMode() {
this.addMode =! this.addMode
},
editSearch(search) {
this.search = search;
},
addElem() {
if (!this.formAdd.title || !this.formAdd.author)
return
this.$store.dispatch('addBook', {
'Titre': this.formAdd.title,
'Auteur': this.formAdd.author,
'Editeur': this.formAdd.editor,
'Type': this.formAdd.type,
'biblio_Index': new Date().valueOf()
})
this.addMode = true
this.formAdd = {}
},
editElem(id) {
this.editMode =! this.editMode
if (this.editMode || this.editRow !== id) {
let currentBook = this.$store.getters.getBooks.filter(b => b.biblio_Index === id)
this.formEdit.author = currentBook[0].Auteur
this.formEdit.title = currentBook[0].Titre
this.formEdit.editor = currentBook[0].Editeur
this.formEdit.type = currentBook[0].Type
this.editRow = id
}
else {
this.editRow = undefined
let book = this.$store.getters.getBooks.filter(b => b.biblio_Index === id)[0]
let newObj = {author: '', title: '', type: '', editor: ''}
if (book) {
newObj.author = this.formEdit.author ? this.formEdit.author : book.Auteur
newObj.title = this.formEdit.title ? this.formEdit.title : book.Titre
newObj.type = this.formEdit.type ? this.formEdit.type : book.Type
newObj.editor = this.formEdit.editor ? this.formEdit.editor : book.Editeur
newObj.id = id
} else {
newObj.author = this.formEdit.author ? this.formEdit.author : ""
newObj.title = this.formEdit.title ? this.formEdit.title : ""
newObj.type = this.formEdit.type ? this.formEdit.type : ""
newObj.editor = this.formEdit.editor ? this.formEdit.editor : ""
newObj.id = id;
}
this.$store.dispatch('editBook', {id: id, newBook: newObj, search: this.search})
this.formEdit = {}
}
},
}
}
</script>
<style scoped>
main {
max-width: 60.5vw;
}
td input {
width: 14.5rem;
}
@keyframes lineInsertedIn {
0% {
height: 0rem;
}
100% {
height: 23rem;
}
}
</style>

View File

@@ -0,0 +1,243 @@
<template>
<div>
<header>
<h1>Films</h1>
<button v-if="!addMode" class="AddButton" v-show="!addMode" @click="toggleEditMode"><i class="fas fa-plus"></i></button>
<button v-else class="AddButton red" @click="addMode=!addMode">X</button>
</header>
<main>
<form ref="form" class="AddForm collapsed" v-on:submit.prevent="addElem" v-if="addMode">
<label for="Director">Director</label>
<input id="Director" v-model="formAdd.director" placeholder="director"/>
<label for="Title">Title</label>
<input id="Title" v-model="formAdd.title" placeholder="title"/>
<label for="Actors">Actors</label>
<input id="Actors" v-model="formAdd.actors" placeholder="Actors"/>
<label for="Length">Length</label>
<input id="Length" type="number" v-model="formAdd.length" placeholder="Lenght (in min)"/>
<label for="Producer">Producer</label>
<input id="Producer" v-model="formAdd.producer" placeholder="Producer"/>
<label for="Type">Type</label>
<input id="Type" v-model="formAdd.type" placeholder="type"/>
<button class="button edit">
<i class="fa-solid fa-arrow-right"></i>
</button>
</form>
<SearchHeader page="Films"/>
<TableView ref="table" :headings="headings" width="10rem">
<tr :key="film" v-for="film in this.$store.getters.getFilms">
<td v-if="editRow === film.Number">
<DropDown
:values="this.filmFields.directors"
field="Director"
:inputValue="film.Director"
@selected="(n) => {formEdit.director = n}"
/>
</td>
<td v-else>{{ film.Director }}</td>
<td v-if="editRow === film.Number">
<DropDown
:values="this.filmFields.titles"
field="Title"
:inputValue="film.Title"
@selected="(n) => {formEdit.title = n}"
/>
</td>
<td v-else>{{ film.Title }}</td>
<td v-if="editRow === film.Number">
<DropDown
:values="this.filmFields.actors"
field="Actors"
:inputValue="film.Actors"
@selected="(n) => {formEdit.actors = n}"
/>
</td>
<td v-else>{{ film.Actors }}</td>
<td v-if="editRow === film.Number">
<input :placeholder="film.Length" v-model="formEdit.length"/>
</td>
<td v-else>{{ film.Length }}</td>
<td v-if="editRow === film.Number">
<DropDown
:values="this.filmFields.producers"
field="Producer"
:inputValue="film.Producer"
@selected="(n) => {formEdit.producer = n}"
/>
</td>
<td v-else>{{ film.Producer }}</td>
<td v-if="editRow === film.Number">
<DropDown
:values="this.filmFields.types"
field="Type"
:inputValue="film.Type"
@selected="(n) => {formEdit.type = n}"
/>
</td>
<td v-else>{{ film.Type }}</td>
<div class="button-panel">
<button class="button edit" @click="editElem(film.Number)">
<i v-if="editRow === film.Number" class="fa fa-check" aria-hidden="true"></i>
<i v-else class="fa fa-pencil-square-o" aria-hidden="true"></i>
</button>
<button class="button edit" @click="removeElem(film.Number)"><i class="fa fa-trash-o" aria-hidden="true"></i></button>
</div>
</tr>
</TableView>
</main>
<footer v-if="$store.getters.getFilmPageNb <= 5">
<button class="page" v-for="index in $store.getters.getFilmCurrentPage + 1" :key="index">{{ index }}</button>
</footer>
<footer v-else>
<button @click="fetchPage(0)" class="page"> {{ '<<' }} </button>
<button
class="page"
:class="{ 'red': $store.getters.getFilmCurrentPage - Math.max(0, Math.min($store.getters.getFilmCurrentPage - 2, $store.getters.getFilmPageNb - 5)) === index}"
v-for="(n, index) in (Math.max(5, Math.min($store.getters.getFilmCurrentPage + 2, $store.getters.getFilmPageNb)) - Math.max(0, Math.min($store.getters.getFilmCurrentPage - 2, $store.getters.getFilmPageNb - 5)))"
:key="index"
@click="fetchPage(Math.max(0, Math.min($store.getters.getFilmCurrentPage - 2, $store.getters.getFilmPageNb - 5)) + index)">
{{ Math.max(0, Math.min($store.getters.getFilmCurrentPage - 2, $store.getters.getFilmPageNb - 5)) + index }}
</button>
<p v-if="$store.getters.getFilmCurrentPage !== $store.getters.getFilmPageNb" class="elipsis">...</p>
<button v-if="$store.getters.getFilmCurrentPage !== $store.getters.getFilmPageNb" class="page" @click="fetchPage($store.getters.getFilmPageNb)">{{ $store.getters.getFilmPageNb }}</button>
<button @click="fetchPage($store.getters.getFilmPageNb)" class="page"> >> </button>
</footer>
</div>
</template>
<script>
import TableView from '../components/TableView.vue';
import DropDown from '../components/DropDown.vue';
import SearchHeader from '../components/SearchHeader.vue';
export default {
components: {
TableView,
DropDown,
SearchHeader
},
data() {
return {
headings: ['Director', 'Title', 'Actors', 'Length', 'Producer', 'Type'],
editMode: false,
editRow: undefined,
addMode: false,
formEdit: {},
formAdd: {},
search: "",
sort: "",
filmFields: {},
}
},
mounted() {
this.$store.dispatch('getFilmField', 'Director').then((res) => {
this.filmFields.directors = [...res];
})
this.$store.dispatch('getFilmField', 'Title').then((res) => {
this.filmFields.titles = [...res];
})
this.$store.dispatch('getFilmField', 'Actors').then((res) => {
this.filmFields.actors = [...res];
})
this.$store.dispatch('getFilmField', 'Producer').then((res) => {
this.filmFields.producers = [...res];
})
this.$store.dispatch('getFilmField', 'Type').then((res) => {
this.filmFields.types = [...res];
})
},
methods: {
removeElem(id) {
this.$store.dispatch('removeFilm', id)
},
toggleEditMode() {
this.addMode =! this.addMode
},
addElem() {
if (!this.formAdd.title || !this.formAdd.director
|| !this.formAdd.producer || !this.formAdd.type
|| !this.formAdd.actors || !this.formAdd.length) {
//TODO: error handling
return
}
this.$store.dispatch('addFilm', {
'Title': this.formAdd.title,
'Director': this.formAdd.director,
'Producer': this.formAdd.producer,
'Type': this.formAdd.type,
'Actors': this.formAdd.actors,
'Length': this.formAdd.length,
'Number': new Date().valueOf()
})
this.addMode = true
this.formAdd = {}
},
editElem(id) {
this.editMode =! this.editMode
if (this.editMode || this.editRow !== id) {
this.editRow = id
}
else {
this.editRow = undefined;
let ele = this.$store.getters.getFilms.filter(test => test.Number === id)[0]
let newObj = {Director: '', Title: '', Producer: '', Type: '', Length: '', Actors: ''}
if (ele) {
newObj.Director = this.formEdit.director ? this.formEdit.director : ele.Director
newObj.Producer = this.formEdit.producer ? this.formEdit.producer : ele.Producer
newObj.Title = this.formEdit.title ? this.formEdit.title : ele.Title
newObj.Type = this.formEdit.type ? this.formEdit.type : ele.Type
newObj.Length = this.formEdit.length ? this.formEdit.length : ele.Length
newObj.Actors = this.formEdit.actors ? this.formEdit.actors : ele.Actors
newObj.Number = id
} else {
newObj.Director = this.formEdit.director ? this.formEdit.director : ""
newObj.Producer = this.formEdit.producer ? this.formEdit.producer : ""
newObj.Title = this.formEdit.title ? this.formEdit.title : ""
newObj.Type = this.formEdit.type ? this.formEdit.type : ""
newObj.Length = this.formEdit.length ? this.formEdit.length : ""
newObj.Actors = this.formEdit.actors ? this.formEdit.actors : ""
newObj.Number = id
}
this.$store.dispatch('editFilm', {id: id, newfilm: newObj})
this.formEdit = {}
}
},
fetchPage(page, nooption=false) {
this.$store.commit('setFilmCurrentPage', page)
if (nooption) {
this.$store.dispatch('getFilms', {page: page, search: "", order: ""})
} else {
this.$store.dispatch('getFilms', {page: page, search: this.search, order: this.sort})
}
},
}
}
</script>
<style scoped>
main {
width: 80vw;
}
table {
margin-left: auto;
margin-right: auto;
}
.elipsis {
font-size: 3rem
}
.page {
border: none;
padding: 1rem;
margin: 0.2rem;
}
footer {
display: flex;
padding: 0.2rem;
align-items: center;
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div>
<header>
<h1>Home</h1>
</header>
<main>
<h2>Welcome to the Library</h2>
<p>You can manage, edit, delete book references from the home library</p>
</main>
</div>
</template>
<script>
export default {
data() {
return {
}
}
}
</script>
<style scoped>
a {
text-decoration: none;
}
p {
text-align: left;
}
h2 {
text-align: left;
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div>
<header>
<h1>Login</h1>
</header>
<main @keydown.enter="login()">
<label for="username">Username</label>
<input id="username" type="text" v-model="username"/>
<label for="password">Password</label>
<input id="password" type="password" v-model="password"/>
<button @click="login">Login</button>
</main>
<footer>
</footer>
</div>
</template>
<script>
export default {
data() {
return {
username: "",
password: ""
}
},
methods: {
login() {
if (this.username !== "" && this.password !== "")
this.$store.dispatch('login', {'username': this.username, 'password': this.password}).then(() => {
this.username = ""
this.password = ""
this.$router.push(this.$route.query.redirect || '/books')
})
}
}
}
</script>
<style scoped>
button {
margin-top: 0.5rem
}
label {
margin: 0.2rem;
text-align: left;
}
input {
background-color: rgba(0.1, 0.1, 0.1, 0.1);
border: none
}
input:hover {
background-color: rgba(0.1, 0.1, 0.1, 0.1);
}
main {
width : 20rem;
align-self: center;
display: flex;
align-content: center;
flex-direction: column;
}
div {
flex-direction: column;
display: flex;
}
</style>

4
front/vue.config.js Normal file
View File

@@ -0,0 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})