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="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="/css/bulma.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/bulma.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Personal Library Manager</title>
</head>
<body>

View File

@@ -1,60 +1,69 @@
<script setup>
import { ref, reactive, computed } from 'vue'
import { postBook, extractFormErrorFromField } from './api.js'
import { useRouter } from 'vue-router'
import CoverUpload from './CoverUpload.vue'
import { ref, reactive, computed } from 'vue'
import { postBook, extractFormErrorFromField } from './api.js'
import { useRouter } from 'vue-router'
import CoverUpload from './CoverUpload.vue'
const router = useRouter();
const router = useRouter()
const book = ref({
title: "",
author: "",
coverId: null
});
const errors = ref(null)
const titleError = computed(() => {
return extractFormErrorFromField("Title", errors.value);
const book = ref({
title: '',
author: '',
coverId: null,
})
const errors = ref(null)
const titleError = computed(() => {
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>
<template>
<form @submit.prevent="onSubmit">
<div class="field">
<label class="label">{{$t('addbook.title')}}</label>
<label class="label">{{ $t('addbook.title') }}</label>
<div class="control">
<input :class="'input ' + (titleError ? 'is-danger' : '')" type="text" maxlength="300"
required v-model="book.title" :placeholder="$t('addbook.title')">
<input
:class="'input ' + (titleError ? 'is-danger' : '')"
type="text"
maxlength="300"
required
v-model="book.title"
:placeholder="$t('addbook.title')"
/>
</div>
<p v-if="titleError" class="help is-danger">{{titleError}}</p>
<p v-if="titleError" class="help is-danger">{{ titleError }}</p>
</div>
<div class="field">
<label class="label">{{$t('addbook.author')}}</label>
<label class="label">{{ $t('addbook.author') }}</label>
<div class="control">
<input :class="'input ' + (authorError ? 'is-danger' : '')" type="text" maxlength="100"
v-model="book.author" :placeholder="$t('addbook.author')">
<input
:class="'input ' + (authorError ? 'is-danger' : '')"
type="text"
maxlength="100"
v-model="book.author"
:placeholder="$t('addbook.author')"
/>
</div>
<p v-if="authorError" class="help is-danger">{{authorError}}</p>
<p v-if="authorError" class="help is-danger">{{ authorError }}</p>
</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="control">
<button class="button is-link">{{$t('addbook.submit')}}</button>
<button class="button is-link">{{ $t('addbook.submit') }}</button>
</div>
</div>
</form>

View File

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

View File

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

View File

@@ -1,34 +1,32 @@
<script setup>
import { ref } from 'vue'
import { getAuthor } from './api.js'
import { onBeforeRouteUpdate } from 'vue-router'
import SearchBook from './SearchBook.vue'
import { ref } from 'vue'
import { getAuthor } from './api.js'
import { onBeforeRouteUpdate } from 'vue-router'
import SearchBook from './SearchBook.vue'
const props = defineProps({
id: String,
})
const props = defineProps({
id: String
});
let author = ref(null)
let authorfetcherror = ref(null)
let author = ref(null);
let authorfetcherror = ref(null);
getAuthor(author, authorfetcherror, props.id)
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>
<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">
<h3 class="title">{{author.name}}</h3>
<p v-if="author.description">{{author.description}}</p>
<h3 class="title">{{ author.name }}</h3>
<p v-if="author.description">{{ author.description }}</p>
</div>
<div class="mt-6">
<SearchBook :author-id="id"/>
<SearchBook :author-id="id" />
</div>
</template>
<style scoped>
</style>
<style scoped></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,24 @@
import { createApp } from 'vue'
import { createI18n } from "vue-i18n";
import { createI18n } from 'vue-i18n'
import { createPinia } from 'pinia'
import { BootstrapIconsPlugin } from "bootstrap-icons-vue";
import { router } from './router.js';
import { BootstrapIconsPlugin } from 'bootstrap-icons-vue'
import { router } from './router.js'
import { createVuetify } from 'vuetify'
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 './styles/global.css';
import fr from './locales/fr.json';
import en from './locales/en.json';
import './styles/global.css'
import fr from './locales/fr.json'
import en from './locales/en.json'
// configure i18n
const i18n = createI18n({
locale: navigator.language,
fallbackLocale: "en",
messages: { fr, en },
});
locale: navigator.language,
fallbackLocale: 'en',
messages: { fr, en },
})
const vuetify = createVuetify({
VRating,
@@ -35,5 +33,4 @@ const vuetify = createVuetify({
const pinia = createPinia()
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) => {
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/', '/login', '/signup'];
const authRequired = !publicPages.includes(to.path);
const auth = useAuthStore();
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/', '/login', '/signup']
const authRequired = !publicPages.includes(to.path)
const auth = useAuthStore()
if (authRequired && !auth.user) {
auth.returnUrl = to.fullPath;
return '/login';
}
});
if (authRequired && !auth.user) {
auth.returnUrl = to.fullPath
return '/login'
}
})

View File

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

View File

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