Add prettier dependency to format frontend code
This commit is contained in:
1
front/.prettierignore
Normal file
1
front/.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
public
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">…</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">…</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>
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
133
front/src/api.js
133
front/src/api.js
@@ -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 ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -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..."
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.clickable {
|
.clickable {
|
||||||
cursor:pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user