Add prettier dependency to format frontend code

This commit is contained in:
2026-03-04 15:37:59 +01:00
parent 2d97aa85c4
commit af44849eda
31 changed files with 1166 additions and 1070 deletions

1
front/.prettierignore Normal file
View File

@@ -0,0 +1 @@
public

View File

@@ -1,10 +1,10 @@
<!DOCTYPE html> <!doctype html>
<html lang=""> <html lang="">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/bulma.min.css"> <link rel="stylesheet" href="/css/bulma.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Personal Library Manager</title> <title>Personal Library Manager</title>
</head> </head>
<body> <body>

View File

@@ -1,60 +1,69 @@
<script setup> <script setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive, computed } from 'vue'
import { postBook, extractFormErrorFromField } from './api.js' import { postBook, extractFormErrorFromField } from './api.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import CoverUpload from './CoverUpload.vue' import CoverUpload from './CoverUpload.vue'
const router = useRouter(); const router = useRouter()
const book = ref({ const book = ref({
title: "", title: '',
author: "", author: '',
coverId: null coverId: null,
}); })
const errors = ref(null) const errors = ref(null)
const titleError = computed(() => { const titleError = computed(() => {
return extractFormErrorFromField("Title", errors.value); return extractFormErrorFromField('Title', errors.value)
})
const authorError = computed(() => {
return extractFormErrorFromField('Author', errors.value)
})
function onSubmit(e) {
postBook(book).then((res) => {
if (res.ok) {
router.push('/')
return
} else {
res.json().then((json) => (errors.value = json))
}
}) })
const authorError = computed(() => { }
return extractFormErrorFromField("Author", errors.value);
})
function onSubmit(e) {
postBook(book)
.then((res) => {
if (res.ok) {
router.push('/');
return;
} else {
res.json().then((json) => (errors.value = json));
}
})
}
</script> </script>
<template> <template>
<form @submit.prevent="onSubmit"> <form @submit.prevent="onSubmit">
<div class="field"> <div class="field">
<label class="label">{{$t('addbook.title')}}</label> <label class="label">{{ $t('addbook.title') }}</label>
<div class="control"> <div class="control">
<input :class="'input ' + (titleError ? 'is-danger' : '')" type="text" maxlength="300" <input
required v-model="book.title" :placeholder="$t('addbook.title')"> :class="'input ' + (titleError ? 'is-danger' : '')"
type="text"
maxlength="300"
required
v-model="book.title"
:placeholder="$t('addbook.title')"
/>
</div> </div>
<p v-if="titleError" class="help is-danger">{{titleError}}</p> <p v-if="titleError" class="help is-danger">{{ titleError }}</p>
</div> </div>
<div class="field"> <div class="field">
<label class="label">{{$t('addbook.author')}}</label> <label class="label">{{ $t('addbook.author') }}</label>
<div class="control"> <div class="control">
<input :class="'input ' + (authorError ? 'is-danger' : '')" type="text" maxlength="100" <input
v-model="book.author" :placeholder="$t('addbook.author')"> :class="'input ' + (authorError ? 'is-danger' : '')"
type="text"
maxlength="100"
v-model="book.author"
:placeholder="$t('addbook.author')"
/>
</div> </div>
<p v-if="authorError" class="help is-danger">{{authorError}}</p> <p v-if="authorError" class="help is-danger">{{ authorError }}</p>
</div> </div>
<CoverUpload name="cover" @on-image-upload="(id) => book.coverId = id"/> <CoverUpload name="cover" @on-image-upload="(id) => (book.coverId = id)" />
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<button class="button is-link">{{$t('addbook.submit')}}</button> <button class="button is-link">{{ $t('addbook.submit') }}</button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -1,11 +1,11 @@
<script setup> <script setup>
import AppNavBar from './AppNavBar.vue' import AppNavBar from './AppNavBar.vue'
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
</script> </script>
<template> <template>
<header> <header>
<AppNavBar/> <AppNavBar />
</header> </header>
<main class="section"> <main class="section">
<RouterView /> <RouterView />

View File

@@ -1,85 +1,96 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter, RouterLink } from 'vue-router' import { useRouter, RouterLink } from 'vue-router'
import NavBarSearch from './NavBarSearch.vue' import NavBarSearch from './NavBarSearch.vue'
import BarcodeModal from './BarcodeModal.vue' import BarcodeModal from './BarcodeModal.vue'
import { useAuthStore } from './auth.store.js' import { useAuthStore } from './auth.store.js'
import { getAppInfo, postLogin } from './api.js' import { getAppInfo, postLogin } from './api.js'
import { onMounted } from 'vue' import { onMounted } from 'vue'
const authStore = useAuthStore(); const authStore = useAuthStore()
const router = useRouter(); const router = useRouter()
const isMenuActive = ref(false) const isMenuActive = ref(false)
const isSearchBarShown = ref(false) const isSearchBarShown = ref(false)
function logout() { function logout() {
authStore.logout(); authStore.logout()
router.push('/'); router.push('/')
}
const appInfo = ref(null)
const appInfoErr = ref(null)
async function logInIfDemoMode(demoMode, demoUsername) {
if (!demoMode) {
return
} }
const appInfo = ref(null); const demouser = ref({
const appInfoErr = ref(null); username: demoUsername,
password: '',
async function logInIfDemoMode(demoMode, demoUsername) {
if (!demoMode) {
return;
}
const demouser = ref({
username: demoUsername,
password: ""
});
const res = await postLogin(demouser)
const json = await res.json();
await useAuthStore().login({username: demouser.value.username, token: json["token"]})
}
onMounted(() => {
getAppInfo(appInfo, appInfoErr)
.then(() => logInIfDemoMode(appInfo.value.demoMode, appInfo.value.demoUsername));
}) })
const res = await postLogin(demouser)
const json = await res.json()
await useAuthStore().login({ username: demouser.value.username, token: json['token'] })
}
onMounted(() => {
getAppInfo(appInfo, appInfoErr).then(() =>
logInIfDemoMode(appInfo.value.demoMode, appInfo.value.demoUsername),
)
})
</script> </script>
<template> <template>
<nav class="navbar"> <nav class="navbar">
<div class="navbar-brand"> <div class="navbar-brand">
<RouterLink to="/" class="navbar-item" activeClass="is-active"> <RouterLink to="/" class="navbar-item" activeClass="is-active"> PLM </RouterLink>
PLM <div class="navbar-item is-hidden-desktop">
</RouterLink> <a
<div class="navbar-item is-hidden-desktop"> @click="isSearchBarShown = !isSearchBarShown"
<a @click="isSearchBarShown = !isSearchBarShown" class="button is-medium"
class="button is-medium" :class="isSearchBarShown ? 'is-active' : '' "> :class="isSearchBarShown ? 'is-active' : ''"
<span class="icon" :title="$t('navbar.search')"> >
<b-icon-search /> <span class="icon" :title="$t('navbar.search')">
</span> <b-icon-search />
</a> </span>
<BarcodeModal size-class="is-medium"/>
</div>
<a v-if="authStore.user" role="button" class="navbar-burger" aria-label="menu"
:class="isMenuActive ? 'is-active' : '' " :aria-expanded="isMenuActive"
@click="isMenuActive = !isMenuActive">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a> </a>
<BarcodeModal size-class="is-medium" />
</div>
<a
v-if="authStore.user"
role="button"
class="navbar-burger"
aria-label="menu"
:class="isMenuActive ? 'is-active' : ''"
:aria-expanded="isMenuActive"
@click="isMenuActive = !isMenuActive"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div> </div>
<div v-if="isSearchBarShown" class="is-hidden-desktop"> <div v-if="isSearchBarShown" class="is-hidden-desktop">
<NavBarSearch @search-done="isSearchBarShown = false" size-class="is-medium" is-mobile/> <NavBarSearch @search-done="isSearchBarShown = false" size-class="is-medium" is-mobile />
</div> </div>
<div id="navmenu" class="navbar-menu" :class="isMenuActive ? 'is-active' : '' "> <div id="navmenu" class="navbar-menu" :class="isMenuActive ? 'is-active' : ''">
<div class="navbar-start"> <div class="navbar-start">
<NavBarSearch size-class="" class="is-hidden-touch"/> <NavBarSearch size-class="" class="is-hidden-touch" />
<RouterLink v-if="authStore.user" to="/books" class="navbar-item" activeClass="is-active"> <RouterLink v-if="authStore.user" to="/books" class="navbar-item" activeClass="is-active">
{{ $t('navbar.mybooks')}} {{ $t('navbar.mybooks') }}
</RouterLink> </RouterLink>
<RouterLink v-if="authStore.user" to="/add" class="navbar-item" activeClass="is-active"> <RouterLink v-if="authStore.user" to="/add" class="navbar-item" activeClass="is-active">
{{ $t('navbar.addbook')}} {{ $t('navbar.addbook') }}
</RouterLink> </RouterLink>
<div v-if="authStore.user && appInfo && !appInfo.demoMode" class="navbar-item is-hidden-desktop"> <div
v-if="authStore.user && appInfo && !appInfo.demoMode"
class="navbar-item is-hidden-desktop"
>
<a @click="logout"> <a @click="logout">
{{ $t('navbar.logout')}} {{ $t('navbar.logout') }}
<span class="icon" :title="$t('navbar.logout')"> <span class="icon" :title="$t('navbar.logout')">
<b-icon-power /> <b-icon-power />
</span> </span>
@@ -88,27 +99,30 @@
</div> </div>
<div class="navbar-end is-hidden-touch"> <div class="navbar-end is-hidden-touch">
<div v-if="authStore.user" class="navbar-item"> <div v-if="authStore.user" class="navbar-item">
<div > <div>
{{ authStore.user.username }} {{ authStore.user.username }}
</div> </div>
<a v-if="appInfo && !appInfo.demoMode" @click="logout" class="button is-light"> <a v-if="appInfo && !appInfo.demoMode" @click="logout" class="button is-light">
{{ $t('navbar.logout')}} {{ $t('navbar.logout') }}
</a> </a>
</div> </div>
<div v-else class="navbar-item"> <div v-else class="navbar-item">
<div class="buttons"> <div class="buttons">
<RouterLink v-if="appInfo && !appInfo.registrationDisabled" to="/signup" class="button is-primary"> <RouterLink
<strong>{{ $t('navbar.signup')}}</strong> v-if="appInfo && !appInfo.registrationDisabled"
to="/signup"
class="button is-primary"
>
<strong>{{ $t('navbar.signup') }}</strong>
</RouterLink> </RouterLink>
<RouterLink to="/login" class="button is-light"> <RouterLink to="/login" class="button is-light">
{{ $t('navbar.login')}} {{ $t('navbar.login') }}
</RouterLink> </RouterLink>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</nav>
</nav>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,34 +1,32 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { getAuthor } from './api.js' import { getAuthor } from './api.js'
import { onBeforeRouteUpdate } from 'vue-router' import { onBeforeRouteUpdate } from 'vue-router'
import SearchBook from './SearchBook.vue' import SearchBook from './SearchBook.vue'
const props = defineProps({
id: String,
})
const props = defineProps({ let author = ref(null)
id: String let authorfetcherror = ref(null)
});
let author = ref(null); getAuthor(author, authorfetcherror, props.id)
let authorfetcherror = ref(null);
getAuthor(author, authorfetcherror, props.id); onBeforeRouteUpdate(async (to, from) => {
getAuthor(author, authorfetcherror, to.params.id)
onBeforeRouteUpdate(async (to, from) => { })
getAuthor(author, authorfetcherror, to.params.id);
})
</script> </script>
<template> <template>
<div v-if="authorfetcherror">{{$t('authorform.error', {err: authorfetcherror.message})}}</div> <div v-if="authorfetcherror">{{ $t('authorform.error', { err: authorfetcherror.message }) }}</div>
<div v-if="author"> <div v-if="author">
<h3 class="title">{{author.name}}</h3> <h3 class="title">{{ author.name }}</h3>
<p v-if="author.description">{{author.description}}</p> <p v-if="author.description">{{ author.description }}</p>
</div> </div>
<div class="mt-6"> <div class="mt-6">
<SearchBook :author-id="id"/> <SearchBook :author-id="id" />
</div> </div>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@@ -4,14 +4,14 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const props = defineProps({ const props = defineProps({
sizeClass: String sizeClass: String,
}); })
const open = ref(false); const open = ref(false)
const router = useRouter(); const router = useRouter()
function onBarcodeDecode(isbn) { function onBarcodeDecode(isbn) {
open.value = false; open.value = false
router.push('/search/' + isbn) router.push('/search/' + isbn)
} }
</script> </script>
@@ -29,38 +29,38 @@ function onBarcodeDecode(isbn) {
<div v-if="open"> <div v-if="open">
<div @click="open = false" class="modal-backdrop"></div> <div @click="open = false" class="modal-backdrop"></div>
<div class="modal has-background-dark"> <div class="modal has-background-dark">
<ScanBook @read-barcode="onBarcodeDecode"/> <ScanBook @read-barcode="onBarcodeDecode" />
</div> </div>
</div> </div>
</Teleport> </Teleport>
</template> </template>
<style scoped> <style scoped>
.modal-backdrop { .modal-backdrop {
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 999; z-index: 999;
} }
.modal { .modal {
height: 80%; height: 80%;
width: 80%; width: 80%;
top: 10%; top: 10%;
left: 10%; left: 10%;
box-shadow: 2px 2px 20px 1px; box-shadow: 2px 2px 20px 1px;
overflow-x: auto; overflow-x: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 15px; border-radius: 15px;
z-index: 1000; z-index: 1000;
} }
</style> </style>

View File

@@ -1,52 +1,55 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({ const props = defineProps({
icon: String, icon: String,
legend: String, legend: String,
isSet: Boolean, isSet: Boolean,
isReadonly: Boolean isReadonly: Boolean,
}); })
const hovered = ref(false) const hovered = ref(false)
const isOnMobile = ref(computeIsOnMobile()) const isOnMobile = ref(computeIsOnMobile())
const computedIcon = computed(() => props.icon + const computedIcon = computed(
(!props.isReadonly && ((hovered.value && !isOnMobile.value) || props.isSet) ? "Fill" : "")); () =>
props.icon +
(!props.isReadonly && ((hovered.value && !isOnMobile.value) || props.isSet) ? 'Fill' : ''),
)
function computeIsOnMobile() { function computeIsOnMobile() {
return window.innerWidth < 1024; return window.innerWidth < 1024
} }
function updateOnMobile() {
isOnMobile.value = computeIsOnMobile();
}
onMounted(() => {
window.addEventListener("resize", updateOnMobile);
});
onUnmounted(() => {
window.removeEventListener("resize", updateOnMobile);
});
function updateOnMobile() {
isOnMobile.value = computeIsOnMobile()
}
onMounted(() => {
window.addEventListener('resize', updateOnMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', updateOnMobile)
})
</script> </script>
<template> <template>
<div class="bigiconandlegend" <div
:class="props.isReadonly ? '' : 'showcanclick'" class="bigiconandlegend"
@mouseover="hovered = true" :class="props.isReadonly ? '' : 'showcanclick'"
@mouseout="hovered = false"> @mouseover="hovered = true"
@mouseout="hovered = false"
>
<span class="bigicon" :title="props.legend"> <span class="bigicon" :title="props.legend">
<component :is="computedIcon"></component> <component :is="computedIcon"></component>
</span> </span>
<div class="bigiconlegend">{{props.legend}}</div> <div class="bigiconlegend">{{ props.legend }}</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.bigiconandlegend { .bigiconandlegend {
border-radius:30px; border-radius: 30px;
margin:25px; margin: 25px;
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@@ -59,21 +62,21 @@
.bigicon { .bigicon {
display: flex; display: flex;
justify-content:center; justify-content: center;
align-items:center; align-items: center;
height: 90px; height: 90px;
width: 200px; width: 200px;
font-size: 78px; font-size: 78px;
padding-top:20px; padding-top: 20px;
} }
.bigiconlegend { .bigiconlegend {
display:flex; display: flex;
justify-content:center; justify-content: center;
align-items:center; align-items: center;
font-size: 34px; font-size: 34px;
width: 200px; width: 200px;
padding-bottom:30px; padding-bottom: 30px;
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
@@ -85,18 +88,18 @@
} }
.bigiconlegend { .bigiconlegend {
flex:1; flex: 1;
font-size: 16px; font-size: 16px;
width: 100%; width: 100%;
padding-bottom:0px; padding-bottom: 0px;
} }
.bigiconandlegend { .bigiconandlegend {
display: flex; display: flex;
justify-content:center; justify-content: center;
align-items:center; align-items: center;
flex-flow: column; flex-flow: column;
margin:0px; margin: 0px;
} }
} }
</style> </style>

View File

@@ -1,46 +1,46 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getImagePathOrDefault } from './api.js' import { getImagePathOrDefault } from './api.js'
import { VRating } from 'vuetify/components/VRating'; import { VRating } from 'vuetify/components/VRating'
const props = defineProps({ const props = defineProps({
id: Number, id: Number,
title: String, title: String,
author: String, author: String,
coverPath: String, coverPath: String,
rating: Number, rating: Number,
read: Boolean read: Boolean,
}); })
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath)); const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath))
const router = useRouter(); const router = useRouter()
function openBook() { function openBook() {
router.push(`/book/${props.id}`) router.push(`/book/${props.id}`)
} }
</script> </script>
<template> <template>
<div class="box container has-background-dark" > <div class="box container has-background-dark">
<div class="media" @click="openBook"> <div class="media" @click="openBook">
<div class="media-left"> <div class="media-left">
<figure class="image mb-3"> <figure class="image mb-3">
<img v-bind:src="imagePathOrDefault" v-bind:alt="title"> <img v-bind:src="imagePathOrDefault" v-bind:alt="title" />
</figure> </figure>
</div> </div>
<div class="media-content"> <div class="media-content">
<div class="content"> <div class="content">
<div class="is-size-5">{{title}}</div> <div class="is-size-5">{{ title }}</div>
<div class="is-size-5 is-italic">{{author}}</div> <div class="is-size-5 is-italic">{{ author }}</div>
<VRating v-if="rating > 0" <VRating
half-increments v-if="rating > 0"
readonly half-increments
:length="5" readonly
size="medium" :length="5"
:model-value="rating/2" size="medium"
active-color="bulma-body-color" :model-value="rating / 2"
/> active-color="bulma-body-color"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -49,14 +49,14 @@ function openBook() {
<style scoped> <style scoped>
img { img {
max-height:100px; max-height: 100px;
max-width:100px; max-width: 100px;
height:auto; height: auto;
width:auto; width: auto;
} }
.box { .box {
transition:ease-in-out 0.04s; transition: ease-in-out 0.04s;
margin-bottom: 15px; margin-bottom: 15px;
} }
@@ -64,5 +64,4 @@ img {
transform: scale(1.01); transform: scale(1.01);
transition: ease-in-out 0.02s; transition: ease-in-out 0.02s;
} }
</style> </style>

View File

@@ -1,8 +1,8 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import BigIcon from './BigIcon.vue'; import BigIcon from './BigIcon.vue'
const props = defineProps({ const props = defineProps({
icon: String, icon: String,
legend: String, legend: String,
startReadDate: String, startReadDate: String,
@@ -10,58 +10,63 @@
isExpanded: Boolean, isExpanded: Boolean,
isReadonly: Boolean, isReadonly: Boolean,
useEndDate: Boolean, useEndDate: Boolean,
lastWidget: Boolean lastWidget: Boolean,
}); })
defineEmits(['onIconClick', 'onStartDateChange', 'onEndDateChange']) defineEmits(['onIconClick', 'onStartDateChange', 'onEndDateChange'])
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10)
function computeParentClasses() { function computeParentClasses() {
let classNames = "bookdatewidget"; let classNames = 'bookdatewidget'
if (props.isExpanded) { if (props.isExpanded) {
classNames += " has-text-dark has-background-text"; classNames += ' has-text-dark has-background-text'
}
if (props.lastWidget) {
classNames += " border-radius-right-and-left";
} else {
classNames += " border-radius-right";
}
if (props.isReadonly) {
classNames += " widget-readonly"
}
return classNames;
} }
if (props.lastWidget) {
classNames += ' border-radius-right-and-left'
} else {
classNames += ' border-radius-right'
}
if (props.isReadonly) {
classNames += ' widget-readonly'
}
return classNames
}
</script> </script>
<template> <template>
<div :class="computeParentClasses()"> <div :class="computeParentClasses()">
<BigIcon :icon="props.icon" <BigIcon
:is-readonly="props.isReadonly" :icon="props.icon"
:legend="props.legend" :is-readonly="props.isReadonly"
:isSet="props.isExpanded" :legend="props.legend"
@click="props.isReadonly ? null : $emit('onIconClick') "/> :isSet="props.isExpanded"
@click="props.isReadonly ? null : $emit('onIconClick')"
/>
<div v-if="props.isExpanded" class="inputdate"> <div v-if="props.isExpanded" class="inputdate">
<div class="ontopofinput"> <div class="ontopofinput">
<label class="datelabel" for="startread"> <label class="datelabel" for="startread">
{{$t('bookdatewidget.started')}} {{ $t('bookdatewidget.started') }}
</label> </label>
<input class="datepicker has-background-dark has-text-light" <input
id="startread" class="datepicker has-background-dark has-text-light"
type="date" id="startread"
@change="(e) => $emit('onStartDateChange', e.target.value)" type="date"
:value="props.startReadDate" @change="(e) => $emit('onStartDateChange', e.target.value)"
:max="today"/> :value="props.startReadDate"
<div v-if="props.useEndDate"> :max="today"
/>
<div v-if="props.useEndDate">
<label class="datelabel" for="endread"> <label class="datelabel" for="endread">
{{$t('bookdatewidget.finished')}} {{ $t('bookdatewidget.finished') }}
</label> </label>
<input class="datepicker has-background-dark has-text-light" <input
id="endread" class="datepicker has-background-dark has-text-light"
type="date" id="endread"
@change="(e) => $emit('onEndDateChange', e.target.value)" type="date"
:value="props.endReadDate" @change="(e) => $emit('onEndDateChange', e.target.value)"
:max="today"/> :value="props.endReadDate"
:max="today"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -69,11 +74,10 @@
</template> </template>
<style scoped> <style scoped>
.inputdate { .inputdate {
display: flex; display: flex;
justify-content:center; justify-content: center;
align-items:center; align-items: center;
padding: 20px; padding: 20px;
} }
@@ -91,8 +95,8 @@
.datelabel { .datelabel {
display: flex; display: flex;
justify-content:center; justify-content: center;
align-items:center; align-items: center;
font-size: 26px; font-size: 26px;
border: none; border: none;
padding-bottom: 15px; padding-bottom: 15px;

View File

@@ -1,148 +1,159 @@
<script setup> <script setup>
import { ref, computed } from 'vue'
import {
getBook,
getImagePathOrDefault,
putReadBook,
putWantReadBook,
putRateBook,
putStartReadDate,
putStartReadDateUnset,
putEndReadDate,
putEndReadDateUnset,
putUnreadBook,
} from './api.js'
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
import { VRating } from 'vuetify/components/VRating'
import BigIcon from './BigIcon.vue'
import BookDateWidget from './BookDateWidget.vue'
import { ref, computed } from 'vue' const router = useRouter()
import { getBook, getImagePathOrDefault, putReadBook, putWantReadBook, putRateBook, const props = defineProps({
putStartReadDate, putStartReadDateUnset, putEndReadDate, putEndReadDateUnset, putUnreadBook } from './api.js' id: String,
import { useRouter, onBeforeRouteUpdate } from 'vue-router' })
import { VRating } from 'vuetify/components/VRating'; const today = new Date().toISOString().slice(0, 10)
import BigIcon from './BigIcon.vue';
import BookDateWidget from './BookDateWidget.vue';
let data = ref(null)
let error = ref(null)
getBook(data, error, props.id)
const imagePathOrDefault = computed(() => getImagePathOrDefault(data.value.coverPath))
const router = useRouter(); onBeforeRouteUpdate(async (to, from) => {
const props = defineProps({ getBook(data, error, to.params.id)
id: String })
});
const today = new Date().toISOString().slice(0, 10);
let data = ref(null); function onRatingUpdate(rating) {
let error = ref(null); data.value.rating = rating * 2
getBook(data, error, props.id); if (data.value.rating > 0) {
const imagePathOrDefault = computed(() => getImagePathOrDefault(data.value.coverPath)); data.value.read = true
data.value.wantread = false
onBeforeRouteUpdate(async (to, from) => {
getBook(data, error, to.params.id);
})
function onRatingUpdate(rating) {
data.value.rating = rating * 2;
if (data.value.rating > 0) {
data.value.read = true;
data.value.wantread = false;
}
putRateBook(props.id, {rating: data.value.rating});
} }
putRateBook(props.id, { rating: data.value.rating })
}
function onReadIconClick() { function onReadIconClick() {
data.value.read = !data.value.read; data.value.read = !data.value.read
if (data.value.read) { if (data.value.read) {
data.value.wantread = false; data.value.wantread = false
data.value.endReadDate = today; data.value.endReadDate = today
putEndReadDate(props.id, today); putEndReadDate(props.id, today)
} else { } else {
putUnreadBook(props.id); putUnreadBook(props.id)
}
} }
}
function onWantReadIconClick() { function onWantReadIconClick() {
data.value.wantread = !data.value.wantread; data.value.wantread = !data.value.wantread
putWantReadBook(props.id, {wantread: data.value.wantread}); putWantReadBook(props.id, { wantread: data.value.wantread })
}
function onStartReadIconClick() {
if (!data.value.startReadDate) {
data.value.startReadDate = today
putStartReadDate(props.id, data.value.startReadDate)
} else {
data.value.startReadDate = null
putStartReadDateUnset(props.id)
} }
}
function onStartReadIconClick() { function onStartReadDateChange(d) {
if (!data.value.startReadDate) { data.value.startReadDate = d
data.value.startReadDate = today; if (d != '') {
putStartReadDate(props.id, data.value.startReadDate); putStartReadDate(props.id, data.value.startReadDate)
} else { } else {
data.value.startReadDate = null; putStartReadDateUnset(props.id)
putStartReadDateUnset(props.id);
}
} }
}
function onStartReadDateChange(d) { function onEndReadDateChange(d) {
data.value.startReadDate = d; data.value.endReadDate = d
if (d != "") { if (d != '') {
putStartReadDate(props.id, data.value.startReadDate); putEndReadDate(props.id, data.value.endReadDate)
} else { } else {
putStartReadDateUnset(props.id); putEndReadDateUnset(props.id)
}
} }
}
function onEndReadDateChange(d) { function isStartReadExpanded() {
data.value.endReadDate = d; let isStartReadDateSet = data.value.startReadDate ? true : false
if (d != "") { let isReadUnset = !data.value.read ? true : false
putEndReadDate(props.id, data.value.endReadDate); return isStartReadDateSet && isReadUnset
} else { }
putEndReadDateUnset(props.id);
}
}
function isStartReadExpanded() {
let isStartReadDateSet = data.value.startReadDate ? true : false;
let isReadUnset = !data.value.read ? true : false;
return isStartReadDateSet && isReadUnset;
}
function goToAuthor() {
router.push("/author/" + data.value.authorId)
}
function goToAuthor() {
router.push('/author/' + data.value.authorId)
}
</script> </script>
<template> <template>
<div v-if="error">{{$t('bookform.error', {error: error.message})}}</div> <div v-if="error">{{ $t('bookform.error', { error: error.message }) }}</div>
<div v-if="data" class="columns"> <div v-if="data" class="columns">
<div class="column is-narrow left-panel"> <div class="column is-narrow left-panel">
<figure class="image"> <figure class="image">
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title"> <img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" />
</figure> </figure>
<VRating <VRating
half-increments half-increments
hover hover
:length="5" :length="5"
size="x-large" size="x-large"
density="compact" density="compact"
:model-value="data.rating/2" :model-value="data.rating / 2"
@update:modelValue="onRatingUpdate" @update:modelValue="onRatingUpdate"
active-color="bulma-body-color" active-color="bulma-body-color"
class="centered" class="centered"
/> />
</div> </div>
<div class="column"> <div class="column">
<h3 class="title">{{data.title}}</h3> <h3 class="title">{{ data.title }}</h3>
<h3 class="subtitle clickable" @click="goToAuthor">{{data.author}}</h3> <h3 class="subtitle clickable" @click="goToAuthor">{{ data.author }}</h3>
<p>{{data.summary}}</p> <p>{{ data.summary }}</p>
<div class="my-5" v-if="data.isbn">ISBN: {{data.isbn}}</div> <div class="my-5" v-if="data.isbn">ISBN: {{ data.isbn }}</div>
<div class="my-5" v-if="data.inventaireid">Inventaire ID: {{data.inventaireid}}</div> <div class="my-5" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div>
<div class="my-5" v-if="data.openlibraryid">OLID: {{data.openlibraryid}}</div> <div class="my-5" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div>
</div> </div>
<div class="column"> <div class="column">
<div class="iconscontainer" :class="data.read ? 'remove-border-bottom' : ''"> <div class="iconscontainer" :class="data.read ? 'remove-border-bottom' : ''">
<div class="bigiconcontainer"> <div class="bigiconcontainer">
<BigIcon icon="BIconEye" <BigIcon
:legend="$t('bookform.wantread')" icon="BIconEye"
:isSet="data.wantread" :legend="$t('bookform.wantread')"
@click="onWantReadIconClick"/> :isSet="data.wantread"
@click="onWantReadIconClick"
/>
</div> </div>
<BookDateWidget <BookDateWidget
icon="BIconBook" icon="BIconBook"
:legend="$t('bookform.startread')" :legend="$t('bookform.startread')"
:start-read-date="data.startReadDate" :start-read-date="data.startReadDate"
:is-expanded="isStartReadExpanded()" :is-expanded="isStartReadExpanded()"
:is-readonly="data.read" :is-readonly="data.read"
@onStartDateChange="onStartReadDateChange" @onStartDateChange="onStartReadDateChange"
@onIconClick="onStartReadIconClick"/> @onIconClick="onStartReadIconClick"
/>
<BookDateWidget <BookDateWidget
icon="BIconCheckCircle" icon="BIconCheckCircle"
:legend="$t('bookform.read')" :legend="$t('bookform.read')"
:start-read-date="data.startReadDate" :start-read-date="data.startReadDate"
use-end-date use-end-date
last-widget last-widget
:endReadDate="data.endReadDate" :endReadDate="data.endReadDate"
:isExpanded="data.read" :isExpanded="data.read"
@onStartDateChange="onStartReadDateChange" @onStartDateChange="onStartReadDateChange"
@onEndDateChange="onEndReadDateChange" @onEndDateChange="onEndReadDateChange"
@onIconClick="onReadIconClick"/> @onIconClick="onReadIconClick"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -150,16 +161,16 @@
<style scoped> <style scoped>
img { img {
max-height:500px; max-height: 500px;
max-width:500px; max-width: 500px;
height:auto; height: auto;
width:auto; width: auto;
} }
.centered { .centered {
display:flex; display: flex;
justify-content:center; justify-content: center;
align-items:center; align-items: center;
} }
.iconscontainer { .iconscontainer {
@@ -178,7 +189,6 @@ img {
} }
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
img { img {
max-height: 250px; max-height: 250px;
@@ -190,15 +200,14 @@ img {
} }
.image { .image {
display:flex; display: flex;
justify-content:center; justify-content: center;
align-items:center; align-items: center;
} }
.iconscontainer { .iconscontainer {
display:flex; display: flex;
width: 100%; width: 100%;
} }
} }
</style> </style>

View File

@@ -1,11 +1,11 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { putReadBook, getImagePathOrDefault, postImportBook } from './api.js' import { putReadBook, getImagePathOrDefault, postImportBook } from './api.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter(); const router = useRouter()
const props = defineProps({ const props = defineProps({
id: Number, id: Number,
inventaireid: String, inventaireid: String,
isinventaireedition: Boolean, isinventaireedition: Boolean,
@@ -16,90 +16,89 @@
read: Boolean, read: Boolean,
wantread: Boolean, wantread: Boolean,
coverPath: String, coverPath: String,
}); })
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath)); const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath))
const error = ref(null) const error = ref(null)
async function onUserBookRead() { async function onUserBookRead() {
const res = await putReadBook(props.id); const res = await putReadBook(props.id)
if (res.ok) {
router.push('/books')
} else {
res.json().then((json) => (error.value = json));
}
}
async function openBook() {
if (props.id != 0) {
router.push(`/book/${props.id}`);
} else if (props.isinventaireedition) {
importInventaireEdition()
} else if (props.inventaireid != "") {
router.push(`/import/inventaire/${props.inventaireid}`)
}
}
async function importInventaireEdition(inventaireid) {
const res = await postImportBook(props.inventaireid, navigator.language.substring(0,2));
const json = await res.json();
if (res.ok) { if (res.ok) {
router.push(`/book/${json.id}`); router.push('/books')
} else { } else {
error.value = json; res.json().then((json) => (error.value = json))
} }
} }
async function openBook() {
if (props.id != 0) {
router.push(`/book/${props.id}`)
} else if (props.isinventaireedition) {
importInventaireEdition()
} else if (props.inventaireid != '') {
router.push(`/import/inventaire/${props.inventaireid}`)
}
}
async function importInventaireEdition(inventaireid) {
const res = await postImportBook(props.inventaireid, navigator.language.substring(0, 2))
const json = await res.json()
if (res.ok) {
router.push(`/book/${json.id}`)
} else {
error.value = json
}
}
</script> </script>
<template> <template>
<div v-if="error" class="notification is-danger"> <div v-if="error" class="notification is-danger">
<p>{{error}}</p> <p>{{ error }}</p>
</div> </div>
<div class="columns no-padding box container has-background-dark"> <div class="columns no-padding box container has-background-dark">
<div class="media column no-margin clickable" @click="openBook"> <div class="media column no-margin clickable" @click="openBook">
<div class="media-left"> <div class="media-left">
<figure class="image mb-3"> <figure class="image mb-3">
<img v-bind:src="imagePathOrDefault" v-bind:alt="title"> <img v-bind:src="imagePathOrDefault" v-bind:alt="title" />
</figure> </figure>
</div> </div>
<div class="media-content"> <div class="media-content">
<div class="is-size-4">{{title}}</div> <div class="is-size-4">{{ title }}</div>
<div class="is-size-5 is-italic">{{author}}</div> <div class="is-size-5 is-italic">{{ author }}</div>
<div class="has-text-text-65 is-size-6" v-if="props.description">{{description}}</div> <div class="has-text-text-65 is-size-6" v-if="props.description">{{ description }}</div>
</div> </div>
</div> </div>
<div v-if="!inventaireid" class="column is-narrow"> <div v-if="!inventaireid" class="column is-narrow">
<button @click="" class="button is-large verticalbutton"> <button @click="" class="button is-large verticalbutton">
<span class="icon" :title="$t('booklistelement.wantread')"> <span class="icon" :title="$t('booklistelement.wantread')">
<b-icon-eye-fill v-if="props.wantread" /> <b-icon-eye-fill v-if="props.wantread" />
<b-icon-eye v-else /> <b-icon-eye v-else />
</span> </span>
</button> </button>
<button @click="" class="button is-large verticalbutton"> <button @click="" class="button is-large verticalbutton">
<span class="icon" :title="$t('booklistelement.startread')"> <span class="icon" :title="$t('booklistelement.startread')">
<b-icon-book /> <b-icon-book />
</span> </span>
</button> </button>
<button @click="onUserBookRead" class="button is-large verticalbutton"> <button @click="onUserBookRead" class="button is-large verticalbutton">
<span class="icon" :title="$t('booklistelement.read')"> <span class="icon" :title="$t('booklistelement.read')">
<b-icon-check-circle-fill v-if="props.read" /> <b-icon-check-circle-fill v-if="props.read" />
<b-icon-check-circle v-else /> <b-icon-check-circle v-else />
</span> </span>
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
img { img {
max-height:180px; max-height: 180px;
max-width:180px; max-width: 180px;
height:auto; height: auto;
width:auto; width: auto;
} }
.box { .box {
transition:ease-in-out 0.04s; transition: ease-in-out 0.04s;
margin-bottom: 15px; margin-bottom: 15px;
} }
@@ -119,5 +118,4 @@ img {
.no-margin { .no-margin {
margin: 0px; margin: 0px;
} }
</style> </style>

View File

@@ -4,89 +4,98 @@ import BookCard from './BookCard.vue'
import { getMyBooks } from './api.js' import { getMyBooks } from './api.js'
import Pagination from './Pagination.vue' import Pagination from './Pagination.vue'
const FilterStates = Object.freeze({ const FilterStates = Object.freeze({
READ: "read", READ: 'read',
WANTREAD: "wantread", WANTREAD: 'wantread',
READING: "reading", READING: 'reading',
}); })
const limit = 6; const limit = 6
const pageNumber = ref(1); const pageNumber = ref(1)
const offset = computed(() => (pageNumber.value - 1) * limit); const offset = computed(() => (pageNumber.value - 1) * limit)
let currentFilterState = ref(FilterStates.READ); let currentFilterState = ref(FilterStates.READ)
let data = ref(null); let data = ref(null)
let error = ref(null); let error = ref(null)
let totalBooksNumber = computed(() => (typeof(data) != 'undefined' && let totalBooksNumber = computed(() =>
data.value != null) ? data.value["count"] : 0); typeof data != 'undefined' && data.value != null ? data.value['count'] : 0,
)
let pageTotal = computed(() => Math.ceil(totalBooksNumber.value / limit)) let pageTotal = computed(() => Math.ceil(totalBooksNumber.value / limit))
fetchData(); fetchData()
function fetchData() { function fetchData() {
let res = getMyBooks(data, error, currentFilterState.value, limit, offset.value); let res = getMyBooks(data, error, currentFilterState.value, limit, offset.value)
} }
function onFilterButtonClick(newstate) { function onFilterButtonClick(newstate) {
currentFilterState.value = newstate; currentFilterState.value = newstate
pageNumber.value = 1; pageNumber.value = 1
fetchData(); fetchData()
} }
function computeDynamicClass(state) { function computeDynamicClass(state) {
return currentFilterState.value === state ? 'is-active is-primary' : ''; return currentFilterState.value === state ? 'is-active is-primary' : ''
} }
function pageChange(newPageNumber) { function pageChange(newPageNumber) {
pageNumber.value = newPageNumber; pageNumber.value = newPageNumber
data.value = null; data.value = null
fetchData(); fetchData()
} }
</script> </script>
<template> <template>
<div class="mb-5"> <div class="mb-5">
<button class="button is-medium" <button
@click="onFilterButtonClick(FilterStates.READ)" class="button is-medium"
:class="computeDynamicClass(FilterStates.READ)"> @click="onFilterButtonClick(FilterStates.READ)"
{{$t('bookbrowser.read')}} :class="computeDynamicClass(FilterStates.READ)"
>
{{ $t('bookbrowser.read') }}
</button> </button>
<button class="button is-medium" <button
@click="onFilterButtonClick(FilterStates.READING)" class="button is-medium"
:class="computeDynamicClass(FilterStates.READING)"> @click="onFilterButtonClick(FilterStates.READING)"
{{$t('bookbrowser.reading')}} :class="computeDynamicClass(FilterStates.READING)"
>
{{ $t('bookbrowser.reading') }}
</button> </button>
<button class="button is-medium" <button
@click="onFilterButtonClick(FilterStates.WANTREAD)" class="button is-medium"
:class="computeDynamicClass(FilterStates.WANTREAD)"> @click="onFilterButtonClick(FilterStates.WANTREAD)"
{{$t('bookbrowser.wantread')}} :class="computeDynamicClass(FilterStates.WANTREAD)"
>
{{ $t('bookbrowser.wantread') }}
</button> </button>
</div> </div>
<div v-if="error">{{$t('bookbrowser.error', {error: error.message})}}</div> <div v-if="error">{{ $t('bookbrowser.error', { error: error.message }) }}</div>
<div v-else-if="data"> <div v-else-if="data">
<div class=""> <div class="">
<div class="" v-for="book in data.books" :key="book.id"> <div class="" v-for="book in data.books" :key="book.id">
<BookCard v-bind="book" /> <BookCard v-bind="book" />
</div> </div>
</div> </div>
<Pagination :pageNumber="pageNumber" :pageTotal="pageTotal" maxItemDisplayed="11" @pageChange="pageChange"/> <Pagination
:pageNumber="pageNumber"
:pageTotal="pageTotal"
maxItemDisplayed="11"
@pageChange="pageChange"
/>
</div> </div>
<div v-else>{{$t('bookbrowser.loading')}}</div> <div v-else>{{ $t('bookbrowser.loading') }}</div>
</template> </template>
<style scoped> <style scoped>
.books { .books {
position:relative; position: relative;
float:left; float: left;
width:100%; height:auto; width: 100%;
padding-bottom: 100px; height: auto;
padding-bottom: 100px;
line-height: 2.5; line-height: 2.5;
column-count: 2; column-count: 2;

View File

@@ -1,86 +1,92 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { postImage } from './api.js' import { postImage } from './api.js'
const emit = defineEmits(['OnImageUpload']) const emit = defineEmits(['OnImageUpload'])
const props = defineProps({ const props = defineProps({
name: String name: String,
}); })
const imagePath = ref(null); const imagePath = ref(null)
const error = ref(null); const error = ref(null)
function onFileChanged(e) { function onFileChanged(e) {
postImage(e.target.files[0]) postImage(e.target.files[0])
.then((res) => res.json()) .then((res) => res.json())
.then((json) => onJsonResult(json)) .then((json) => onJsonResult(json))
.catch((err) => (error.value = err["error"])); .catch((err) => (error.value = err['error']))
} }
function onJsonResult(json) { function onJsonResult(json) {
imagePath.value = json["filepath"]; imagePath.value = json['filepath']
emit('OnImageUpload', json["fileId"]) emit('OnImageUpload', json['fileId'])
} }
function unsetImage() { function unsetImage() {
imagePath.value = null; imagePath.value = null
} }
const imageSrc = computed(() => { const imageSrc = computed(() => {
return "http://localhost:8080" + imagePath.value return 'http://localhost:8080' + imagePath.value
}) })
</script> </script>
<template> <template>
<div v-if="imagePath"> <div v-if="imagePath">
<div class="relative"> <div class="relative">
<figure class="image mb-3"> <figure class="image mb-3">
<img v-bind:src="imageSrc" v-bind:alt="props.name"> <img v-bind:src="imageSrc" v-bind:alt="props.name" />
</figure> </figure>
<span class="icon is-large" @click="unsetImage"> <span class="icon is-large" @click="unsetImage">
<b-icon-x-circle-fill/> <b-icon-x-circle-fill />
</span> </span>
</div> </div>
</div> </div>
<div v-else class="file"> <div v-else class="file">
<label class="file-label"> <label class="file-label">
<input class="file-input" @change="onFileChanged" type="file" :name="props.name" accept="image/*"/> <input
class="file-input"
@change="onFileChanged"
type="file"
:name="props.name"
accept="image/*"
/>
<span class="file-cta"> <span class="file-cta">
<span class="file-icon"> <span class="file-icon">
<b-icon-upload /> <b-icon-upload />
</span> </span>
<span class="file-label">{{$t('addbook.coverupload')}}</span> <span class="file-label">{{ $t('addbook.coverupload') }}</span>
</span> </span>
</label> </label>
</div> </div>
</template> </template>
<style scoped> <style scoped>
img { img {
max-height:500px; max-height: 500px;
max-width:500px; max-width: 500px;
height:auto; height: auto;
width:auto; width: auto;
} }
.relative { .relative {
position: relative; position: relative;
} }
.relative img { .relative img {
display: block; display: block;
} }
.relative .icon { .relative .icon {
position: absolute; position: absolute;
bottom:5px; bottom: 5px;
left:5px; left: 5px;
background-color: rgba(0,0,0,0.7); background-color: rgba(0, 0, 0, 0.7);
font-size: 24px; font-size: 24px;
border-radius: 5px; border-radius: 5px;
} }
.relative .icon:hover { .relative .icon:hover {
background-color: rgba(0,0,0,0.8); background-color: rgba(0, 0, 0, 0.8);
cursor: pointer; cursor: pointer;
} }
</style> </style>

View File

@@ -1,27 +1,24 @@
<script setup> <script setup>
import { useAuthStore } from './auth.store.js' import { useAuthStore } from './auth.store.js'
const authStore = useAuthStore();
const authStore = useAuthStore()
</script> </script>
<template> <template>
<div v-if="authStore.user"> <div v-if="authStore.user">
{{ $t('home.welcomeuser', {username: authStore.user.username}) }} {{ $t('home.welcomeuser', { username: authStore.user.username }) }}
</div> </div>
<div v-else> <div v-else>
<p>{{ $t('home.welcome') }}</p> <p>{{ $t('home.welcome') }}</p>
<div class="mt-5 is-hidden-desktop"> <div class="mt-5 is-hidden-desktop">
<RouterLink to="/signup" class="button is-primary mx-2"> <RouterLink to="/signup" class="button is-primary mx-2">
<strong>{{ $t('navbar.signup')}}</strong> <strong>{{ $t('navbar.signup') }}</strong>
</RouterLink> </RouterLink>
<RouterLink to="/login" class="button is-light mx-2"> <RouterLink to="/login" class="button is-light mx-2">
{{ $t('navbar.login')}} {{ $t('navbar.login') }}
</RouterLink> </RouterLink>
</div> </div>
</div> </div>
</template> </template>
<style scoped></style>
<style scoped>
</style>

View File

@@ -1,68 +1,66 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import ImportListElement from './ImportListElement.vue'; import ImportListElement from './ImportListElement.vue'
import { getInventaireEditionBooks } from './api.js' import { getInventaireEditionBooks } from './api.js'
import { onBeforeRouteUpdate } from 'vue-router' import { onBeforeRouteUpdate } from 'vue-router'
import Pagination from './Pagination.vue' import Pagination from './Pagination.vue'
const limit = 5; const limit = 5
const pageNumber = ref(1); const pageNumber = ref(1)
const offset = computed(() => (pageNumber.value - 1) * limit); const offset = computed(() => (pageNumber.value - 1) * limit)
const props = defineProps({ const props = defineProps({
inventaireid: String, inventaireid: String,
}); })
let data = ref(null); let data = ref(null)
let error = ref(null); let error = ref(null)
const pageTotal = computed(() => { const pageTotal = computed(() => {
let countValue = (data.value !== null) ? data.value['count'] : 0; let countValue = data.value !== null ? data.value['count'] : 0
return Math.ceil(countValue / limit); return Math.ceil(countValue / limit)
}); })
fetchData(props.inventaireid); fetchData(props.inventaireid)
function fetchData(inventaireid, authorId) {
function fetchData(inventaireid, authorId) { if (inventaireid != null) {
if (inventaireid != null) { let lang = navigator.language.substring(0, 2)
let lang = navigator.language.substring(0,2); getInventaireEditionBooks(data, error, inventaireid, lang, limit, offset.value)
getInventaireEditionBooks(data, error, inventaireid, lang, limit, offset.value);
}
} }
}
onBeforeRouteUpdate(async (to, from) => { onBeforeRouteUpdate(async (to, from) => {
pageNumber.value = 1; pageNumber.value = 1
fetchData(to.params.inventaireid); fetchData(to.params.inventaireid)
}) })
function pageChange(newPageNumber) {
pageNumber.value = newPageNumber;
data.value = null;
fetchData(props.inventaireid);
}
function pageChange(newPageNumber) {
pageNumber.value = newPageNumber
data.value = null
fetchData(props.inventaireid)
}
</script> </script>
<template> <template>
<div class="booksearch my-2"> <div class="booksearch my-2">
<h1 class="title">{{$t('importinventaire.title')}}</h1> <h1 class="title">{{ $t('importinventaire.title') }}</h1>
<div v-if="error">{{$t('searchbook.error', {error: error.message})}}</div> <div v-if="error">{{ $t('searchbook.error', { error: error.message }) }}</div>
<div v-else-if="data && data.results && data.results.length > 0"> <div v-else-if="data && data.results && data.results.length > 0">
<div class="booksearchlist" v-for="book in data.results" :key="book.id"> <div class="booksearchlist" v-for="book in data.results" :key="book.id">
<ImportListElement v-bind="book" /> <ImportListElement v-bind="book" />
</div> </div>
<Pagination <Pagination
:pageNumber="pageNumber" :pageNumber="pageNumber"
:pageTotal="pageTotal" :pageTotal="pageTotal"
maxItemDisplayed="11" maxItemDisplayed="11"
@pageChange="pageChange"/> @pageChange="pageChange"
/>
</div> </div>
<div v-else-if="data === null">{{$t('searchbook.loading')}}</div> <div v-else-if="data === null">{{ $t('searchbook.loading') }}</div>
<div v-else>{{$t('searchbook.noresult')}}</div> <div v-else>{{ $t('searchbook.noresult') }}</div>
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,65 +1,67 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { getInventaireImagePathOrDefault, postImportBook } from './api.js' import { getInventaireImagePathOrDefault, postImportBook } from './api.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter(); const router = useRouter()
const props = defineProps({ const props = defineProps({
uri: String, uri: String,
title: String, title: String,
image: String, image: String,
publisher: String, publisher: String,
date: String, date: String,
isbn: String, isbn: String,
lang: String lang: String,
}); })
function displayDate(date) { function displayDate(date) {
const regex = /^\d{4}-\d{2}-\d{2}$/; const regex = /^\d{4}-\d{2}-\d{2}$/
if (regex.test(date)) { if (regex.test(date)) {
const d = new Date(date); const d = new Date(date)
return d.toLocaleDateString(); return d.toLocaleDateString()
} else { } else {
return date; return date
}
} }
const error = ref(null); }
const data = ref(null); const error = ref(null)
const importing = ref(false); const data = ref(null)
const importing = ref(false)
async function importInventaireEdition() { async function importInventaireEdition() {
importing.value = true; importing.value = true
const res = await postImportBook(props.uri, navigator.language.substring(0,2)); const res = await postImportBook(props.uri, navigator.language.substring(0, 2))
const json = await res.json(); const json = await res.json()
if (res.ok) { if (res.ok) {
router.push(`/book/${json.id}`); router.push(`/book/${json.id}`)
} else { } else {
error.value = json; error.value = json
}
} }
const imagePathOrDefault = computed(() => getInventaireImagePathOrDefault(props.image)); }
const imagePathOrDefault = computed(() => getInventaireImagePathOrDefault(props.image))
</script> </script>
<template> <template>
<div class="columns no-padding box container has-background-dark"> <div class="columns no-padding box container has-background-dark">
<div v-if="importing && !data && !error">{{$t('importlistelement.importing')}}</div> <div v-if="importing && !data && !error">{{ $t('importlistelement.importing') }}</div>
<div v-else class="media column no-margin clickable" @click="importInventaireEdition"> <div v-else class="media column no-margin clickable" @click="importInventaireEdition">
<div class="media-left"> <div class="media-left">
<figure class="image mb-3"> <figure class="image mb-3">
<img v-bind:src="imagePathOrDefault" v-bind:alt="title"> <img v-bind:src="imagePathOrDefault" v-bind:alt="title" />
</figure> </figure>
</div> </div>
<div class="media-content"> <div class="media-content">
<div v-if="error" class="has-text-danger"> <div v-if="error" class="has-text-danger">
<p>{{error}}</p> <p>{{ error }}</p>
</div> </div>
<div v-else> <div v-else>
<div class="is-size-4">{{title}}</div> <div class="is-size-4">{{ title }}</div>
<div v-if="props.date" class="is-size-5">{{$t('importlistelement.releasedate') + "" + <div v-if="props.date" class="is-size-5">
displayDate(date)}}</div> {{ $t('importlistelement.releasedate') + '' + displayDate(date) }}
<div v-if="props.publisher" class="is-size-5">{{$t('importlistelement.publisher') + "" + publisher}}</div> </div>
<div v-if="props.isbn" class="is-size-5">{{"ISBN: " + isbn}}</div> <div v-if="props.publisher" class="is-size-5">
{{ $t('importlistelement.publisher') + '' + publisher }}
</div>
<div v-if="props.isbn" class="is-size-5">{{ 'ISBN: ' + isbn }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -68,14 +70,14 @@
<style scoped> <style scoped>
img { img {
max-height:180px; max-height: 180px;
max-width:180px; max-width: 180px;
height:auto; height: auto;
width:auto; width: auto;
} }
.box { .box {
transition:ease-in-out 0.04s; transition: ease-in-out 0.04s;
margin-bottom: 15px; margin-bottom: 15px;
} }
@@ -95,5 +97,4 @@ img {
.no-margin { .no-margin {
margin: 0px; margin: 0px;
} }
</style> </style>

View File

@@ -1,71 +1,84 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { postLogin, extractFormErrorFromField, extractGlobalFormError } from './api.js' import { postLogin, extractFormErrorFromField, extractGlobalFormError } from './api.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAuthStore } from './auth.store.js' import { useAuthStore } from './auth.store.js'
const router = useRouter(); const router = useRouter()
const user = ref({ const user = ref({
username: "", username: '',
password: "" password: '',
}); })
const errors = ref(null) const errors = ref(null)
const formError = computed(() => { const formError = computed(() => {
return extractGlobalFormError(errors.value); return extractGlobalFormError(errors.value)
}) })
const userError = computed(() => { const userError = computed(() => {
return extractFormErrorFromField("Username", errors.value); return extractFormErrorFromField('Username', errors.value)
}) })
const passwordError = computed(() => { const passwordError = computed(() => {
return extractFormErrorFromField("Password", errors.value); return extractFormErrorFromField('Password', errors.value)
}) })
async function onSubmit(e) { async function onSubmit(e) {
const res = await postLogin(user) const res = await postLogin(user)
if (res.ok) { if (res.ok) {
let json = await res.json(); let json = await res.json()
await login(user.value.username, json); await login(user.value.username, json)
router.push('/'); router.push('/')
return; return
} else { } else {
res.json().then((json) => (errors.value = json)); res.json().then((json) => (errors.value = json))
}
} }
}
async function login(username, json) { async function login(username, json) {
useAuthStore().login({username: username, token: json["token"]}) useAuthStore().login({ username: username, token: json['token'] })
} }
</script> </script>
<template> <template>
<div v-if="formError" class="notification is-danger"> <div v-if="formError" class="notification is-danger">
<p>{{formError}}</p> <p>{{ formError }}</p>
</div> </div>
<h1 class="title">{{$t('login.title')}}</h1> <h1 class="title">{{ $t('login.title') }}</h1>
<form class="box" @submit.prevent="onSubmit"> <form class="box" @submit.prevent="onSubmit">
<div class="field"> <div class="field">
<label class="label">{{$t('login.username')}}</label> <label class="label">{{ $t('login.username') }}</label>
<div class="control"> <div class="control">
<input :class="'input ' + (userError ? 'is-danger' : '')" type="text" minlength="2" maxlength="20" <input
required v-model="user.username" :placeholder="$t('login.username')"> :class="'input ' + (userError ? 'is-danger' : '')"
type="text"
minlength="2"
maxlength="20"
required
v-model="user.username"
:placeholder="$t('login.username')"
/>
</div> </div>
<p v-if="userError" class="help is-danger">{{userError}}</p> <p v-if="userError" class="help is-danger">{{ userError }}</p>
</div> </div>
<div class="field"> <div class="field">
<label class="label">{{$t('login.password')}}</label> <label class="label">{{ $t('login.password') }}</label>
<div class="control"> <div class="control">
<input :class="'input ' + (passwordError ? 'is-danger' : '')" type="password" minlength="6" <input
maxlength="100" v-model="user.password" required :placeholder="$t('login.password')"> :class="'input ' + (passwordError ? 'is-danger' : '')"
type="password"
minlength="6"
maxlength="100"
v-model="user.password"
required
:placeholder="$t('login.password')"
/>
</div> </div>
<p v-if="passwordError" class="help is-danger">{{passwordError}}</p> <p v-if="passwordError" class="help is-danger">{{ passwordError }}</p>
</div> </div>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<button class="button is-link">{{$t('login.login')}}</button> <button class="button is-link">{{ $t('login.login') }}</button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -1,40 +1,47 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import BarcodeModal from './BarcodeModal.vue' import BarcodeModal from './BarcodeModal.vue'
const searchterm = ref(""); const searchterm = ref('')
const router = useRouter(); const router = useRouter()
const props = defineProps({ const props = defineProps({
sizeClass: String, sizeClass: String,
isMobile: Boolean isMobile: Boolean,
}); })
const emit = defineEmits('searchDone') const emit = defineEmits('searchDone')
const vMobileFocus = { const vMobileFocus = {
mounted: (el, binding) => { mounted: (el, binding) => {
if (binding.value) { if (binding.value) {
el.focus() el.focus()
}
} }
}; },
}
function onSearchClick() { function onSearchClick() {
if (typeof searchterm.value === "undefined" || searchterm.value === "") { if (typeof searchterm.value === 'undefined' || searchterm.value === '') {
return return
}
emit('searchDone')
router.push('/search/' + searchterm.value);
} }
emit('searchDone')
router.push('/search/' + searchterm.value)
}
</script> </script>
<template> <template>
<div class="navbar-item"> <div class="navbar-item">
<div class="field has-addons"> <div class="field has-addons">
<div class="control" :class="isMobile ? 'fullwidth' : ''"> <div class="control" :class="isMobile ? 'fullwidth' : ''">
<input v-mobile-focus="isMobile ? true : null" v-model="searchterm" @keyup.enter="onSearchClick()" class="input" :class="sizeClass" type="text" /> <input
v-mobile-focus="isMobile ? true : null"
v-model="searchterm"
@keyup.enter="onSearchClick()"
class="input"
:class="sizeClass"
type="text"
/>
</div> </div>
<div class="control"> <div class="control">
<button @click="onSearchClick()" class="button" :class="sizeClass"> <button @click="onSearchClick()" class="button" :class="sizeClass">
@@ -43,13 +50,13 @@
</span> </span>
</button> </button>
</div> </div>
<BarcodeModal v-if="!isMobile"/> <BarcodeModal v-if="!isMobile" />
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.fullwidth { .fullwidth {
width: 100%; width: 100%;
} }
</style> </style>

View File

@@ -1,82 +1,91 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps(['pageNumber', 'pageTotal', 'maxItemDisplayed']); const props = defineProps(['pageNumber', 'pageTotal', 'maxItemDisplayed'])
const emit = defineEmits(['pageChange']); const emit = defineEmits(['pageChange'])
const paginatedItems = computed(() => { const paginatedItems = computed(() => {
let items = []; let items = []
if (props.pageTotal > props.maxItemDisplayed) { if (props.pageTotal > props.maxItemDisplayed) {
//number of pages we can display before and after the current page
const maxNumberOfItemsAroundCurrentItem = Math.floor((props.maxItemDisplayed - 2) / 2)
//number of pages we can display before and after the current page items.push(1)
const maxNumberOfItemsAroundCurrentItem = Math.floor((props.maxItemDisplayed - 2) / 2); //compute first item number
let firstItemNumber
let lastItemNumber
items.push(1); if (props.pageNumber - maxNumberOfItemsAroundCurrentItem < 4) {
//compute first item number //starting at the left
let firstItemNumber; firstItemNumber = 2
let lastItemNumber; lastItemNumber = props.maxItemDisplayed
} else if (props.pageNumber + maxNumberOfItemsAroundCurrentItem > props.pageTotal - 2) {
if (props.pageNumber - maxNumberOfItemsAroundCurrentItem < 4) { //starting at the right
//starting at the left firstItemNumber = props.pageTotal - props.maxItemDisplayed + 1
firstItemNumber = 2; lastItemNumber = props.pageTotal - 1
lastItemNumber = props.maxItemDisplayed;
} else if (props.pageNumber + maxNumberOfItemsAroundCurrentItem > props.pageTotal - 2) {
//starting at the right
firstItemNumber = props.pageTotal - props.maxItemDisplayed + 1;
lastItemNumber = props.pageTotal - 1;
} else {
firstItemNumber = props.pageNumber - maxNumberOfItemsAroundCurrentItem;
lastItemNumber = props.pageNumber + maxNumberOfItemsAroundCurrentItem;
}
if (firstItemNumber !== 2) {
items.push(-1);
}
for (let i = firstItemNumber; i <= lastItemNumber; i++) {
items.push(i);
}
if (lastItemNumber !== props.pageTotal - 1) {
items.push(-1);
}
items.push(props.pageTotal);
} else { } else {
for (let i = 1; i <= props.pageTotal; i++) { firstItemNumber = props.pageNumber - maxNumberOfItemsAroundCurrentItem
items.push(i); lastItemNumber = props.pageNumber + maxNumberOfItemsAroundCurrentItem
}
} }
return items;})
if (firstItemNumber !== 2) {
items.push(-1)
}
for (let i = firstItemNumber; i <= lastItemNumber; i++) {
items.push(i)
}
if (lastItemNumber !== props.pageTotal - 1) {
items.push(-1)
}
items.push(props.pageTotal)
} else {
for (let i = 1; i <= props.pageTotal; i++) {
items.push(i)
}
}
return items
})
</script> </script>
<template> <template>
<nav v-if="props.pageTotal > 1" class="pagination" role="navigation" aria-label="pagination"> <nav v-if="props.pageTotal > 1" class="pagination" role="navigation" aria-label="pagination">
<a href="#" v-if="props.pageNumber > 1" <a
@click="$emit('pageChange', Math.max(1, props.pageNumber - 1))" href="#"
class="pagination-previous"> v-if="props.pageNumber > 1"
{{$t('pagination.previous')}} @click="$emit('pageChange', Math.max(1, props.pageNumber - 1))"
class="pagination-previous"
>
{{ $t('pagination.previous') }}
</a> </a>
<a href="#" v-if="props.pageNumber < props.pageTotal" <a
@click="$emit('pageChange', props.pageNumber + 1)" href="#"
class="pagination-next"> v-if="props.pageNumber < props.pageTotal"
{{$t('pagination.next')}} @click="$emit('pageChange', props.pageNumber + 1)"
</a> class="pagination-next"
<ul class="pagination-list"> >
<li v-for="item in paginatedItems" :key="item"> {{ $t('pagination.next') }}
<span v-if="item === -1" class="pagination-ellipsis">&hellip;</span> </a>
<a v-else <ul class="pagination-list">
href="#" <li v-for="item in paginatedItems" :key="item">
@click="$emit('pageChange', item)" <span v-if="item === -1" class="pagination-ellipsis">&hellip;</span>
class="pagination-link" <a
:class="item === props.pageNumber ? 'is-current' : ''" v-else
:aria-current="item === props.pageNumber ? 'page' : null" href="#"
:aria-label="$t(item === props.pageNumber ? 'pagination.page' : 'pagination.goto', {pageNumber: item})"> @click="$emit('pageChange', item)"
{{ item }} class="pagination-link"
</a> :class="item === props.pageNumber ? 'is-current' : ''"
</li> :aria-current="item === props.pageNumber ? 'page' : null"
</ul> :aria-label="
</nav> $t(item === props.pageNumber ? 'pagination.page' : 'pagination.goto', {
pageNumber: item,
})
"
>
{{ item }}
</a>
</li>
</ul>
</nav>
</template> </template>
<style scoped></style>
<style scoped>
</style>

View File

@@ -1,34 +1,34 @@
<script setup> <script setup>
import { useTemplateRef, onMounted, ref } from 'vue' import { useTemplateRef, onMounted, ref } from 'vue'
import { BrowserMultiFormatReader } from "@zxing/library"; import { BrowserMultiFormatReader } from '@zxing/library'
const emit = defineEmits('readBarcode') const emit = defineEmits('readBarcode')
const scanResult = ref(null); const scanResult = ref(null)
const codeReader = new BrowserMultiFormatReader(); const codeReader = new BrowserMultiFormatReader()
const scannerElement = useTemplateRef("scanner"); const scannerElement = useTemplateRef('scanner')
onMounted(() => { onMounted(() => {
codeReader.decodeFromVideoDevice(undefined, scannerElement.value, (result, err) => { codeReader.decodeFromVideoDevice(undefined, scannerElement.value, (result, err) => {
if (result) { if (result) {
emit('readBarcode', result.text) emit('readBarcode', result.text)
} }
}); })
}); })
function onResult(result) { function onResult(result) {
scanResult.value = result scanResult.value = result
} }
</script> </script>
<template> <template>
<h1 class="subtitle">{{$t('barcode.title')}}</h1> <h1 class="subtitle">{{ $t('barcode.title') }}</h1>
<div v-if="scanResult">{{scanResult}}</div> <div v-if="scanResult">{{ scanResult }}</div>
<video poster="data:image/gif,AAAA" ref="scanner"></video> <video poster="data:image/gif,AAAA" ref="scanner"></video>
</template> </template>
<style scoped> <style scoped>
video { video {
max-width: 90%; max-width: 90%;
} }
</style> </style>

View File

@@ -1,90 +1,93 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import BookListElement from './BookListElement.vue'; import BookListElement from './BookListElement.vue'
import { getSearchBooks, getAuthorBooks } from './api.js' import { getSearchBooks, getAuthorBooks } from './api.js'
import { onBeforeRouteUpdate } from 'vue-router' import { onBeforeRouteUpdate } from 'vue-router'
import Pagination from './Pagination.vue' import Pagination from './Pagination.vue'
const limit = 5; const limit = 5
const pageNumber = ref(1); const pageNumber = ref(1)
const offset = computed(() => (pageNumber.value - 1) * limit); const offset = computed(() => (pageNumber.value - 1) * limit)
const props = defineProps({ const props = defineProps({
searchterm: String, searchterm: String,
authorId: Number authorId: Number,
}); })
const forceSearchInventaire = ref(false); const forceSearchInventaire = ref(false)
const searchInventaire = computed(() => { const searchInventaire = computed(() => {
return forceSearchInventaire.value || (data.value !== null && data.value['inventaire']) return forceSearchInventaire.value || (data.value !== null && data.value['inventaire'])
}); })
let data = ref(null); let data = ref(null)
let error = ref(null); let error = ref(null)
const pageTotal = computed(() => { const pageTotal = computed(() => {
const countValue = (data.value !== null) ? data.value['count'] : 0; const countValue = data.value !== null ? data.value['count'] : 0
return Math.ceil(countValue / limit); return Math.ceil(countValue / limit)
}); })
fetchData(props.searchterm, props.authorId); fetchData(props.searchterm, props.authorId)
function fetchData(searchTerm, authorId) {
function fetchData(searchTerm, authorId) { data.value = null
data.value = null; error.value = null
error.value = null; if (searchTerm != null) {
if (searchTerm != null) { const lang = navigator.language.substring(0, 2)
const lang = navigator.language.substring(0,2); getSearchBooks(data, error, searchTerm, lang, forceSearchInventaire.value, limit, offset.value)
getSearchBooks(data, error, searchTerm, lang, forceSearchInventaire.value, limit, offset.value); } else if (authorId != null) {
} else if (authorId != null) { getAuthorBooks(data, error, authorId, limit, offset.value)
getAuthorBooks(data, error, authorId, limit, offset.value);
}
} }
}
onBeforeRouteUpdate(async (to, from) => { onBeforeRouteUpdate(async (to, from) => {
pageNumber.value = 1; pageNumber.value = 1
fetchData(to.params.searchterm, props.authorId); fetchData(to.params.searchterm, props.authorId)
}) })
function pageChange(newPageNumber) { function pageChange(newPageNumber) {
pageNumber.value = newPageNumber; pageNumber.value = newPageNumber
data.value = null; data.value = null
fetchData(props.searchterm, props.authorId); fetchData(props.searchterm, props.authorId)
} }
function toggleSearchInventaire() {
pageNumber.value = 1;
forceSearchInventaire.value = !forceSearchInventaire.value;
fetchData(props.searchterm, props.authorId)
window.scrollTo(0,0);
}
function toggleSearchInventaire() {
pageNumber.value = 1
forceSearchInventaire.value = !forceSearchInventaire.value
fetchData(props.searchterm, props.authorId)
window.scrollTo(0, 0)
}
</script> </script>
<template> <template>
<div class="booksearch my-2"> <div class="booksearch my-2">
<h1 class="title" v-if="data && data.inventaire">{{$t('searchbook.importinventaire')}}</h1> <h1 class="title" v-if="data && data.inventaire">{{ $t('searchbook.importinventaire') }}</h1>
<div v-if="error">{{$t('searchbook.error', {error: error.message})}}</div> <div v-if="error">{{ $t('searchbook.error', { error: error.message }) }}</div>
<div v-else-if="data && data.books && data.books.length > 0"> <div v-else-if="data && data.books && data.books.length > 0">
<div class="booksearchlist" v-for="book in data.books" :key="book.id"> <div class="booksearchlist" v-for="book in data.books" :key="book.id">
<BookListElement v-bind="book" /> <BookListElement v-bind="book" />
</div> </div>
<div v-if="(!searchInventaire || forceSearchInventaire) && !authorId" class="box container clickable has-background-dark" @click="toggleSearchInventaire"> <div
<div class="is-size-4">{{searchInventaire ? $t('searchbook.backtosearch') : $t('searchbook.searchinventaire')}}</div> v-if="(!searchInventaire || forceSearchInventaire) && !authorId"
class="box container clickable has-background-dark"
@click="toggleSearchInventaire"
>
<div class="is-size-4">
{{ searchInventaire ? $t('searchbook.backtosearch') : $t('searchbook.searchinventaire') }}
</div>
</div> </div>
<Pagination <Pagination
:pageNumber="pageNumber" :pageNumber="pageNumber"
:pageTotal="pageTotal" :pageTotal="pageTotal"
maxItemDisplayed="11" maxItemDisplayed="11"
@pageChange="pageChange"/> @pageChange="pageChange"
/>
</div> </div>
<div v-else-if="data === null">{{$t('searchbook.loading')}}</div> <div v-else-if="data === null">{{ $t('searchbook.loading') }}</div>
<div v-else>{{$t('searchbook.noresult')}}</div> <div v-else>{{ $t('searchbook.noresult') }}</div>
</div> </div>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@@ -1,61 +1,74 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { postSignUp, extractFormErrorFromField, extractGlobalFormError } from './api.js' import { postSignUp, extractFormErrorFromField, extractGlobalFormError } from './api.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter(); const router = useRouter()
const user = ref({ const user = ref({
username: "", username: '',
password: "" password: '',
}); })
const errors = ref(null) const errors = ref(null)
const formError = computed(() => { const formError = computed(() => {
return extractGlobalFormError(errors.value); return extractGlobalFormError(errors.value)
})
const userError = computed(() => {
return extractFormErrorFromField('Username', errors.value)
})
const passwordError = computed(() => {
return extractFormErrorFromField('Password', errors.value)
})
function onSubmit() {
postSignUp(user).then((res) => {
if (res.ok) {
router.push('/')
return
} else {
res.json().then((json) => (errors.value = json))
}
}) })
const userError = computed(() => { }
return extractFormErrorFromField("Username", errors.value);
})
const passwordError = computed(() => {
return extractFormErrorFromField("Password", errors.value);
})
function onSubmit() {
postSignUp(user)
.then((res) => {
if (res.ok) {
router.push('/');
return;
} else {
res.json().then((json) => (errors.value = json));
}
})
}
</script> </script>
<template> <template>
<div v-if="formError" class="notification is-danger"> <div v-if="formError" class="notification is-danger">
<p>{{formError}}</p> <p>{{ formError }}</p>
</div> </div>
<h1 class="title">{{$t('signup.title')}}</h1> <h1 class="title">{{ $t('signup.title') }}</h1>
<form class="box" @submit.prevent="onSubmit"> <form class="box" @submit.prevent="onSubmit">
<div class="field"> <div class="field">
<label class="label">{{$t('signup.username')}}</label> <label class="label">{{ $t('signup.username') }}</label>
<div class="control"> <div class="control">
<input :class="'input ' + (userError ? 'is-danger' : '')" type="text" minlength="2" maxlength="20" <input
required v-model="user.username" :placeholder="$t('signup.username')"> :class="'input ' + (userError ? 'is-danger' : '')"
type="text"
minlength="2"
maxlength="20"
required
v-model="user.username"
:placeholder="$t('signup.username')"
/>
</div> </div>
<p v-if="userError" class="help is-danger">{{userError}}</p> <p v-if="userError" class="help is-danger">{{ userError }}</p>
</div> </div>
<div class="field"> <div class="field">
<label class="label">{{ $t('signup.password') }}</label> <label class="label">{{ $t('signup.password') }}</label>
<div class="control"> <div class="control">
<input :class="'input ' + (passwordError ? 'is-danger' : '')" type="password" minlength="6" <input
maxlength="100" v-model="user.password" required :placeholder="$t('signup.password')"> :class="'input ' + (passwordError ? 'is-danger' : '')"
type="password"
minlength="6"
maxlength="100"
v-model="user.password"
required
:placeholder="$t('signup.password')"
/>
</div> </div>
<p v-if="passwordError" class="help is-danger">{{passwordError}}</p> <p v-if="passwordError" class="help is-danger">{{ passwordError }}</p>
</div> </div>
<div class="field"> <div class="field">
<div class="control"> <div class="control">

View File

@@ -1,79 +1,93 @@
import { useAuthStore } from './auth.store.js' import { useAuthStore } from './auth.store.js'
export function getInventaireImagePathOrDefault(path) { export function getInventaireImagePathOrDefault(path) {
return getImagePathOrGivenDefault(path, "../../image/defaultinventairebook.png") return getImagePathOrGivenDefault(path, '../../image/defaultinventairebook.png')
} }
export function getImagePathOrDefault(path) { export function getImagePathOrDefault(path) {
return getImagePathOrGivenDefault(path, "../image/defaultbook.png") return getImagePathOrGivenDefault(path, '../image/defaultbook.png')
} }
export function getImagePathOrGivenDefault(path, defaultpath) { export function getImagePathOrGivenDefault(path, defaultpath) {
if (path == "" || typeof path === 'undefined') { if (path == '' || typeof path === 'undefined') {
return defaultpath; return defaultpath
} else if (path.startsWith("https://")) { } else if (path.startsWith('https://')) {
return path; return path
} else { } else {
return path; return path
} }
} }
function useFetch(data, error, url) { function useFetch(data, error, url) {
const { user } = useAuthStore(); const { user } = useAuthStore()
if (user != null) { if (user != null) {
fetch(url, { fetch(url, {
method: 'GET', method: 'GET',
headers: { headers: {
'Authorization': 'Bearer ' + user.token Authorization: 'Bearer ' + user.token,
} },
}) })
.then((res) => { .then((res) => {
if (res.status === 401) { if (res.status === 401) {
const authStore = useAuthStore(); const authStore = useAuthStore()
authStore.logout(); authStore.logout()
} }
return res.json(); return res.json()
}) })
.then((json) => (data.value = json)) .then((json) => (data.value = json))
.catch((err) => (error.value = err)); .catch((err) => (error.value = err))
} }
} }
export async function getAppInfo(appInfo, appInfoErr) { export async function getAppInfo(appInfo, appInfoErr) {
return fetch('/ws/appinfo', { return fetch('/ws/appinfo', {
method: 'GET' method: 'GET',
}).then((res) => res.json()) })
.then((json) => appInfo.value = json) .then((res) => res.json())
.catch((err) => (appInfoErr.value = err)) .then((json) => (appInfo.value = json))
.catch((err) => (appInfoErr.value = err))
} }
export function getMyBooks(data, error, arg, limit, offset) { export function getMyBooks(data, error, arg, limit, offset) {
const queryParams = new URLSearchParams({limit: limit, offset: offset}); const queryParams = new URLSearchParams({ limit: limit, offset: offset })
return useFetch(data, error, '/ws/mybooks/' + arg + "?" + queryParams.toString()); return useFetch(data, error, '/ws/mybooks/' + arg + '?' + queryParams.toString())
} }
export function getSearchBooks(data, error, searchterm, lang, searchInventaire, limit, offset) { export function getSearchBooks(data, error, searchterm, lang, searchInventaire, limit, offset) {
const queryParams = new URLSearchParams({lang: lang, inventaire: searchInventaire, limit: limit, offset: offset}); const queryParams = new URLSearchParams({
return useFetch(data, error, '/ws/search/' + encodeURIComponent(searchterm) + "?" + queryParams.toString()); lang: lang,
inventaire: searchInventaire,
limit: limit,
offset: offset,
})
return useFetch(
data,
error,
'/ws/search/' + encodeURIComponent(searchterm) + '?' + queryParams.toString(),
)
} }
export function getInventaireEditionBooks(data, error, inventaireId, lang, limit, offset) { export function getInventaireEditionBooks(data, error, inventaireId, lang, limit, offset) {
const queryParams = new URLSearchParams({lang: lang, limit: limit, offset: offset}); const queryParams = new URLSearchParams({ lang: lang, limit: limit, offset: offset })
return useFetch(data, error, '/ws/inventaire/books/' + encodeURIComponent(inventaireId) + "?" + queryParams.toString()); return useFetch(
data,
error,
'/ws/inventaire/books/' + encodeURIComponent(inventaireId) + '?' + queryParams.toString(),
)
} }
export function getAuthor(data, error, id) { export function getAuthor(data, error, id) {
return useFetch(data, error, '/ws/author/' + id); return useFetch(data, error, '/ws/author/' + id)
} }
export function getAuthorBooks(data, error, id, limit, offset) { export function getAuthorBooks(data, error, id, limit, offset) {
const queryParams = new URLSearchParams({limit: limit, offset: offset}); const queryParams = new URLSearchParams({ limit: limit, offset: offset })
return useFetch(data, error, '/ws/author/' + id + "/books" + "?" + queryParams.toString()); return useFetch(data, error, '/ws/author/' + id + '/books' + '?' + queryParams.toString())
} }
export function getBook(data, error, id) { export function getBook(data, error, id) {
return useFetch(data, error, '/ws/book/' + id); return useFetch(data, error, '/ws/book/' + id)
} }
export function postBook(book) { export function postBook(book) {
@@ -81,39 +95,39 @@ export function postBook(book) {
} }
export async function postImportBook(id, language) { export async function postImportBook(id, language) {
return genericPayloadCall('/ws/importbook', {inventaireid: id, lang: language}, 'POST'); return genericPayloadCall('/ws/importbook', { inventaireid: id, lang: language }, 'POST')
} }
export async function putReadBook(bookId) { export async function putReadBook(bookId) {
return genericPayloadCall('/ws/book/' + bookId + "/read", {read: true}, 'PUT') return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true }, 'PUT')
} }
export async function putUnreadBook(bookId) { export async function putUnreadBook(bookId) {
return genericPayloadCall('/ws/book/' + bookId + "/read", {read: false}, 'PUT') return genericPayloadCall('/ws/book/' + bookId + '/read', { read: false }, 'PUT')
} }
export async function putEndReadDate(bookId, enddate) { export async function putEndReadDate(bookId, enddate) {
return genericPayloadCall('/ws/book/' + bookId + "/read", {read: true, endDate: enddate}, 'PUT') return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: enddate }, 'PUT')
} }
export async function putEndReadDateUnset(bookId) { export async function putEndReadDateUnset(bookId) {
return genericPayloadCall('/ws/book/' + bookId + "/read", {read: true, endDate: "null"}, 'PUT') return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: 'null' }, 'PUT')
} }
export async function putStartReadDateUnset(bookId) { export async function putStartReadDateUnset(bookId) {
return genericPayloadCall('/ws/book/' + bookId + "/startread", {startDate: "null"}, 'PUT') return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: 'null' }, 'PUT')
} }
export async function putStartReadDate(bookId, startdate) { export async function putStartReadDate(bookId, startdate) {
return genericPayloadCall('/ws/book/' + bookId + "/startread", {startDate: startdate}, 'PUT') return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: startdate }, 'PUT')
} }
export async function putWantReadBook(bookId, payload) { export async function putWantReadBook(bookId, payload) {
return genericPayloadCall('/ws/book/' + bookId + "/wantread", payload, 'PUT') return genericPayloadCall('/ws/book/' + bookId + '/wantread', payload, 'PUT')
} }
export async function putRateBook(bookId, payload) { export async function putRateBook(bookId, payload) {
return genericPayloadCall('/ws/book/' + bookId + "/rate", payload, 'PUT') return genericPayloadCall('/ws/book/' + bookId + '/rate', payload, 'PUT')
} }
export function postLogin(user) { export function postLogin(user) {
@@ -125,19 +139,19 @@ export function postSignUp(user) {
} }
export function postImage(file) { export function postImage(file) {
const { user } = useAuthStore(); const { user } = useAuthStore()
const formData = new FormData(); const formData = new FormData()
formData.append('file', file); formData.append('file', file)
if (user != null) { if (user != null) {
return fetch("/ws/upload/cover", { return fetch('/ws/upload/cover', {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': 'Bearer ' + user.token Authorization: 'Bearer ' + user.token,
}, },
body: formData body: formData,
}) })
} else { } else {
return Promise.resolve(); return Promise.resolve()
} }
} }
@@ -147,48 +161,47 @@ export function genericPostCallNoAuth(apiRoute, object) {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(object) body: JSON.stringify(object),
}) })
} }
export function genericPayloadCall(apiRoute, object, method) { export function genericPayloadCall(apiRoute, object, method) {
const { user } = useAuthStore(); const { user } = useAuthStore()
if (user != null) { if (user != null) {
return fetch(apiRoute, { return fetch(apiRoute, {
method: method, method: method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + user.token Authorization: 'Bearer ' + user.token,
}, },
body: JSON.stringify(object) body: JSON.stringify(object),
}) })
} } else {
else { return Promise.resolve()
return Promise.resolve();
} }
} }
export function extractFormErrorFromField(fieldName, errors) { export function extractFormErrorFromField(fieldName, errors) {
if (errors === null) { if (errors === null) {
return ""; return ''
} }
if (errors.value == null) { if (errors.value == null) {
return ""; return ''
} }
console.log(errors.value); console.log(errors.value)
const titleErr = errors.find((e) => e["field"] === fieldName); const titleErr = errors.find((e) => e['field'] === fieldName)
if (typeof titleErr !== 'undefined') { if (typeof titleErr !== 'undefined') {
return titleErr.error; return titleErr.error
} else { } else {
return ""; return ''
} }
} }
export function extractGlobalFormError(errors) { export function extractGlobalFormError(errors) {
if (errors !== null && "error" in errors) { if (errors !== null && 'error' in errors) {
return errors["error"]; return errors['error']
} else { } else {
return ""; return ''
} }
} }

View File

@@ -1,20 +1,19 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', { export const useAuthStore = defineStore('auth', {
state: () => ({ state: () => ({
// initialize state from local storage to enable user to stay logged in // initialize state from local storage to enable user to stay logged in
user: JSON.parse(localStorage.getItem('user')), user: JSON.parse(localStorage.getItem('user')),
returnUrl: null returnUrl: null,
}), }),
actions: { actions: {
async login(user) { async login(user) {
this.user = user; this.user = user
localStorage.setItem('user', JSON.stringify(user)); localStorage.setItem('user', JSON.stringify(user))
}, },
logout() { logout() {
this.user = null; this.user = null
localStorage.removeItem('user'); localStorage.removeItem('user')
} },
} },
}); })

View File

@@ -16,22 +16,22 @@
"barcode": "Scan barcode" "barcode": "Scan barcode"
}, },
"addbook": { "addbook": {
"title":"Title", "title": "Title",
"author":"Author", "author": "Author",
"submit":"Submit", "submit": "Submit",
"coverupload":"Upload cover" "coverupload": "Upload cover"
}, },
"signup": { "signup": {
"title":"Sign up", "title": "Sign up",
"username":"Username", "username": "Username",
"password":"Password", "password": "Password",
"signup":"Sign up" "signup": "Sign up"
}, },
"login": { "login": {
"title":"Log in", "title": "Log in",
"username":"Username", "username": "Username",
"password":"Password", "password": "Password",
"login":"Log in" "login": "Log in"
}, },
"bookbrowser": { "bookbrowser": {
"error": "Error when loading books: {error}", "error": "Error when loading books: {error}",
@@ -63,22 +63,21 @@
"wantread": "Interested" "wantread": "Interested"
}, },
"pagination": { "pagination": {
"previous":"Previous", "previous": "Previous",
"next":"Next", "next": "Next",
"goto":"Goto page {pageNumber}", "goto": "Goto page {pageNumber}",
"page":"Page {pageNumber}" "page": "Page {pageNumber}"
}, },
"bookdatewidget": { "bookdatewidget": {
"started": "Started at :", "started": "Started at :",
"finished": "Finished at :" "finished": "Finished at :"
}, },
"importinventaire": { "importinventaire": {
"title":"Please select a book to import" "title": "Please select a book to import"
}, },
"importlistelement": { "importlistelement": {
"releasedate":"Release date:", "releasedate": "Release date:",
"publisher":"Publisher:", "publisher": "Publisher:",
"importing":"Importing..." "importing": "Importing..."
} }
} }

View File

@@ -8,7 +8,7 @@
"addbook": "Ajouter Un Livre", "addbook": "Ajouter Un Livre",
"logout": "Se déconnecter", "logout": "Se déconnecter",
"signup": "S'inscrire", "signup": "S'inscrire",
"search":"Rechercher", "search": "Rechercher",
"login": "Se connecter" "login": "Se connecter"
}, },
"barcode": { "barcode": {
@@ -16,22 +16,22 @@
"barcode": "Scanner le code-barres" "barcode": "Scanner le code-barres"
}, },
"addbook": { "addbook": {
"title":"Titre", "title": "Titre",
"author":"Auteur", "author": "Auteur",
"submit":"Confirmer", "submit": "Confirmer",
"coverupload":"Téléverser la couverture" "coverupload": "Téléverser la couverture"
}, },
"signup": { "signup": {
"title":"Inscription", "title": "Inscription",
"username":"Nom d'utilisateur", "username": "Nom d'utilisateur",
"password":"Mot de passe", "password": "Mot de passe",
"signup":"S'inscrire" "signup": "S'inscrire"
}, },
"login": { "login": {
"title":"Connexion", "title": "Connexion",
"username":"Nom d'utilisateur", "username": "Nom d'utilisateur",
"password":"Mot de passe", "password": "Mot de passe",
"login":"Se connecter" "login": "Se connecter"
}, },
"bookbrowser": { "bookbrowser": {
"error": "Erreur pendant le chargement des livres: {error}", "error": "Erreur pendant le chargement des livres: {error}",
@@ -63,21 +63,21 @@
"wantread": "À lire" "wantread": "À lire"
}, },
"pagination": { "pagination": {
"previous":"Précédent", "previous": "Précédent",
"next":"Suivant", "next": "Suivant",
"goto":"Aller à la page {pageNumber}", "goto": "Aller à la page {pageNumber}",
"page":"Page {pageNumber}" "page": "Page {pageNumber}"
}, },
"bookdatewidget": { "bookdatewidget": {
"started": "Commencé le :", "started": "Commencé le :",
"finished": "Fini le :" "finished": "Fini le :"
}, },
"importinventaire": { "importinventaire": {
"title":"Sélectionner l'édition à importer" "title": "Sélectionner l'édition à importer"
}, },
"importlistelement": { "importlistelement": {
"releasedate":"Date de publication : ", "releasedate": "Date de publication : ",
"publisher":"Maison d'édition : ", "publisher": "Maison d'édition : ",
"importing":"Import en cours..." "importing": "Import en cours..."
} }
} }

View File

@@ -1,26 +1,24 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createI18n } from "vue-i18n"; import { createI18n } from 'vue-i18n'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { BootstrapIconsPlugin } from "bootstrap-icons-vue"; import { BootstrapIconsPlugin } from 'bootstrap-icons-vue'
import { router } from './router.js'; import { router } from './router.js'
import { createVuetify } from 'vuetify' import { createVuetify } from 'vuetify'
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg' import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
import { VRating } from 'vuetify/components/VRating'; import { VRating } from 'vuetify/components/VRating'
import App from './App.vue' import App from './App.vue'
import './styles/global.css'; import './styles/global.css'
import fr from './locales/fr.json';
import en from './locales/en.json';
import fr from './locales/fr.json'
import en from './locales/en.json'
// configure i18n // configure i18n
const i18n = createI18n({ const i18n = createI18n({
locale: navigator.language, locale: navigator.language,
fallbackLocale: "en", fallbackLocale: 'en',
messages: { fr, en }, messages: { fr, en },
}); })
const vuetify = createVuetify({ const vuetify = createVuetify({
VRating, VRating,
@@ -35,5 +33,4 @@ const vuetify = createVuetify({
const pinia = createPinia() const pinia = createPinia()
createApp(App).use(i18n).use(vuetify).use(pinia).use(BootstrapIconsPlugin).use(router).mount('#app') createApp(App).use(i18n).use(vuetify).use(pinia).use(BootstrapIconsPlugin).use(router).mount('#app')

View File

@@ -31,13 +31,13 @@ export const router = createRouter({
}) })
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
// redirect to login page if not logged in and trying to access a restricted page // redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/', '/login', '/signup']; const publicPages = ['/', '/login', '/signup']
const authRequired = !publicPages.includes(to.path); const authRequired = !publicPages.includes(to.path)
const auth = useAuthStore(); const auth = useAuthStore()
if (authRequired && !auth.user) { if (authRequired && !auth.user) {
auth.returnUrl = to.fullPath; auth.returnUrl = to.fullPath
return '/login'; return '/login'
} }
}); })

View File

@@ -1,3 +1,3 @@
.clickable { .clickable {
cursor:pointer; cursor: pointer;
} }

View File

@@ -6,13 +6,10 @@ import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [vue(), vueDevTools()],
vue(),
vueDevTools(),
],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
}) })