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="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="stylesheet" href="/css/bulma.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/css/bulma.min.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Personal Library Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,60 +1,69 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { postBook, extractFormErrorFromField } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CoverUpload from './CoverUpload.vue'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { postBook, extractFormErrorFromField } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CoverUpload from './CoverUpload.vue'
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
const book = ref({
|
||||
title: "",
|
||||
author: "",
|
||||
coverId: null
|
||||
});
|
||||
const errors = ref(null)
|
||||
const titleError = computed(() => {
|
||||
return extractFormErrorFromField("Title", errors.value);
|
||||
const book = ref({
|
||||
title: '',
|
||||
author: '',
|
||||
coverId: null,
|
||||
})
|
||||
const errors = ref(null)
|
||||
const titleError = computed(() => {
|
||||
return extractFormErrorFromField('Title', errors.value)
|
||||
})
|
||||
const authorError = computed(() => {
|
||||
return extractFormErrorFromField('Author', errors.value)
|
||||
})
|
||||
|
||||
function onSubmit(e) {
|
||||
postBook(book).then((res) => {
|
||||
if (res.ok) {
|
||||
router.push('/')
|
||||
return
|
||||
} else {
|
||||
res.json().then((json) => (errors.value = json))
|
||||
}
|
||||
})
|
||||
const authorError = computed(() => {
|
||||
return extractFormErrorFromField("Author", errors.value);
|
||||
})
|
||||
|
||||
function onSubmit(e) {
|
||||
postBook(book)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
router.push('/');
|
||||
return;
|
||||
} else {
|
||||
res.json().then((json) => (errors.value = json));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div class="field">
|
||||
<label class="label">{{$t('addbook.title')}}</label>
|
||||
<label class="label">{{ $t('addbook.title') }}</label>
|
||||
<div class="control">
|
||||
<input :class="'input ' + (titleError ? 'is-danger' : '')" type="text" maxlength="300"
|
||||
required v-model="book.title" :placeholder="$t('addbook.title')">
|
||||
<input
|
||||
:class="'input ' + (titleError ? 'is-danger' : '')"
|
||||
type="text"
|
||||
maxlength="300"
|
||||
required
|
||||
v-model="book.title"
|
||||
:placeholder="$t('addbook.title')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="titleError" class="help is-danger">{{titleError}}</p>
|
||||
<p v-if="titleError" class="help is-danger">{{ titleError }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{$t('addbook.author')}}</label>
|
||||
<label class="label">{{ $t('addbook.author') }}</label>
|
||||
<div class="control">
|
||||
<input :class="'input ' + (authorError ? 'is-danger' : '')" type="text" maxlength="100"
|
||||
v-model="book.author" :placeholder="$t('addbook.author')">
|
||||
<input
|
||||
:class="'input ' + (authorError ? 'is-danger' : '')"
|
||||
type="text"
|
||||
maxlength="100"
|
||||
v-model="book.author"
|
||||
:placeholder="$t('addbook.author')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="authorError" class="help is-danger">{{authorError}}</p>
|
||||
<p v-if="authorError" class="help is-danger">{{ authorError }}</p>
|
||||
</div>
|
||||
<CoverUpload name="cover" @on-image-upload="(id) => book.coverId = id"/>
|
||||
<CoverUpload name="cover" @on-image-upload="(id) => (book.coverId = id)" />
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-link">{{$t('addbook.submit')}}</button>
|
||||
<button class="button is-link">{{ $t('addbook.submit') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
import AppNavBar from './AppNavBar.vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import AppNavBar from './AppNavBar.vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<AppNavBar/>
|
||||
<AppNavBar />
|
||||
</header>
|
||||
<main class="section">
|
||||
<RouterView />
|
||||
|
||||
@@ -1,85 +1,96 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
import NavBarSearch from './NavBarSearch.vue'
|
||||
import BarcodeModal from './BarcodeModal.vue'
|
||||
import { useAuthStore } from './auth.store.js'
|
||||
import { getAppInfo, postLogin } from './api.js'
|
||||
import { onMounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
import NavBarSearch from './NavBarSearch.vue'
|
||||
import BarcodeModal from './BarcodeModal.vue'
|
||||
import { useAuthStore } from './auth.store.js'
|
||||
import { getAppInfo, postLogin } from './api.js'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const isMenuActive = ref(false)
|
||||
const isSearchBarShown = ref(false)
|
||||
const isMenuActive = ref(false)
|
||||
const isSearchBarShown = ref(false)
|
||||
|
||||
function logout() {
|
||||
authStore.logout();
|
||||
router.push('/');
|
||||
function logout() {
|
||||
authStore.logout()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const appInfo = ref(null)
|
||||
const appInfoErr = ref(null)
|
||||
|
||||
async function logInIfDemoMode(demoMode, demoUsername) {
|
||||
if (!demoMode) {
|
||||
return
|
||||
}
|
||||
|
||||
const appInfo = ref(null);
|
||||
const appInfoErr = ref(null);
|
||||
|
||||
async function logInIfDemoMode(demoMode, demoUsername) {
|
||||
if (!demoMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const demouser = ref({
|
||||
username: demoUsername,
|
||||
password: ""
|
||||
});
|
||||
const res = await postLogin(demouser)
|
||||
const json = await res.json();
|
||||
await useAuthStore().login({username: demouser.value.username, token: json["token"]})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getAppInfo(appInfo, appInfoErr)
|
||||
.then(() => logInIfDemoMode(appInfo.value.demoMode, appInfo.value.demoUsername));
|
||||
const demouser = ref({
|
||||
username: demoUsername,
|
||||
password: '',
|
||||
})
|
||||
const res = await postLogin(demouser)
|
||||
const json = await res.json()
|
||||
await useAuthStore().login({ username: demouser.value.username, token: json['token'] })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getAppInfo(appInfo, appInfoErr).then(() =>
|
||||
logInIfDemoMode(appInfo.value.demoMode, appInfo.value.demoUsername),
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<RouterLink to="/" class="navbar-item" activeClass="is-active">
|
||||
PLM
|
||||
</RouterLink>
|
||||
<div class="navbar-item is-hidden-desktop">
|
||||
<a @click="isSearchBarShown = !isSearchBarShown"
|
||||
class="button is-medium" :class="isSearchBarShown ? 'is-active' : '' ">
|
||||
<span class="icon" :title="$t('navbar.search')">
|
||||
<b-icon-search />
|
||||
</span>
|
||||
</a>
|
||||
<BarcodeModal size-class="is-medium"/>
|
||||
</div>
|
||||
<a v-if="authStore.user" role="button" class="navbar-burger" aria-label="menu"
|
||||
:class="isMenuActive ? 'is-active' : '' " :aria-expanded="isMenuActive"
|
||||
@click="isMenuActive = !isMenuActive">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<RouterLink to="/" class="navbar-item" activeClass="is-active"> PLM </RouterLink>
|
||||
<div class="navbar-item is-hidden-desktop">
|
||||
<a
|
||||
@click="isSearchBarShown = !isSearchBarShown"
|
||||
class="button is-medium"
|
||||
:class="isSearchBarShown ? 'is-active' : ''"
|
||||
>
|
||||
<span class="icon" :title="$t('navbar.search')">
|
||||
<b-icon-search />
|
||||
</span>
|
||||
</a>
|
||||
<BarcodeModal size-class="is-medium" />
|
||||
</div>
|
||||
<a
|
||||
v-if="authStore.user"
|
||||
role="button"
|
||||
class="navbar-burger"
|
||||
aria-label="menu"
|
||||
:class="isMenuActive ? 'is-active' : ''"
|
||||
:aria-expanded="isMenuActive"
|
||||
@click="isMenuActive = !isMenuActive"
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="isSearchBarShown" class="is-hidden-desktop">
|
||||
<NavBarSearch @search-done="isSearchBarShown = false" size-class="is-medium" is-mobile/>
|
||||
<NavBarSearch @search-done="isSearchBarShown = false" size-class="is-medium" is-mobile />
|
||||
</div>
|
||||
<div id="navmenu" class="navbar-menu" :class="isMenuActive ? 'is-active' : '' ">
|
||||
<div id="navmenu" class="navbar-menu" :class="isMenuActive ? 'is-active' : ''">
|
||||
<div class="navbar-start">
|
||||
<NavBarSearch size-class="" class="is-hidden-touch"/>
|
||||
<NavBarSearch size-class="" class="is-hidden-touch" />
|
||||
<RouterLink v-if="authStore.user" to="/books" class="navbar-item" activeClass="is-active">
|
||||
{{ $t('navbar.mybooks')}}
|
||||
{{ $t('navbar.mybooks') }}
|
||||
</RouterLink>
|
||||
<RouterLink v-if="authStore.user" to="/add" class="navbar-item" activeClass="is-active">
|
||||
{{ $t('navbar.addbook')}}
|
||||
{{ $t('navbar.addbook') }}
|
||||
</RouterLink>
|
||||
<div v-if="authStore.user && appInfo && !appInfo.demoMode" class="navbar-item is-hidden-desktop">
|
||||
<div
|
||||
v-if="authStore.user && appInfo && !appInfo.demoMode"
|
||||
class="navbar-item is-hidden-desktop"
|
||||
>
|
||||
<a @click="logout">
|
||||
{{ $t('navbar.logout')}}
|
||||
{{ $t('navbar.logout') }}
|
||||
<span class="icon" :title="$t('navbar.logout')">
|
||||
<b-icon-power />
|
||||
</span>
|
||||
@@ -88,27 +99,30 @@
|
||||
</div>
|
||||
<div class="navbar-end is-hidden-touch">
|
||||
<div v-if="authStore.user" class="navbar-item">
|
||||
<div >
|
||||
<div>
|
||||
{{ authStore.user.username }}
|
||||
</div>
|
||||
<a v-if="appInfo && !appInfo.demoMode" @click="logout" class="button is-light">
|
||||
{{ $t('navbar.logout')}}
|
||||
{{ $t('navbar.logout') }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-else class="navbar-item">
|
||||
<div class="buttons">
|
||||
<RouterLink v-if="appInfo && !appInfo.registrationDisabled" to="/signup" class="button is-primary">
|
||||
<strong>{{ $t('navbar.signup')}}</strong>
|
||||
<RouterLink
|
||||
v-if="appInfo && !appInfo.registrationDisabled"
|
||||
to="/signup"
|
||||
class="button is-primary"
|
||||
>
|
||||
<strong>{{ $t('navbar.signup') }}</strong>
|
||||
</RouterLink>
|
||||
<RouterLink to="/login" class="button is-light">
|
||||
{{ $t('navbar.login')}}
|
||||
{{ $t('navbar.login') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { getAuthor } from './api.js'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
import SearchBook from './SearchBook.vue'
|
||||
import { ref } from 'vue'
|
||||
import { getAuthor } from './api.js'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
import SearchBook from './SearchBook.vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: String,
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
id: String
|
||||
});
|
||||
let author = ref(null)
|
||||
let authorfetcherror = ref(null)
|
||||
|
||||
let author = ref(null);
|
||||
let authorfetcherror = ref(null);
|
||||
getAuthor(author, authorfetcherror, props.id)
|
||||
|
||||
getAuthor(author, authorfetcherror, props.id);
|
||||
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
getAuthor(author, authorfetcherror, to.params.id);
|
||||
})
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
getAuthor(author, authorfetcherror, to.params.id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="authorfetcherror">{{$t('authorform.error', {err: authorfetcherror.message})}}</div>
|
||||
<div v-if="authorfetcherror">{{ $t('authorform.error', { err: authorfetcherror.message }) }}</div>
|
||||
<div v-if="author">
|
||||
<h3 class="title">{{author.name}}</h3>
|
||||
<p v-if="author.description">{{author.description}}</p>
|
||||
<h3 class="title">{{ author.name }}</h3>
|
||||
<p v-if="author.description">{{ author.description }}</p>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<SearchBook :author-id="id"/>
|
||||
<SearchBook :author-id="id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -4,14 +4,14 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
sizeClass: String
|
||||
});
|
||||
sizeClass: String,
|
||||
})
|
||||
|
||||
const open = ref(false);
|
||||
const router = useRouter();
|
||||
const open = ref(false)
|
||||
const router = useRouter()
|
||||
|
||||
function onBarcodeDecode(isbn) {
|
||||
open.value = false;
|
||||
open.value = false
|
||||
router.push('/search/' + isbn)
|
||||
}
|
||||
</script>
|
||||
@@ -29,38 +29,38 @@ function onBarcodeDecode(isbn) {
|
||||
<div v-if="open">
|
||||
<div @click="open = false" class="modal-backdrop"></div>
|
||||
<div class="modal has-background-dark">
|
||||
<ScanBook @read-barcode="onBarcodeDecode"/>
|
||||
<ScanBook @read-barcode="onBarcodeDecode" />
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
}
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal {
|
||||
height: 80%;
|
||||
width: 80%;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
box-shadow: 2px 2px 20px 1px;
|
||||
overflow-x: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 15px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal {
|
||||
height: 80%;
|
||||
width: 80%;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
box-shadow: 2px 2px 20px 1px;
|
||||
overflow-x: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 15px;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,52 +1,55 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
const props = defineProps({
|
||||
icon: String,
|
||||
legend: String,
|
||||
isSet: Boolean,
|
||||
isReadonly: Boolean
|
||||
});
|
||||
isReadonly: Boolean,
|
||||
})
|
||||
|
||||
const hovered = ref(false)
|
||||
const isOnMobile = ref(computeIsOnMobile())
|
||||
const hovered = ref(false)
|
||||
const isOnMobile = ref(computeIsOnMobile())
|
||||
|
||||
const computedIcon = computed(() => props.icon +
|
||||
(!props.isReadonly && ((hovered.value && !isOnMobile.value) || props.isSet) ? "Fill" : ""));
|
||||
const computedIcon = computed(
|
||||
() =>
|
||||
props.icon +
|
||||
(!props.isReadonly && ((hovered.value && !isOnMobile.value) || props.isSet) ? 'Fill' : ''),
|
||||
)
|
||||
|
||||
function computeIsOnMobile() {
|
||||
return window.innerWidth < 1024;
|
||||
}
|
||||
|
||||
function updateOnMobile() {
|
||||
isOnMobile.value = computeIsOnMobile();
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", updateOnMobile);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", updateOnMobile);
|
||||
});
|
||||
function computeIsOnMobile() {
|
||||
return window.innerWidth < 1024
|
||||
}
|
||||
|
||||
function updateOnMobile() {
|
||||
isOnMobile.value = computeIsOnMobile()
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updateOnMobile)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateOnMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bigiconandlegend"
|
||||
:class="props.isReadonly ? '' : 'showcanclick'"
|
||||
@mouseover="hovered = true"
|
||||
@mouseout="hovered = false">
|
||||
<div
|
||||
class="bigiconandlegend"
|
||||
:class="props.isReadonly ? '' : 'showcanclick'"
|
||||
@mouseover="hovered = true"
|
||||
@mouseout="hovered = false"
|
||||
>
|
||||
<span class="bigicon" :title="props.legend">
|
||||
<component :is="computedIcon"></component>
|
||||
</span>
|
||||
<div class="bigiconlegend">{{props.legend}}</div>
|
||||
<div class="bigiconlegend">{{ props.legend }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.bigiconandlegend {
|
||||
border-radius:30px;
|
||||
margin:25px;
|
||||
border-radius: 30px;
|
||||
margin: 25px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@@ -59,21 +62,21 @@
|
||||
|
||||
.bigicon {
|
||||
display: flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 90px;
|
||||
width: 200px;
|
||||
font-size: 78px;
|
||||
padding-top:20px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.bigiconlegend {
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 34px;
|
||||
width: 200px;
|
||||
padding-bottom:30px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
@@ -85,18 +88,18 @@
|
||||
}
|
||||
|
||||
.bigiconlegend {
|
||||
flex:1;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
padding-bottom:0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.bigiconandlegend {
|
||||
display: flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-flow: column;
|
||||
margin:0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getImagePathOrDefault } from './api.js'
|
||||
import { VRating } from 'vuetify/components/VRating';
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getImagePathOrDefault } from './api.js'
|
||||
import { VRating } from 'vuetify/components/VRating'
|
||||
|
||||
const props = defineProps({
|
||||
const props = defineProps({
|
||||
id: Number,
|
||||
title: String,
|
||||
author: String,
|
||||
coverPath: String,
|
||||
rating: Number,
|
||||
read: Boolean
|
||||
});
|
||||
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath));
|
||||
const router = useRouter();
|
||||
read: Boolean,
|
||||
})
|
||||
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath))
|
||||
const router = useRouter()
|
||||
|
||||
function openBook() {
|
||||
router.push(`/book/${props.id}`)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="box container has-background-dark" >
|
||||
<div class="box container has-background-dark">
|
||||
<div class="media" @click="openBook">
|
||||
<div class="media-left">
|
||||
<figure class="image mb-3">
|
||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="title">
|
||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="title" />
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<div class="is-size-5">{{title}}</div>
|
||||
<div class="is-size-5 is-italic">{{author}}</div>
|
||||
<VRating v-if="rating > 0"
|
||||
half-increments
|
||||
readonly
|
||||
:length="5"
|
||||
size="medium"
|
||||
:model-value="rating/2"
|
||||
active-color="bulma-body-color"
|
||||
/>
|
||||
<div class="is-size-5">{{ title }}</div>
|
||||
<div class="is-size-5 is-italic">{{ author }}</div>
|
||||
<VRating
|
||||
v-if="rating > 0"
|
||||
half-increments
|
||||
readonly
|
||||
:length="5"
|
||||
size="medium"
|
||||
:model-value="rating / 2"
|
||||
active-color="bulma-body-color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,14 +49,14 @@ function openBook() {
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
max-height:100px;
|
||||
max-width:100px;
|
||||
height:auto;
|
||||
width:auto;
|
||||
max-height: 100px;
|
||||
max-width: 100px;
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.box {
|
||||
transition:ease-in-out 0.04s;
|
||||
transition: ease-in-out 0.04s;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@@ -64,5 +64,4 @@ img {
|
||||
transform: scale(1.01);
|
||||
transition: ease-in-out 0.02s;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import BigIcon from './BigIcon.vue';
|
||||
import { ref } from 'vue'
|
||||
import BigIcon from './BigIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
const props = defineProps({
|
||||
icon: String,
|
||||
legend: String,
|
||||
startReadDate: String,
|
||||
@@ -10,58 +10,63 @@
|
||||
isExpanded: Boolean,
|
||||
isReadonly: Boolean,
|
||||
useEndDate: Boolean,
|
||||
lastWidget: Boolean
|
||||
});
|
||||
defineEmits(['onIconClick', 'onStartDateChange', 'onEndDateChange'])
|
||||
lastWidget: Boolean,
|
||||
})
|
||||
defineEmits(['onIconClick', 'onStartDateChange', 'onEndDateChange'])
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
|
||||
function computeParentClasses() {
|
||||
let classNames = "bookdatewidget";
|
||||
if (props.isExpanded) {
|
||||
classNames += " has-text-dark has-background-text";
|
||||
}
|
||||
if (props.lastWidget) {
|
||||
classNames += " border-radius-right-and-left";
|
||||
} else {
|
||||
classNames += " border-radius-right";
|
||||
}
|
||||
if (props.isReadonly) {
|
||||
classNames += " widget-readonly"
|
||||
}
|
||||
return classNames;
|
||||
function computeParentClasses() {
|
||||
let classNames = 'bookdatewidget'
|
||||
if (props.isExpanded) {
|
||||
classNames += ' has-text-dark has-background-text'
|
||||
}
|
||||
|
||||
if (props.lastWidget) {
|
||||
classNames += ' border-radius-right-and-left'
|
||||
} else {
|
||||
classNames += ' border-radius-right'
|
||||
}
|
||||
if (props.isReadonly) {
|
||||
classNames += ' widget-readonly'
|
||||
}
|
||||
return classNames
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="computeParentClasses()">
|
||||
<BigIcon :icon="props.icon"
|
||||
:is-readonly="props.isReadonly"
|
||||
:legend="props.legend"
|
||||
:isSet="props.isExpanded"
|
||||
@click="props.isReadonly ? null : $emit('onIconClick') "/>
|
||||
<BigIcon
|
||||
:icon="props.icon"
|
||||
:is-readonly="props.isReadonly"
|
||||
:legend="props.legend"
|
||||
:isSet="props.isExpanded"
|
||||
@click="props.isReadonly ? null : $emit('onIconClick')"
|
||||
/>
|
||||
<div v-if="props.isExpanded" class="inputdate">
|
||||
<div class="ontopofinput">
|
||||
<label class="datelabel" for="startread">
|
||||
{{$t('bookdatewidget.started')}}
|
||||
{{ $t('bookdatewidget.started') }}
|
||||
</label>
|
||||
<input class="datepicker has-background-dark has-text-light"
|
||||
id="startread"
|
||||
type="date"
|
||||
@change="(e) => $emit('onStartDateChange', e.target.value)"
|
||||
:value="props.startReadDate"
|
||||
:max="today"/>
|
||||
<div v-if="props.useEndDate">
|
||||
<input
|
||||
class="datepicker has-background-dark has-text-light"
|
||||
id="startread"
|
||||
type="date"
|
||||
@change="(e) => $emit('onStartDateChange', e.target.value)"
|
||||
:value="props.startReadDate"
|
||||
:max="today"
|
||||
/>
|
||||
<div v-if="props.useEndDate">
|
||||
<label class="datelabel" for="endread">
|
||||
{{$t('bookdatewidget.finished')}}
|
||||
{{ $t('bookdatewidget.finished') }}
|
||||
</label>
|
||||
<input class="datepicker has-background-dark has-text-light"
|
||||
id="endread"
|
||||
type="date"
|
||||
@change="(e) => $emit('onEndDateChange', e.target.value)"
|
||||
:value="props.endReadDate"
|
||||
:max="today"/>
|
||||
<input
|
||||
class="datepicker has-background-dark has-text-light"
|
||||
id="endread"
|
||||
type="date"
|
||||
@change="(e) => $emit('onEndDateChange', e.target.value)"
|
||||
:value="props.endReadDate"
|
||||
:max="today"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,11 +74,10 @@
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.inputdate {
|
||||
display: flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -91,8 +95,8 @@
|
||||
|
||||
.datelabel {
|
||||
display: flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 26px;
|
||||
border: none;
|
||||
padding-bottom: 15px;
|
||||
|
||||
@@ -1,148 +1,159 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
getBook,
|
||||
getImagePathOrDefault,
|
||||
putReadBook,
|
||||
putWantReadBook,
|
||||
putRateBook,
|
||||
putStartReadDate,
|
||||
putStartReadDateUnset,
|
||||
putEndReadDate,
|
||||
putEndReadDateUnset,
|
||||
putUnreadBook,
|
||||
} from './api.js'
|
||||
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
|
||||
import { VRating } from 'vuetify/components/VRating'
|
||||
import BigIcon from './BigIcon.vue'
|
||||
import BookDateWidget from './BookDateWidget.vue'
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { getBook, getImagePathOrDefault, putReadBook, putWantReadBook, putRateBook,
|
||||
putStartReadDate, putStartReadDateUnset, putEndReadDate, putEndReadDateUnset, putUnreadBook } from './api.js'
|
||||
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
|
||||
import { VRating } from 'vuetify/components/VRating';
|
||||
import BigIcon from './BigIcon.vue';
|
||||
import BookDateWidget from './BookDateWidget.vue';
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
id: String,
|
||||
})
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
|
||||
let data = ref(null)
|
||||
let error = ref(null)
|
||||
getBook(data, error, props.id)
|
||||
const imagePathOrDefault = computed(() => getImagePathOrDefault(data.value.coverPath))
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
id: String
|
||||
});
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
getBook(data, error, to.params.id)
|
||||
})
|
||||
|
||||
let data = ref(null);
|
||||
let error = ref(null);
|
||||
getBook(data, error, props.id);
|
||||
const imagePathOrDefault = computed(() => getImagePathOrDefault(data.value.coverPath));
|
||||
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
getBook(data, error, to.params.id);
|
||||
})
|
||||
|
||||
function onRatingUpdate(rating) {
|
||||
data.value.rating = rating * 2;
|
||||
if (data.value.rating > 0) {
|
||||
data.value.read = true;
|
||||
data.value.wantread = false;
|
||||
}
|
||||
putRateBook(props.id, {rating: data.value.rating});
|
||||
function onRatingUpdate(rating) {
|
||||
data.value.rating = rating * 2
|
||||
if (data.value.rating > 0) {
|
||||
data.value.read = true
|
||||
data.value.wantread = false
|
||||
}
|
||||
putRateBook(props.id, { rating: data.value.rating })
|
||||
}
|
||||
|
||||
function onReadIconClick() {
|
||||
data.value.read = !data.value.read;
|
||||
if (data.value.read) {
|
||||
data.value.wantread = false;
|
||||
data.value.endReadDate = today;
|
||||
putEndReadDate(props.id, today);
|
||||
} else {
|
||||
putUnreadBook(props.id);
|
||||
}
|
||||
function onReadIconClick() {
|
||||
data.value.read = !data.value.read
|
||||
if (data.value.read) {
|
||||
data.value.wantread = false
|
||||
data.value.endReadDate = today
|
||||
putEndReadDate(props.id, today)
|
||||
} else {
|
||||
putUnreadBook(props.id)
|
||||
}
|
||||
}
|
||||
|
||||
function onWantReadIconClick() {
|
||||
data.value.wantread = !data.value.wantread;
|
||||
putWantReadBook(props.id, {wantread: data.value.wantread});
|
||||
function onWantReadIconClick() {
|
||||
data.value.wantread = !data.value.wantread
|
||||
putWantReadBook(props.id, { wantread: data.value.wantread })
|
||||
}
|
||||
|
||||
function onStartReadIconClick() {
|
||||
if (!data.value.startReadDate) {
|
||||
data.value.startReadDate = today
|
||||
putStartReadDate(props.id, data.value.startReadDate)
|
||||
} else {
|
||||
data.value.startReadDate = null
|
||||
putStartReadDateUnset(props.id)
|
||||
}
|
||||
}
|
||||
|
||||
function onStartReadIconClick() {
|
||||
if (!data.value.startReadDate) {
|
||||
data.value.startReadDate = today;
|
||||
putStartReadDate(props.id, data.value.startReadDate);
|
||||
} else {
|
||||
data.value.startReadDate = null;
|
||||
putStartReadDateUnset(props.id);
|
||||
}
|
||||
function onStartReadDateChange(d) {
|
||||
data.value.startReadDate = d
|
||||
if (d != '') {
|
||||
putStartReadDate(props.id, data.value.startReadDate)
|
||||
} else {
|
||||
putStartReadDateUnset(props.id)
|
||||
}
|
||||
}
|
||||
|
||||
function onStartReadDateChange(d) {
|
||||
data.value.startReadDate = d;
|
||||
if (d != "") {
|
||||
putStartReadDate(props.id, data.value.startReadDate);
|
||||
} else {
|
||||
putStartReadDateUnset(props.id);
|
||||
}
|
||||
function onEndReadDateChange(d) {
|
||||
data.value.endReadDate = d
|
||||
if (d != '') {
|
||||
putEndReadDate(props.id, data.value.endReadDate)
|
||||
} else {
|
||||
putEndReadDateUnset(props.id)
|
||||
}
|
||||
}
|
||||
|
||||
function onEndReadDateChange(d) {
|
||||
data.value.endReadDate = d;
|
||||
if (d != "") {
|
||||
putEndReadDate(props.id, data.value.endReadDate);
|
||||
} else {
|
||||
putEndReadDateUnset(props.id);
|
||||
}
|
||||
}
|
||||
|
||||
function isStartReadExpanded() {
|
||||
let isStartReadDateSet = data.value.startReadDate ? true : false;
|
||||
let isReadUnset = !data.value.read ? true : false;
|
||||
return isStartReadDateSet && isReadUnset;
|
||||
}
|
||||
|
||||
function goToAuthor() {
|
||||
router.push("/author/" + data.value.authorId)
|
||||
}
|
||||
function isStartReadExpanded() {
|
||||
let isStartReadDateSet = data.value.startReadDate ? true : false
|
||||
let isReadUnset = !data.value.read ? true : false
|
||||
return isStartReadDateSet && isReadUnset
|
||||
}
|
||||
|
||||
function goToAuthor() {
|
||||
router.push('/author/' + data.value.authorId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="error">{{$t('bookform.error', {error: error.message})}}</div>
|
||||
<div v-if="error">{{ $t('bookform.error', { error: error.message }) }}</div>
|
||||
<div v-if="data" class="columns">
|
||||
<div class="column is-narrow left-panel">
|
||||
<figure class="image">
|
||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title">
|
||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" />
|
||||
</figure>
|
||||
<VRating
|
||||
half-increments
|
||||
hover
|
||||
:length="5"
|
||||
size="x-large"
|
||||
density="compact"
|
||||
:model-value="data.rating/2"
|
||||
@update:modelValue="onRatingUpdate"
|
||||
active-color="bulma-body-color"
|
||||
class="centered"
|
||||
/>
|
||||
half-increments
|
||||
hover
|
||||
:length="5"
|
||||
size="x-large"
|
||||
density="compact"
|
||||
:model-value="data.rating / 2"
|
||||
@update:modelValue="onRatingUpdate"
|
||||
active-color="bulma-body-color"
|
||||
class="centered"
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="title">{{data.title}}</h3>
|
||||
<h3 class="subtitle clickable" @click="goToAuthor">{{data.author}}</h3>
|
||||
<p>{{data.summary}}</p>
|
||||
<div class="my-5" v-if="data.isbn">ISBN: {{data.isbn}}</div>
|
||||
<div class="my-5" v-if="data.inventaireid">Inventaire ID: {{data.inventaireid}}</div>
|
||||
<div class="my-5" v-if="data.openlibraryid">OLID: {{data.openlibraryid}}</div>
|
||||
<h3 class="title">{{ data.title }}</h3>
|
||||
<h3 class="subtitle clickable" @click="goToAuthor">{{ data.author }}</h3>
|
||||
<p>{{ data.summary }}</p>
|
||||
<div class="my-5" v-if="data.isbn">ISBN: {{ data.isbn }}</div>
|
||||
<div class="my-5" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div>
|
||||
<div class="my-5" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="iconscontainer" :class="data.read ? 'remove-border-bottom' : ''">
|
||||
<div class="bigiconcontainer">
|
||||
<BigIcon icon="BIconEye"
|
||||
:legend="$t('bookform.wantread')"
|
||||
:isSet="data.wantread"
|
||||
@click="onWantReadIconClick"/>
|
||||
<BigIcon
|
||||
icon="BIconEye"
|
||||
:legend="$t('bookform.wantread')"
|
||||
:isSet="data.wantread"
|
||||
@click="onWantReadIconClick"
|
||||
/>
|
||||
</div>
|
||||
<BookDateWidget
|
||||
icon="BIconBook"
|
||||
:legend="$t('bookform.startread')"
|
||||
:start-read-date="data.startReadDate"
|
||||
:is-expanded="isStartReadExpanded()"
|
||||
:is-readonly="data.read"
|
||||
@onStartDateChange="onStartReadDateChange"
|
||||
@onIconClick="onStartReadIconClick"/>
|
||||
icon="BIconBook"
|
||||
:legend="$t('bookform.startread')"
|
||||
:start-read-date="data.startReadDate"
|
||||
:is-expanded="isStartReadExpanded()"
|
||||
:is-readonly="data.read"
|
||||
@onStartDateChange="onStartReadDateChange"
|
||||
@onIconClick="onStartReadIconClick"
|
||||
/>
|
||||
<BookDateWidget
|
||||
icon="BIconCheckCircle"
|
||||
:legend="$t('bookform.read')"
|
||||
:start-read-date="data.startReadDate"
|
||||
use-end-date
|
||||
last-widget
|
||||
:endReadDate="data.endReadDate"
|
||||
:isExpanded="data.read"
|
||||
@onStartDateChange="onStartReadDateChange"
|
||||
@onEndDateChange="onEndReadDateChange"
|
||||
@onIconClick="onReadIconClick"/>
|
||||
icon="BIconCheckCircle"
|
||||
:legend="$t('bookform.read')"
|
||||
:start-read-date="data.startReadDate"
|
||||
use-end-date
|
||||
last-widget
|
||||
:endReadDate="data.endReadDate"
|
||||
:isExpanded="data.read"
|
||||
@onStartDateChange="onStartReadDateChange"
|
||||
@onEndDateChange="onEndReadDateChange"
|
||||
@onIconClick="onReadIconClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,16 +161,16 @@
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
max-height:500px;
|
||||
max-width:500px;
|
||||
height:auto;
|
||||
width:auto;
|
||||
max-height: 500px;
|
||||
max-width: 500px;
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iconscontainer {
|
||||
@@ -178,7 +189,6 @@ img {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
img {
|
||||
max-height: 250px;
|
||||
@@ -190,15 +200,14 @@ img {
|
||||
}
|
||||
|
||||
.image {
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iconscontainer {
|
||||
display:flex;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { putReadBook, getImagePathOrDefault, postImportBook } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, computed } from 'vue'
|
||||
import { putReadBook, getImagePathOrDefault, postImportBook } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
const props = defineProps({
|
||||
id: Number,
|
||||
inventaireid: String,
|
||||
isinventaireedition: Boolean,
|
||||
@@ -16,90 +16,89 @@
|
||||
read: Boolean,
|
||||
wantread: Boolean,
|
||||
coverPath: String,
|
||||
});
|
||||
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath));
|
||||
const error = ref(null)
|
||||
})
|
||||
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath))
|
||||
const error = ref(null)
|
||||
|
||||
async function onUserBookRead() {
|
||||
const res = await putReadBook(props.id);
|
||||
if (res.ok) {
|
||||
router.push('/books')
|
||||
} else {
|
||||
res.json().then((json) => (error.value = json));
|
||||
}
|
||||
}
|
||||
|
||||
async function openBook() {
|
||||
if (props.id != 0) {
|
||||
router.push(`/book/${props.id}`);
|
||||
} else if (props.isinventaireedition) {
|
||||
importInventaireEdition()
|
||||
} else if (props.inventaireid != "") {
|
||||
router.push(`/import/inventaire/${props.inventaireid}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function importInventaireEdition(inventaireid) {
|
||||
const res = await postImportBook(props.inventaireid, navigator.language.substring(0,2));
|
||||
const json = await res.json();
|
||||
async function onUserBookRead() {
|
||||
const res = await putReadBook(props.id)
|
||||
if (res.ok) {
|
||||
router.push(`/book/${json.id}`);
|
||||
router.push('/books')
|
||||
} else {
|
||||
error.value = json;
|
||||
res.json().then((json) => (error.value = json))
|
||||
}
|
||||
}
|
||||
|
||||
async function openBook() {
|
||||
if (props.id != 0) {
|
||||
router.push(`/book/${props.id}`)
|
||||
} else if (props.isinventaireedition) {
|
||||
importInventaireEdition()
|
||||
} else if (props.inventaireid != '') {
|
||||
router.push(`/import/inventaire/${props.inventaireid}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function importInventaireEdition(inventaireid) {
|
||||
const res = await postImportBook(props.inventaireid, navigator.language.substring(0, 2))
|
||||
const json = await res.json()
|
||||
if (res.ok) {
|
||||
router.push(`/book/${json.id}`)
|
||||
} else {
|
||||
error.value = json
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="error" class="notification is-danger">
|
||||
<p>{{error}}</p>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
<div class="columns no-padding box container has-background-dark">
|
||||
<div class="media column no-margin clickable" @click="openBook">
|
||||
<div class="media-left">
|
||||
<figure class="image mb-3">
|
||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="title">
|
||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="title" />
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="is-size-4">{{title}}</div>
|
||||
<div class="is-size-5 is-italic">{{author}}</div>
|
||||
<div class="has-text-text-65 is-size-6" v-if="props.description">{{description}}</div>
|
||||
<div class="is-size-4">{{ title }}</div>
|
||||
<div class="is-size-5 is-italic">{{ author }}</div>
|
||||
<div class="has-text-text-65 is-size-6" v-if="props.description">{{ description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!inventaireid" class="column is-narrow">
|
||||
<button @click="" class="button is-large verticalbutton">
|
||||
<span class="icon" :title="$t('booklistelement.wantread')">
|
||||
<b-icon-eye-fill v-if="props.wantread" />
|
||||
<b-icon-eye v-else />
|
||||
</span>
|
||||
</button>
|
||||
<button @click="" class="button is-large verticalbutton">
|
||||
<span class="icon" :title="$t('booklistelement.startread')">
|
||||
<b-icon-book />
|
||||
</span>
|
||||
</button>
|
||||
<button @click="onUserBookRead" class="button is-large verticalbutton">
|
||||
<span class="icon" :title="$t('booklistelement.read')">
|
||||
<b-icon-check-circle-fill v-if="props.read" />
|
||||
<b-icon-check-circle v-else />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!inventaireid" class="column is-narrow">
|
||||
<button @click="" class="button is-large verticalbutton">
|
||||
<span class="icon" :title="$t('booklistelement.wantread')">
|
||||
<b-icon-eye-fill v-if="props.wantread" />
|
||||
<b-icon-eye v-else />
|
||||
</span>
|
||||
</button>
|
||||
<button @click="" class="button is-large verticalbutton">
|
||||
<span class="icon" :title="$t('booklistelement.startread')">
|
||||
<b-icon-book />
|
||||
</span>
|
||||
</button>
|
||||
<button @click="onUserBookRead" class="button is-large verticalbutton">
|
||||
<span class="icon" :title="$t('booklistelement.read')">
|
||||
<b-icon-check-circle-fill v-if="props.read" />
|
||||
<b-icon-check-circle v-else />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
max-height:180px;
|
||||
max-width:180px;
|
||||
height:auto;
|
||||
width:auto;
|
||||
max-height: 180px;
|
||||
max-width: 180px;
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.box {
|
||||
transition:ease-in-out 0.04s;
|
||||
transition: ease-in-out 0.04s;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@@ -119,5 +118,4 @@ img {
|
||||
.no-margin {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -4,89 +4,98 @@ import BookCard from './BookCard.vue'
|
||||
import { getMyBooks } from './api.js'
|
||||
import Pagination from './Pagination.vue'
|
||||
|
||||
|
||||
|
||||
const FilterStates = Object.freeze({
|
||||
READ: "read",
|
||||
WANTREAD: "wantread",
|
||||
READING: "reading",
|
||||
});
|
||||
READ: 'read',
|
||||
WANTREAD: 'wantread',
|
||||
READING: 'reading',
|
||||
})
|
||||
|
||||
const limit = 6;
|
||||
const pageNumber = ref(1);
|
||||
const limit = 6
|
||||
const pageNumber = ref(1)
|
||||
|
||||
const offset = computed(() => (pageNumber.value - 1) * limit);
|
||||
const offset = computed(() => (pageNumber.value - 1) * limit)
|
||||
|
||||
let currentFilterState = ref(FilterStates.READ);
|
||||
let currentFilterState = ref(FilterStates.READ)
|
||||
|
||||
let data = ref(null);
|
||||
let error = ref(null);
|
||||
let data = ref(null)
|
||||
let error = ref(null)
|
||||
|
||||
let totalBooksNumber = computed(() => (typeof(data) != 'undefined' &&
|
||||
data.value != null) ? data.value["count"] : 0);
|
||||
let totalBooksNumber = computed(() =>
|
||||
typeof data != 'undefined' && data.value != null ? data.value['count'] : 0,
|
||||
)
|
||||
let pageTotal = computed(() => Math.ceil(totalBooksNumber.value / limit))
|
||||
|
||||
fetchData();
|
||||
fetchData()
|
||||
|
||||
function fetchData() {
|
||||
let res = getMyBooks(data, error, currentFilterState.value, limit, offset.value);
|
||||
let res = getMyBooks(data, error, currentFilterState.value, limit, offset.value)
|
||||
}
|
||||
|
||||
function onFilterButtonClick(newstate) {
|
||||
currentFilterState.value = newstate;
|
||||
pageNumber.value = 1;
|
||||
fetchData();
|
||||
currentFilterState.value = newstate
|
||||
pageNumber.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function computeDynamicClass(state) {
|
||||
return currentFilterState.value === state ? 'is-active is-primary' : '';
|
||||
return currentFilterState.value === state ? 'is-active is-primary' : ''
|
||||
}
|
||||
|
||||
function pageChange(newPageNumber) {
|
||||
pageNumber.value = newPageNumber;
|
||||
data.value = null;
|
||||
fetchData();
|
||||
pageNumber.value = newPageNumber
|
||||
data.value = null
|
||||
fetchData()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5">
|
||||
<button class="button is-medium"
|
||||
@click="onFilterButtonClick(FilterStates.READ)"
|
||||
:class="computeDynamicClass(FilterStates.READ)">
|
||||
{{$t('bookbrowser.read')}}
|
||||
<button
|
||||
class="button is-medium"
|
||||
@click="onFilterButtonClick(FilterStates.READ)"
|
||||
:class="computeDynamicClass(FilterStates.READ)"
|
||||
>
|
||||
{{ $t('bookbrowser.read') }}
|
||||
</button>
|
||||
<button class="button is-medium"
|
||||
@click="onFilterButtonClick(FilterStates.READING)"
|
||||
:class="computeDynamicClass(FilterStates.READING)">
|
||||
{{$t('bookbrowser.reading')}}
|
||||
<button
|
||||
class="button is-medium"
|
||||
@click="onFilterButtonClick(FilterStates.READING)"
|
||||
:class="computeDynamicClass(FilterStates.READING)"
|
||||
>
|
||||
{{ $t('bookbrowser.reading') }}
|
||||
</button>
|
||||
<button class="button is-medium"
|
||||
@click="onFilterButtonClick(FilterStates.WANTREAD)"
|
||||
:class="computeDynamicClass(FilterStates.WANTREAD)">
|
||||
{{$t('bookbrowser.wantread')}}
|
||||
<button
|
||||
class="button is-medium"
|
||||
@click="onFilterButtonClick(FilterStates.WANTREAD)"
|
||||
:class="computeDynamicClass(FilterStates.WANTREAD)"
|
||||
>
|
||||
{{ $t('bookbrowser.wantread') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="error">{{$t('bookbrowser.error', {error: error.message})}}</div>
|
||||
<div v-if="error">{{ $t('bookbrowser.error', { error: error.message }) }}</div>
|
||||
<div v-else-if="data">
|
||||
<div class="">
|
||||
<div class="" v-for="book in data.books" :key="book.id">
|
||||
<BookCard v-bind="book" />
|
||||
</div>
|
||||
</div>
|
||||
<Pagination :pageNumber="pageNumber" :pageTotal="pageTotal" maxItemDisplayed="11" @pageChange="pageChange"/>
|
||||
<Pagination
|
||||
:pageNumber="pageNumber"
|
||||
:pageTotal="pageTotal"
|
||||
maxItemDisplayed="11"
|
||||
@pageChange="pageChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>{{$t('bookbrowser.loading')}}</div>
|
||||
<div v-else>{{ $t('bookbrowser.loading') }}</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.books {
|
||||
position:relative;
|
||||
float:left;
|
||||
width:100%; height:auto;
|
||||
padding-bottom: 100px;
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding-bottom: 100px;
|
||||
line-height: 2.5;
|
||||
|
||||
column-count: 2;
|
||||
|
||||
@@ -1,86 +1,92 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { postImage } from './api.js'
|
||||
import { ref, computed } from 'vue'
|
||||
import { postImage } from './api.js'
|
||||
|
||||
const emit = defineEmits(['OnImageUpload'])
|
||||
const props = defineProps({
|
||||
name: String
|
||||
});
|
||||
const emit = defineEmits(['OnImageUpload'])
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
})
|
||||
|
||||
const imagePath = ref(null);
|
||||
const error = ref(null);
|
||||
const imagePath = ref(null)
|
||||
const error = ref(null)
|
||||
|
||||
function onFileChanged(e) {
|
||||
postImage(e.target.files[0])
|
||||
.then((res) => res.json())
|
||||
.then((json) => onJsonResult(json))
|
||||
.catch((err) => (error.value = err["error"]));
|
||||
}
|
||||
function onFileChanged(e) {
|
||||
postImage(e.target.files[0])
|
||||
.then((res) => res.json())
|
||||
.then((json) => onJsonResult(json))
|
||||
.catch((err) => (error.value = err['error']))
|
||||
}
|
||||
|
||||
function onJsonResult(json) {
|
||||
imagePath.value = json["filepath"];
|
||||
emit('OnImageUpload', json["fileId"])
|
||||
}
|
||||
function onJsonResult(json) {
|
||||
imagePath.value = json['filepath']
|
||||
emit('OnImageUpload', json['fileId'])
|
||||
}
|
||||
|
||||
function unsetImage() {
|
||||
imagePath.value = null;
|
||||
}
|
||||
function unsetImage() {
|
||||
imagePath.value = null
|
||||
}
|
||||
|
||||
const imageSrc = computed(() => {
|
||||
return "http://localhost:8080" + imagePath.value
|
||||
})
|
||||
const imageSrc = computed(() => {
|
||||
return 'http://localhost:8080' + imagePath.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="imagePath">
|
||||
<div class="relative">
|
||||
<figure class="image mb-3">
|
||||
<img v-bind:src="imageSrc" v-bind:alt="props.name">
|
||||
<img v-bind:src="imageSrc" v-bind:alt="props.name" />
|
||||
</figure>
|
||||
<span class="icon is-large" @click="unsetImage">
|
||||
<b-icon-x-circle-fill/>
|
||||
<b-icon-x-circle-fill />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="file">
|
||||
<label class="file-label">
|
||||
<input class="file-input" @change="onFileChanged" type="file" :name="props.name" accept="image/*"/>
|
||||
<input
|
||||
class="file-input"
|
||||
@change="onFileChanged"
|
||||
type="file"
|
||||
:name="props.name"
|
||||
accept="image/*"
|
||||
/>
|
||||
<span class="file-cta">
|
||||
<span class="file-icon">
|
||||
<b-icon-upload />
|
||||
</span>
|
||||
<span class="file-label">{{$t('addbook.coverupload')}}</span>
|
||||
<span class="file-label">{{ $t('addbook.coverupload') }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
max-height:500px;
|
||||
max-width:500px;
|
||||
height:auto;
|
||||
width:auto;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
img {
|
||||
max-height: 500px;
|
||||
max-width: 500px;
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.relative img {
|
||||
display: block;
|
||||
}
|
||||
.relative img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.relative .icon {
|
||||
position: absolute;
|
||||
bottom:5px;
|
||||
left:5px;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
font-size: 24px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.relative .icon {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
font-size: 24px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.relative .icon:hover {
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
.relative .icon:hover {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
<script setup>
|
||||
import { useAuthStore } from './auth.store.js'
|
||||
|
||||
const authStore = useAuthStore();
|
||||
import { useAuthStore } from './auth.store.js'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="authStore.user">
|
||||
{{ $t('home.welcomeuser', {username: authStore.user.username}) }}
|
||||
{{ $t('home.welcomeuser', { username: authStore.user.username }) }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $t('home.welcome') }}</p>
|
||||
<div class="mt-5 is-hidden-desktop">
|
||||
<RouterLink to="/signup" class="button is-primary mx-2">
|
||||
<strong>{{ $t('navbar.signup')}}</strong>
|
||||
<strong>{{ $t('navbar.signup') }}</strong>
|
||||
</RouterLink>
|
||||
<RouterLink to="/login" class="button is-light mx-2">
|
||||
{{ $t('navbar.login')}}
|
||||
{{ $t('navbar.login') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,68 +1,66 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import ImportListElement from './ImportListElement.vue';
|
||||
import { getInventaireEditionBooks } from './api.js'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
import Pagination from './Pagination.vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import ImportListElement from './ImportListElement.vue'
|
||||
import { getInventaireEditionBooks } from './api.js'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
import Pagination from './Pagination.vue'
|
||||
|
||||
const limit = 5;
|
||||
const pageNumber = ref(1);
|
||||
const limit = 5
|
||||
const pageNumber = ref(1)
|
||||
|
||||
const offset = computed(() => (pageNumber.value - 1) * limit);
|
||||
const offset = computed(() => (pageNumber.value - 1) * limit)
|
||||
|
||||
const props = defineProps({
|
||||
inventaireid: String,
|
||||
});
|
||||
const props = defineProps({
|
||||
inventaireid: String,
|
||||
})
|
||||
|
||||
let data = ref(null);
|
||||
let error = ref(null);
|
||||
let data = ref(null)
|
||||
let error = ref(null)
|
||||
|
||||
const pageTotal = computed(() => {
|
||||
let countValue = (data.value !== null) ? data.value['count'] : 0;
|
||||
return Math.ceil(countValue / limit);
|
||||
});
|
||||
const pageTotal = computed(() => {
|
||||
let countValue = data.value !== null ? data.value['count'] : 0
|
||||
return Math.ceil(countValue / limit)
|
||||
})
|
||||
|
||||
fetchData(props.inventaireid);
|
||||
fetchData(props.inventaireid)
|
||||
|
||||
|
||||
function fetchData(inventaireid, authorId) {
|
||||
if (inventaireid != null) {
|
||||
let lang = navigator.language.substring(0,2);
|
||||
getInventaireEditionBooks(data, error, inventaireid, lang, limit, offset.value);
|
||||
}
|
||||
function fetchData(inventaireid, authorId) {
|
||||
if (inventaireid != null) {
|
||||
let lang = navigator.language.substring(0, 2)
|
||||
getInventaireEditionBooks(data, error, inventaireid, lang, limit, offset.value)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
pageNumber.value = 1;
|
||||
fetchData(to.params.inventaireid);
|
||||
})
|
||||
|
||||
function pageChange(newPageNumber) {
|
||||
pageNumber.value = newPageNumber;
|
||||
data.value = null;
|
||||
fetchData(props.inventaireid);
|
||||
}
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
pageNumber.value = 1
|
||||
fetchData(to.params.inventaireid)
|
||||
})
|
||||
|
||||
function pageChange(newPageNumber) {
|
||||
pageNumber.value = newPageNumber
|
||||
data.value = null
|
||||
fetchData(props.inventaireid)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="booksearch my-2">
|
||||
<h1 class="title">{{$t('importinventaire.title')}}</h1>
|
||||
<div v-if="error">{{$t('searchbook.error', {error: error.message})}}</div>
|
||||
<h1 class="title">{{ $t('importinventaire.title') }}</h1>
|
||||
<div v-if="error">{{ $t('searchbook.error', { error: error.message }) }}</div>
|
||||
<div v-else-if="data && data.results && data.results.length > 0">
|
||||
<div class="booksearchlist" v-for="book in data.results" :key="book.id">
|
||||
<div class="booksearchlist" v-for="book in data.results" :key="book.id">
|
||||
<ImportListElement v-bind="book" />
|
||||
</div>
|
||||
<Pagination
|
||||
:pageNumber="pageNumber"
|
||||
:pageTotal="pageTotal"
|
||||
maxItemDisplayed="11"
|
||||
@pageChange="pageChange"/>
|
||||
:pageNumber="pageNumber"
|
||||
:pageTotal="pageTotal"
|
||||
maxItemDisplayed="11"
|
||||
@pageChange="pageChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="data === null">{{$t('searchbook.loading')}}</div>
|
||||
<div v-else>{{$t('searchbook.noresult')}}</div>
|
||||
<div v-else-if="data === null">{{ $t('searchbook.loading') }}</div>
|
||||
<div v-else>{{ $t('searchbook.noresult') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
|
||||
@@ -1,65 +1,67 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { getInventaireImagePathOrDefault, postImportBook } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, computed } from 'vue'
|
||||
import { getInventaireImagePathOrDefault, postImportBook } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
uri: String,
|
||||
title: String,
|
||||
image: String,
|
||||
publisher: String,
|
||||
date: String,
|
||||
isbn: String,
|
||||
lang: String
|
||||
});
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
uri: String,
|
||||
title: String,
|
||||
image: String,
|
||||
publisher: String,
|
||||
date: String,
|
||||
isbn: String,
|
||||
lang: String,
|
||||
})
|
||||
|
||||
function displayDate(date) {
|
||||
const regex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (regex.test(date)) {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString();
|
||||
} else {
|
||||
return date;
|
||||
}
|
||||
function displayDate(date) {
|
||||
const regex = /^\d{4}-\d{2}-\d{2}$/
|
||||
if (regex.test(date)) {
|
||||
const d = new Date(date)
|
||||
return d.toLocaleDateString()
|
||||
} else {
|
||||
return date
|
||||
}
|
||||
const error = ref(null);
|
||||
const data = ref(null);
|
||||
const importing = ref(false);
|
||||
}
|
||||
const error = ref(null)
|
||||
const data = ref(null)
|
||||
const importing = ref(false)
|
||||
|
||||
async function importInventaireEdition() {
|
||||
importing.value = true;
|
||||
const res = await postImportBook(props.uri, navigator.language.substring(0,2));
|
||||
const json = await res.json();
|
||||
if (res.ok) {
|
||||
router.push(`/book/${json.id}`);
|
||||
} else {
|
||||
error.value = json;
|
||||
}
|
||||
async function importInventaireEdition() {
|
||||
importing.value = true
|
||||
const res = await postImportBook(props.uri, navigator.language.substring(0, 2))
|
||||
const json = await res.json()
|
||||
if (res.ok) {
|
||||
router.push(`/book/${json.id}`)
|
||||
} else {
|
||||
error.value = json
|
||||
}
|
||||
const imagePathOrDefault = computed(() => getInventaireImagePathOrDefault(props.image));
|
||||
}
|
||||
const imagePathOrDefault = computed(() => getInventaireImagePathOrDefault(props.image))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="columns no-padding box container has-background-dark">
|
||||
<div v-if="importing && !data && !error">{{$t('importlistelement.importing')}}</div>
|
||||
<div v-if="importing && !data && !error">{{ $t('importlistelement.importing') }}</div>
|
||||
<div v-else class="media column no-margin clickable" @click="importInventaireEdition">
|
||||
<div class="media-left">
|
||||
<figure class="image mb-3">
|
||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="title">
|
||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="title" />
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
|
||||
<div v-if="error" class="has-text-danger">
|
||||
<p>{{error}}</p>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="is-size-4">{{title}}</div>
|
||||
<div v-if="props.date" class="is-size-5">{{$t('importlistelement.releasedate') + "" +
|
||||
displayDate(date)}}</div>
|
||||
<div v-if="props.publisher" class="is-size-5">{{$t('importlistelement.publisher') + "" + publisher}}</div>
|
||||
<div v-if="props.isbn" class="is-size-5">{{"ISBN: " + isbn}}</div>
|
||||
<div class="is-size-4">{{ title }}</div>
|
||||
<div v-if="props.date" class="is-size-5">
|
||||
{{ $t('importlistelement.releasedate') + '' + displayDate(date) }}
|
||||
</div>
|
||||
<div v-if="props.publisher" class="is-size-5">
|
||||
{{ $t('importlistelement.publisher') + '' + publisher }}
|
||||
</div>
|
||||
<div v-if="props.isbn" class="is-size-5">{{ 'ISBN: ' + isbn }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,14 +70,14 @@
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
max-height:180px;
|
||||
max-width:180px;
|
||||
height:auto;
|
||||
width:auto;
|
||||
max-height: 180px;
|
||||
max-width: 180px;
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.box {
|
||||
transition:ease-in-out 0.04s;
|
||||
transition: ease-in-out 0.04s;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@@ -95,5 +97,4 @@ img {
|
||||
.no-margin {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,71 +1,84 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { postLogin, extractFormErrorFromField, extractGlobalFormError } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from './auth.store.js'
|
||||
import { ref, computed } from 'vue'
|
||||
import { postLogin, extractFormErrorFromField, extractGlobalFormError } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from './auth.store.js'
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
const user = ref({
|
||||
username: "",
|
||||
password: ""
|
||||
});
|
||||
const user = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const errors = ref(null)
|
||||
const errors = ref(null)
|
||||
|
||||
const formError = computed(() => {
|
||||
return extractGlobalFormError(errors.value);
|
||||
})
|
||||
const userError = computed(() => {
|
||||
return extractFormErrorFromField("Username", errors.value);
|
||||
})
|
||||
const passwordError = computed(() => {
|
||||
return extractFormErrorFromField("Password", errors.value);
|
||||
})
|
||||
const formError = computed(() => {
|
||||
return extractGlobalFormError(errors.value)
|
||||
})
|
||||
const userError = computed(() => {
|
||||
return extractFormErrorFromField('Username', errors.value)
|
||||
})
|
||||
const passwordError = computed(() => {
|
||||
return extractFormErrorFromField('Password', errors.value)
|
||||
})
|
||||
|
||||
async function onSubmit(e) {
|
||||
const res = await postLogin(user)
|
||||
if (res.ok) {
|
||||
let json = await res.json();
|
||||
await login(user.value.username, json);
|
||||
router.push('/');
|
||||
return;
|
||||
} else {
|
||||
res.json().then((json) => (errors.value = json));
|
||||
}
|
||||
async function onSubmit(e) {
|
||||
const res = await postLogin(user)
|
||||
if (res.ok) {
|
||||
let json = await res.json()
|
||||
await login(user.value.username, json)
|
||||
router.push('/')
|
||||
return
|
||||
} else {
|
||||
res.json().then((json) => (errors.value = json))
|
||||
}
|
||||
}
|
||||
|
||||
async function login(username, json) {
|
||||
useAuthStore().login({username: username, token: json["token"]})
|
||||
}
|
||||
async function login(username, json) {
|
||||
useAuthStore().login({ username: username, token: json['token'] })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div v-if="formError" class="notification is-danger">
|
||||
<p>{{formError}}</p>
|
||||
<p>{{ formError }}</p>
|
||||
</div>
|
||||
<h1 class="title">{{$t('login.title')}}</h1>
|
||||
<h1 class="title">{{ $t('login.title') }}</h1>
|
||||
<form class="box" @submit.prevent="onSubmit">
|
||||
<div class="field">
|
||||
<label class="label">{{$t('login.username')}}</label>
|
||||
<label class="label">{{ $t('login.username') }}</label>
|
||||
<div class="control">
|
||||
<input :class="'input ' + (userError ? 'is-danger' : '')" type="text" minlength="2" maxlength="20"
|
||||
required v-model="user.username" :placeholder="$t('login.username')">
|
||||
<input
|
||||
:class="'input ' + (userError ? 'is-danger' : '')"
|
||||
type="text"
|
||||
minlength="2"
|
||||
maxlength="20"
|
||||
required
|
||||
v-model="user.username"
|
||||
:placeholder="$t('login.username')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="userError" class="help is-danger">{{userError}}</p>
|
||||
<p v-if="userError" class="help is-danger">{{ userError }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{$t('login.password')}}</label>
|
||||
<label class="label">{{ $t('login.password') }}</label>
|
||||
<div class="control">
|
||||
<input :class="'input ' + (passwordError ? 'is-danger' : '')" type="password" minlength="6"
|
||||
maxlength="100" v-model="user.password" required :placeholder="$t('login.password')">
|
||||
<input
|
||||
:class="'input ' + (passwordError ? 'is-danger' : '')"
|
||||
type="password"
|
||||
minlength="6"
|
||||
maxlength="100"
|
||||
v-model="user.password"
|
||||
required
|
||||
:placeholder="$t('login.password')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="passwordError" class="help is-danger">{{passwordError}}</p>
|
||||
<p v-if="passwordError" class="help is-danger">{{ passwordError }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-link">{{$t('login.login')}}</button>
|
||||
<button class="button is-link">{{ $t('login.login') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BarcodeModal from './BarcodeModal.vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BarcodeModal from './BarcodeModal.vue'
|
||||
|
||||
const searchterm = ref("");
|
||||
const router = useRouter();
|
||||
const searchterm = ref('')
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
sizeClass: String,
|
||||
isMobile: Boolean
|
||||
});
|
||||
const props = defineProps({
|
||||
sizeClass: String,
|
||||
isMobile: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits('searchDone')
|
||||
const emit = defineEmits('searchDone')
|
||||
|
||||
const vMobileFocus = {
|
||||
mounted: (el, binding) => {
|
||||
if (binding.value) {
|
||||
el.focus()
|
||||
}
|
||||
const vMobileFocus = {
|
||||
mounted: (el, binding) => {
|
||||
if (binding.value) {
|
||||
el.focus()
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
function onSearchClick() {
|
||||
if (typeof searchterm.value === "undefined" || searchterm.value === "") {
|
||||
return
|
||||
}
|
||||
emit('searchDone')
|
||||
router.push('/search/' + searchterm.value);
|
||||
function onSearchClick() {
|
||||
if (typeof searchterm.value === 'undefined' || searchterm.value === '') {
|
||||
return
|
||||
}
|
||||
emit('searchDone')
|
||||
router.push('/search/' + searchterm.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar-item">
|
||||
<div class="field has-addons">
|
||||
<div class="control" :class="isMobile ? 'fullwidth' : ''">
|
||||
<input v-mobile-focus="isMobile ? true : null" v-model="searchterm" @keyup.enter="onSearchClick()" class="input" :class="sizeClass" type="text" />
|
||||
<input
|
||||
v-mobile-focus="isMobile ? true : null"
|
||||
v-model="searchterm"
|
||||
@keyup.enter="onSearchClick()"
|
||||
class="input"
|
||||
:class="sizeClass"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button @click="onSearchClick()" class="button" :class="sizeClass">
|
||||
@@ -43,13 +50,13 @@
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<BarcodeModal v-if="!isMobile"/>
|
||||
<BarcodeModal v-if="!isMobile" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fullwidth {
|
||||
width: 100%;
|
||||
}
|
||||
.fullwidth {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,82 +1,91 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps(['pageNumber', 'pageTotal', 'maxItemDisplayed']);
|
||||
const emit = defineEmits(['pageChange']);
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps(['pageNumber', 'pageTotal', 'maxItemDisplayed'])
|
||||
const emit = defineEmits(['pageChange'])
|
||||
|
||||
const paginatedItems = computed(() => {
|
||||
let items = [];
|
||||
if (props.pageTotal > props.maxItemDisplayed) {
|
||||
const paginatedItems = computed(() => {
|
||||
let items = []
|
||||
if (props.pageTotal > props.maxItemDisplayed) {
|
||||
//number of pages we can display before and after the current page
|
||||
const maxNumberOfItemsAroundCurrentItem = Math.floor((props.maxItemDisplayed - 2) / 2)
|
||||
|
||||
//number of pages we can display before and after the current page
|
||||
const maxNumberOfItemsAroundCurrentItem = Math.floor((props.maxItemDisplayed - 2) / 2);
|
||||
items.push(1)
|
||||
//compute first item number
|
||||
let firstItemNumber
|
||||
let lastItemNumber
|
||||
|
||||
items.push(1);
|
||||
//compute first item number
|
||||
let firstItemNumber;
|
||||
let lastItemNumber;
|
||||
|
||||
if (props.pageNumber - maxNumberOfItemsAroundCurrentItem < 4) {
|
||||
//starting at the left
|
||||
firstItemNumber = 2;
|
||||
lastItemNumber = props.maxItemDisplayed;
|
||||
} else if (props.pageNumber + maxNumberOfItemsAroundCurrentItem > props.pageTotal - 2) {
|
||||
//starting at the right
|
||||
firstItemNumber = props.pageTotal - props.maxItemDisplayed + 1;
|
||||
lastItemNumber = props.pageTotal - 1;
|
||||
} else {
|
||||
firstItemNumber = props.pageNumber - maxNumberOfItemsAroundCurrentItem;
|
||||
lastItemNumber = props.pageNumber + maxNumberOfItemsAroundCurrentItem;
|
||||
}
|
||||
|
||||
if (firstItemNumber !== 2) {
|
||||
items.push(-1);
|
||||
}
|
||||
|
||||
for (let i = firstItemNumber; i <= lastItemNumber; i++) {
|
||||
items.push(i);
|
||||
}
|
||||
if (lastItemNumber !== props.pageTotal - 1) {
|
||||
items.push(-1);
|
||||
}
|
||||
items.push(props.pageTotal);
|
||||
if (props.pageNumber - maxNumberOfItemsAroundCurrentItem < 4) {
|
||||
//starting at the left
|
||||
firstItemNumber = 2
|
||||
lastItemNumber = props.maxItemDisplayed
|
||||
} else if (props.pageNumber + maxNumberOfItemsAroundCurrentItem > props.pageTotal - 2) {
|
||||
//starting at the right
|
||||
firstItemNumber = props.pageTotal - props.maxItemDisplayed + 1
|
||||
lastItemNumber = props.pageTotal - 1
|
||||
} else {
|
||||
for (let i = 1; i <= props.pageTotal; i++) {
|
||||
items.push(i);
|
||||
}
|
||||
firstItemNumber = props.pageNumber - maxNumberOfItemsAroundCurrentItem
|
||||
lastItemNumber = props.pageNumber + maxNumberOfItemsAroundCurrentItem
|
||||
}
|
||||
return items;})
|
||||
|
||||
if (firstItemNumber !== 2) {
|
||||
items.push(-1)
|
||||
}
|
||||
|
||||
for (let i = firstItemNumber; i <= lastItemNumber; i++) {
|
||||
items.push(i)
|
||||
}
|
||||
if (lastItemNumber !== props.pageTotal - 1) {
|
||||
items.push(-1)
|
||||
}
|
||||
items.push(props.pageTotal)
|
||||
} else {
|
||||
for (let i = 1; i <= props.pageTotal; i++) {
|
||||
items.push(i)
|
||||
}
|
||||
}
|
||||
return items
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="props.pageTotal > 1" class="pagination" role="navigation" aria-label="pagination">
|
||||
<a href="#" v-if="props.pageNumber > 1"
|
||||
@click="$emit('pageChange', Math.max(1, props.pageNumber - 1))"
|
||||
class="pagination-previous">
|
||||
{{$t('pagination.previous')}}
|
||||
<a
|
||||
href="#"
|
||||
v-if="props.pageNumber > 1"
|
||||
@click="$emit('pageChange', Math.max(1, props.pageNumber - 1))"
|
||||
class="pagination-previous"
|
||||
>
|
||||
{{ $t('pagination.previous') }}
|
||||
</a>
|
||||
<a href="#" v-if="props.pageNumber < props.pageTotal"
|
||||
@click="$emit('pageChange', props.pageNumber + 1)"
|
||||
class="pagination-next">
|
||||
{{$t('pagination.next')}}
|
||||
</a>
|
||||
<ul class="pagination-list">
|
||||
<li v-for="item in paginatedItems" :key="item">
|
||||
<span v-if="item === -1" class="pagination-ellipsis">…</span>
|
||||
<a v-else
|
||||
href="#"
|
||||
@click="$emit('pageChange', item)"
|
||||
class="pagination-link"
|
||||
:class="item === props.pageNumber ? 'is-current' : ''"
|
||||
:aria-current="item === props.pageNumber ? 'page' : null"
|
||||
:aria-label="$t(item === props.pageNumber ? 'pagination.page' : 'pagination.goto', {pageNumber: item})">
|
||||
{{ item }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<a
|
||||
href="#"
|
||||
v-if="props.pageNumber < props.pageTotal"
|
||||
@click="$emit('pageChange', props.pageNumber + 1)"
|
||||
class="pagination-next"
|
||||
>
|
||||
{{ $t('pagination.next') }}
|
||||
</a>
|
||||
<ul class="pagination-list">
|
||||
<li v-for="item in paginatedItems" :key="item">
|
||||
<span v-if="item === -1" class="pagination-ellipsis">…</span>
|
||||
<a
|
||||
v-else
|
||||
href="#"
|
||||
@click="$emit('pageChange', item)"
|
||||
class="pagination-link"
|
||||
:class="item === props.pageNumber ? 'is-current' : ''"
|
||||
:aria-current="item === props.pageNumber ? 'page' : null"
|
||||
:aria-label="
|
||||
$t(item === props.pageNumber ? 'pagination.page' : 'pagination.goto', {
|
||||
pageNumber: item,
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ item }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
<script setup>
|
||||
import { useTemplateRef, onMounted, ref } from 'vue'
|
||||
import { BrowserMultiFormatReader } from "@zxing/library";
|
||||
import { useTemplateRef, onMounted, ref } from 'vue'
|
||||
import { BrowserMultiFormatReader } from '@zxing/library'
|
||||
|
||||
const emit = defineEmits('readBarcode')
|
||||
const emit = defineEmits('readBarcode')
|
||||
|
||||
const scanResult = ref(null);
|
||||
const codeReader = new BrowserMultiFormatReader();
|
||||
const scannerElement = useTemplateRef("scanner");
|
||||
const scanResult = ref(null)
|
||||
const codeReader = new BrowserMultiFormatReader()
|
||||
const scannerElement = useTemplateRef('scanner')
|
||||
|
||||
onMounted(() => {
|
||||
codeReader.decodeFromVideoDevice(undefined, scannerElement.value, (result, err) => {
|
||||
if (result) {
|
||||
emit('readBarcode', result.text)
|
||||
}
|
||||
});
|
||||
});
|
||||
onMounted(() => {
|
||||
codeReader.decodeFromVideoDevice(undefined, scannerElement.value, (result, err) => {
|
||||
if (result) {
|
||||
emit('readBarcode', result.text)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function onResult(result) {
|
||||
scanResult.value = result
|
||||
}
|
||||
function onResult(result) {
|
||||
scanResult.value = result
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="subtitle">{{$t('barcode.title')}}</h1>
|
||||
<div v-if="scanResult">{{scanResult}}</div>
|
||||
<h1 class="subtitle">{{ $t('barcode.title') }}</h1>
|
||||
<div v-if="scanResult">{{ scanResult }}</div>
|
||||
<video poster="data:image/gif,AAAA" ref="scanner"></video>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
video {
|
||||
max-width: 90%;
|
||||
}
|
||||
video {
|
||||
max-width: 90%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,90 +1,93 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import BookListElement from './BookListElement.vue';
|
||||
import { getSearchBooks, getAuthorBooks } from './api.js'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
import Pagination from './Pagination.vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import BookListElement from './BookListElement.vue'
|
||||
import { getSearchBooks, getAuthorBooks } from './api.js'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
import Pagination from './Pagination.vue'
|
||||
|
||||
const limit = 5;
|
||||
const pageNumber = ref(1);
|
||||
const limit = 5
|
||||
const pageNumber = ref(1)
|
||||
|
||||
const offset = computed(() => (pageNumber.value - 1) * limit);
|
||||
const offset = computed(() => (pageNumber.value - 1) * limit)
|
||||
|
||||
const props = defineProps({
|
||||
searchterm: String,
|
||||
authorId: Number
|
||||
});
|
||||
const props = defineProps({
|
||||
searchterm: String,
|
||||
authorId: Number,
|
||||
})
|
||||
|
||||
const forceSearchInventaire = ref(false);
|
||||
const forceSearchInventaire = ref(false)
|
||||
|
||||
const searchInventaire = computed(() => {
|
||||
return forceSearchInventaire.value || (data.value !== null && data.value['inventaire'])
|
||||
});
|
||||
const searchInventaire = computed(() => {
|
||||
return forceSearchInventaire.value || (data.value !== null && data.value['inventaire'])
|
||||
})
|
||||
|
||||
let data = ref(null);
|
||||
let error = ref(null);
|
||||
let data = ref(null)
|
||||
let error = ref(null)
|
||||
|
||||
const pageTotal = computed(() => {
|
||||
const countValue = (data.value !== null) ? data.value['count'] : 0;
|
||||
return Math.ceil(countValue / limit);
|
||||
});
|
||||
const pageTotal = computed(() => {
|
||||
const countValue = data.value !== null ? data.value['count'] : 0
|
||||
return Math.ceil(countValue / limit)
|
||||
})
|
||||
|
||||
fetchData(props.searchterm, props.authorId);
|
||||
fetchData(props.searchterm, props.authorId)
|
||||
|
||||
|
||||
function fetchData(searchTerm, authorId) {
|
||||
data.value = null;
|
||||
error.value = null;
|
||||
if (searchTerm != null) {
|
||||
const lang = navigator.language.substring(0,2);
|
||||
getSearchBooks(data, error, searchTerm, lang, forceSearchInventaire.value, limit, offset.value);
|
||||
} else if (authorId != null) {
|
||||
getAuthorBooks(data, error, authorId, limit, offset.value);
|
||||
}
|
||||
function fetchData(searchTerm, authorId) {
|
||||
data.value = null
|
||||
error.value = null
|
||||
if (searchTerm != null) {
|
||||
const lang = navigator.language.substring(0, 2)
|
||||
getSearchBooks(data, error, searchTerm, lang, forceSearchInventaire.value, limit, offset.value)
|
||||
} else if (authorId != null) {
|
||||
getAuthorBooks(data, error, authorId, limit, offset.value)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
pageNumber.value = 1;
|
||||
fetchData(to.params.searchterm, props.authorId);
|
||||
})
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
pageNumber.value = 1
|
||||
fetchData(to.params.searchterm, props.authorId)
|
||||
})
|
||||
|
||||
function pageChange(newPageNumber) {
|
||||
pageNumber.value = newPageNumber;
|
||||
data.value = null;
|
||||
fetchData(props.searchterm, props.authorId);
|
||||
}
|
||||
|
||||
function toggleSearchInventaire() {
|
||||
pageNumber.value = 1;
|
||||
forceSearchInventaire.value = !forceSearchInventaire.value;
|
||||
fetchData(props.searchterm, props.authorId)
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
function pageChange(newPageNumber) {
|
||||
pageNumber.value = newPageNumber
|
||||
data.value = null
|
||||
fetchData(props.searchterm, props.authorId)
|
||||
}
|
||||
|
||||
function toggleSearchInventaire() {
|
||||
pageNumber.value = 1
|
||||
forceSearchInventaire.value = !forceSearchInventaire.value
|
||||
fetchData(props.searchterm, props.authorId)
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="booksearch my-2">
|
||||
<h1 class="title" v-if="data && data.inventaire">{{$t('searchbook.importinventaire')}}</h1>
|
||||
<div v-if="error">{{$t('searchbook.error', {error: error.message})}}</div>
|
||||
<h1 class="title" v-if="data && data.inventaire">{{ $t('searchbook.importinventaire') }}</h1>
|
||||
<div v-if="error">{{ $t('searchbook.error', { error: error.message }) }}</div>
|
||||
<div v-else-if="data && data.books && data.books.length > 0">
|
||||
<div class="booksearchlist" v-for="book in data.books" :key="book.id">
|
||||
<div class="booksearchlist" v-for="book in data.books" :key="book.id">
|
||||
<BookListElement v-bind="book" />
|
||||
</div>
|
||||
<div v-if="(!searchInventaire || forceSearchInventaire) && !authorId" class="box container clickable has-background-dark" @click="toggleSearchInventaire">
|
||||
<div class="is-size-4">{{searchInventaire ? $t('searchbook.backtosearch') : $t('searchbook.searchinventaire')}}</div>
|
||||
<div
|
||||
v-if="(!searchInventaire || forceSearchInventaire) && !authorId"
|
||||
class="box container clickable has-background-dark"
|
||||
@click="toggleSearchInventaire"
|
||||
>
|
||||
<div class="is-size-4">
|
||||
{{ searchInventaire ? $t('searchbook.backtosearch') : $t('searchbook.searchinventaire') }}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
:pageNumber="pageNumber"
|
||||
:pageTotal="pageTotal"
|
||||
maxItemDisplayed="11"
|
||||
@pageChange="pageChange"/>
|
||||
:pageNumber="pageNumber"
|
||||
:pageTotal="pageTotal"
|
||||
maxItemDisplayed="11"
|
||||
@pageChange="pageChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="data === null">{{$t('searchbook.loading')}}</div>
|
||||
<div v-else>{{$t('searchbook.noresult')}}</div>
|
||||
<div v-else-if="data === null">{{ $t('searchbook.loading') }}</div>
|
||||
<div v-else>{{ $t('searchbook.noresult') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,61 +1,74 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { postSignUp, extractFormErrorFromField, extractGlobalFormError } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, computed } from 'vue'
|
||||
import { postSignUp, extractFormErrorFromField, extractGlobalFormError } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
const user = ref({
|
||||
username: "",
|
||||
password: ""
|
||||
});
|
||||
const user = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const errors = ref(null)
|
||||
const errors = ref(null)
|
||||
|
||||
const formError = computed(() => {
|
||||
return extractGlobalFormError(errors.value);
|
||||
const formError = computed(() => {
|
||||
return extractGlobalFormError(errors.value)
|
||||
})
|
||||
const userError = computed(() => {
|
||||
return extractFormErrorFromField('Username', errors.value)
|
||||
})
|
||||
const passwordError = computed(() => {
|
||||
return extractFormErrorFromField('Password', errors.value)
|
||||
})
|
||||
|
||||
function onSubmit() {
|
||||
postSignUp(user).then((res) => {
|
||||
if (res.ok) {
|
||||
router.push('/')
|
||||
return
|
||||
} else {
|
||||
res.json().then((json) => (errors.value = json))
|
||||
}
|
||||
})
|
||||
const userError = computed(() => {
|
||||
return extractFormErrorFromField("Username", errors.value);
|
||||
})
|
||||
const passwordError = computed(() => {
|
||||
return extractFormErrorFromField("Password", errors.value);
|
||||
})
|
||||
|
||||
function onSubmit() {
|
||||
postSignUp(user)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
router.push('/');
|
||||
return;
|
||||
} else {
|
||||
res.json().then((json) => (errors.value = json));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="formError" class="notification is-danger">
|
||||
<p>{{formError}}</p>
|
||||
<p>{{ formError }}</p>
|
||||
</div>
|
||||
<h1 class="title">{{$t('signup.title')}}</h1>
|
||||
<h1 class="title">{{ $t('signup.title') }}</h1>
|
||||
<form class="box" @submit.prevent="onSubmit">
|
||||
<div class="field">
|
||||
<label class="label">{{$t('signup.username')}}</label>
|
||||
<label class="label">{{ $t('signup.username') }}</label>
|
||||
<div class="control">
|
||||
<input :class="'input ' + (userError ? 'is-danger' : '')" type="text" minlength="2" maxlength="20"
|
||||
required v-model="user.username" :placeholder="$t('signup.username')">
|
||||
<input
|
||||
:class="'input ' + (userError ? 'is-danger' : '')"
|
||||
type="text"
|
||||
minlength="2"
|
||||
maxlength="20"
|
||||
required
|
||||
v-model="user.username"
|
||||
:placeholder="$t('signup.username')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="userError" class="help is-danger">{{userError}}</p>
|
||||
<p v-if="userError" class="help is-danger">{{ userError }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('signup.password') }}</label>
|
||||
<div class="control">
|
||||
<input :class="'input ' + (passwordError ? 'is-danger' : '')" type="password" minlength="6"
|
||||
maxlength="100" v-model="user.password" required :placeholder="$t('signup.password')">
|
||||
<input
|
||||
:class="'input ' + (passwordError ? 'is-danger' : '')"
|
||||
type="password"
|
||||
minlength="6"
|
||||
maxlength="100"
|
||||
v-model="user.password"
|
||||
required
|
||||
:placeholder="$t('signup.password')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="passwordError" class="help is-danger">{{passwordError}}</p>
|
||||
<p v-if="passwordError" class="help is-danger">{{ passwordError }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
|
||||
133
front/src/api.js
133
front/src/api.js
@@ -1,79 +1,93 @@
|
||||
import { useAuthStore } from './auth.store.js'
|
||||
|
||||
export function getInventaireImagePathOrDefault(path) {
|
||||
return getImagePathOrGivenDefault(path, "../../image/defaultinventairebook.png")
|
||||
return getImagePathOrGivenDefault(path, '../../image/defaultinventairebook.png')
|
||||
}
|
||||
|
||||
export function getImagePathOrDefault(path) {
|
||||
return getImagePathOrGivenDefault(path, "../image/defaultbook.png")
|
||||
return getImagePathOrGivenDefault(path, '../image/defaultbook.png')
|
||||
}
|
||||
|
||||
export function getImagePathOrGivenDefault(path, defaultpath) {
|
||||
if (path == "" || typeof path === 'undefined') {
|
||||
return defaultpath;
|
||||
} else if (path.startsWith("https://")) {
|
||||
return path;
|
||||
if (path == '' || typeof path === 'undefined') {
|
||||
return defaultpath
|
||||
} else if (path.startsWith('https://')) {
|
||||
return path
|
||||
} else {
|
||||
return path;
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
function useFetch(data, error, url) {
|
||||
const { user } = useAuthStore();
|
||||
const { user } = useAuthStore()
|
||||
|
||||
if (user != null) {
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + user.token
|
||||
}
|
||||
Authorization: 'Bearer ' + user.token,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === 401) {
|
||||
const authStore = useAuthStore();
|
||||
authStore.logout();
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
}
|
||||
return res.json();
|
||||
return res.json()
|
||||
})
|
||||
.then((json) => (data.value = json))
|
||||
.catch((err) => (error.value = err));
|
||||
.catch((err) => (error.value = err))
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAppInfo(appInfo, appInfoErr) {
|
||||
return fetch('/ws/appinfo', {
|
||||
method: 'GET'
|
||||
}).then((res) => res.json())
|
||||
.then((json) => appInfo.value = json)
|
||||
.catch((err) => (appInfoErr.value = err))
|
||||
method: 'GET',
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((json) => (appInfo.value = json))
|
||||
.catch((err) => (appInfoErr.value = err))
|
||||
}
|
||||
|
||||
export function getMyBooks(data, error, arg, limit, offset) {
|
||||
const queryParams = new URLSearchParams({limit: limit, offset: offset});
|
||||
return useFetch(data, error, '/ws/mybooks/' + arg + "?" + queryParams.toString());
|
||||
const queryParams = new URLSearchParams({ limit: limit, offset: offset })
|
||||
return useFetch(data, error, '/ws/mybooks/' + arg + '?' + queryParams.toString())
|
||||
}
|
||||
|
||||
export function getSearchBooks(data, error, searchterm, lang, searchInventaire, limit, offset) {
|
||||
const queryParams = new URLSearchParams({lang: lang, inventaire: searchInventaire, limit: limit, offset: offset});
|
||||
return useFetch(data, error, '/ws/search/' + encodeURIComponent(searchterm) + "?" + queryParams.toString());
|
||||
const queryParams = new URLSearchParams({
|
||||
lang: lang,
|
||||
inventaire: searchInventaire,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
})
|
||||
return useFetch(
|
||||
data,
|
||||
error,
|
||||
'/ws/search/' + encodeURIComponent(searchterm) + '?' + queryParams.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
export function getInventaireEditionBooks(data, error, inventaireId, lang, limit, offset) {
|
||||
const queryParams = new URLSearchParams({lang: lang, limit: limit, offset: offset});
|
||||
return useFetch(data, error, '/ws/inventaire/books/' + encodeURIComponent(inventaireId) + "?" + queryParams.toString());
|
||||
const queryParams = new URLSearchParams({ lang: lang, limit: limit, offset: offset })
|
||||
return useFetch(
|
||||
data,
|
||||
error,
|
||||
'/ws/inventaire/books/' + encodeURIComponent(inventaireId) + '?' + queryParams.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAuthor(data, error, id) {
|
||||
return useFetch(data, error, '/ws/author/' + id);
|
||||
return useFetch(data, error, '/ws/author/' + id)
|
||||
}
|
||||
|
||||
export function getAuthorBooks(data, error, id, limit, offset) {
|
||||
const queryParams = new URLSearchParams({limit: limit, offset: offset});
|
||||
return useFetch(data, error, '/ws/author/' + id + "/books" + "?" + queryParams.toString());
|
||||
const queryParams = new URLSearchParams({ limit: limit, offset: offset })
|
||||
return useFetch(data, error, '/ws/author/' + id + '/books' + '?' + queryParams.toString())
|
||||
}
|
||||
|
||||
export function getBook(data, error, id) {
|
||||
return useFetch(data, error, '/ws/book/' + id);
|
||||
return useFetch(data, error, '/ws/book/' + id)
|
||||
}
|
||||
|
||||
export function postBook(book) {
|
||||
@@ -81,39 +95,39 @@ export function postBook(book) {
|
||||
}
|
||||
|
||||
export async function postImportBook(id, language) {
|
||||
return genericPayloadCall('/ws/importbook', {inventaireid: id, lang: language}, 'POST');
|
||||
return genericPayloadCall('/ws/importbook', { inventaireid: id, lang: language }, 'POST')
|
||||
}
|
||||
|
||||
export async function putReadBook(bookId) {
|
||||
return genericPayloadCall('/ws/book/' + bookId + "/read", {read: true}, 'PUT')
|
||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true }, 'PUT')
|
||||
}
|
||||
|
||||
export async function putUnreadBook(bookId) {
|
||||
return genericPayloadCall('/ws/book/' + bookId + "/read", {read: false}, 'PUT')
|
||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: false }, 'PUT')
|
||||
}
|
||||
|
||||
export async function putEndReadDate(bookId, enddate) {
|
||||
return genericPayloadCall('/ws/book/' + bookId + "/read", {read: true, endDate: enddate}, 'PUT')
|
||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: enddate }, 'PUT')
|
||||
}
|
||||
|
||||
export async function putEndReadDateUnset(bookId) {
|
||||
return genericPayloadCall('/ws/book/' + bookId + "/read", {read: true, endDate: "null"}, 'PUT')
|
||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: 'null' }, 'PUT')
|
||||
}
|
||||
|
||||
export async function putStartReadDateUnset(bookId) {
|
||||
return genericPayloadCall('/ws/book/' + bookId + "/startread", {startDate: "null"}, 'PUT')
|
||||
return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: 'null' }, 'PUT')
|
||||
}
|
||||
|
||||
export async function putStartReadDate(bookId, startdate) {
|
||||
return genericPayloadCall('/ws/book/' + bookId + "/startread", {startDate: startdate}, 'PUT')
|
||||
return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: startdate }, 'PUT')
|
||||
}
|
||||
|
||||
export async function putWantReadBook(bookId, payload) {
|
||||
return genericPayloadCall('/ws/book/' + bookId + "/wantread", payload, 'PUT')
|
||||
return genericPayloadCall('/ws/book/' + bookId + '/wantread', payload, 'PUT')
|
||||
}
|
||||
|
||||
export async function putRateBook(bookId, payload) {
|
||||
return genericPayloadCall('/ws/book/' + bookId + "/rate", payload, 'PUT')
|
||||
return genericPayloadCall('/ws/book/' + bookId + '/rate', payload, 'PUT')
|
||||
}
|
||||
|
||||
export function postLogin(user) {
|
||||
@@ -125,19 +139,19 @@ export function postSignUp(user) {
|
||||
}
|
||||
|
||||
export function postImage(file) {
|
||||
const { user } = useAuthStore();
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const { user } = useAuthStore()
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
if (user != null) {
|
||||
return fetch("/ws/upload/cover", {
|
||||
return fetch('/ws/upload/cover', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + user.token
|
||||
Authorization: 'Bearer ' + user.token,
|
||||
},
|
||||
body: formData
|
||||
body: formData,
|
||||
})
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,48 +161,47 @@ export function genericPostCallNoAuth(apiRoute, object) {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(object)
|
||||
body: JSON.stringify(object),
|
||||
})
|
||||
}
|
||||
|
||||
export function genericPayloadCall(apiRoute, object, method) {
|
||||
const { user } = useAuthStore();
|
||||
const { user } = useAuthStore()
|
||||
|
||||
if (user != null) {
|
||||
return fetch(apiRoute, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + user.token
|
||||
Authorization: 'Bearer ' + user.token,
|
||||
},
|
||||
body: JSON.stringify(object)
|
||||
body: JSON.stringify(object),
|
||||
})
|
||||
}
|
||||
else {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
export function extractFormErrorFromField(fieldName, errors) {
|
||||
if (errors === null) {
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
if (errors.value == null) {
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
console.log(errors.value);
|
||||
const titleErr = errors.find((e) => e["field"] === fieldName);
|
||||
console.log(errors.value)
|
||||
const titleErr = errors.find((e) => e['field'] === fieldName)
|
||||
if (typeof titleErr !== 'undefined') {
|
||||
return titleErr.error;
|
||||
return titleErr.error
|
||||
} else {
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function extractGlobalFormError(errors) {
|
||||
if (errors !== null && "error" in errors) {
|
||||
return errors["error"];
|
||||
if (errors !== null && 'error' in errors) {
|
||||
return errors['error']
|
||||
} else {
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
// initialize state from local storage to enable user to stay logged in
|
||||
user: JSON.parse(localStorage.getItem('user')),
|
||||
returnUrl: null
|
||||
returnUrl: null,
|
||||
}),
|
||||
actions: {
|
||||
async login(user) {
|
||||
this.user = user;
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
this.user = user
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
},
|
||||
logout() {
|
||||
this.user = null;
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
}
|
||||
});
|
||||
this.user = null
|
||||
localStorage.removeItem('user')
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -16,22 +16,22 @@
|
||||
"barcode": "Scan barcode"
|
||||
},
|
||||
"addbook": {
|
||||
"title":"Title",
|
||||
"author":"Author",
|
||||
"submit":"Submit",
|
||||
"coverupload":"Upload cover"
|
||||
"title": "Title",
|
||||
"author": "Author",
|
||||
"submit": "Submit",
|
||||
"coverupload": "Upload cover"
|
||||
},
|
||||
"signup": {
|
||||
"title":"Sign up",
|
||||
"username":"Username",
|
||||
"password":"Password",
|
||||
"signup":"Sign up"
|
||||
"title": "Sign up",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"signup": "Sign up"
|
||||
},
|
||||
"login": {
|
||||
"title":"Log in",
|
||||
"username":"Username",
|
||||
"password":"Password",
|
||||
"login":"Log in"
|
||||
"title": "Log in",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"login": "Log in"
|
||||
},
|
||||
"bookbrowser": {
|
||||
"error": "Error when loading books: {error}",
|
||||
@@ -63,22 +63,21 @@
|
||||
"wantread": "Interested"
|
||||
},
|
||||
"pagination": {
|
||||
"previous":"Previous",
|
||||
"next":"Next",
|
||||
"goto":"Goto page {pageNumber}",
|
||||
"page":"Page {pageNumber}"
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"goto": "Goto page {pageNumber}",
|
||||
"page": "Page {pageNumber}"
|
||||
},
|
||||
"bookdatewidget": {
|
||||
"started": "Started at :",
|
||||
"finished": "Finished at :"
|
||||
},
|
||||
"importinventaire": {
|
||||
"title":"Please select a book to import"
|
||||
"title": "Please select a book to import"
|
||||
},
|
||||
"importlistelement": {
|
||||
"releasedate":"Release date:",
|
||||
"publisher":"Publisher:",
|
||||
"importing":"Importing..."
|
||||
"releasedate": "Release date:",
|
||||
"publisher": "Publisher:",
|
||||
"importing": "Importing..."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"addbook": "Ajouter Un Livre",
|
||||
"logout": "Se déconnecter",
|
||||
"signup": "S'inscrire",
|
||||
"search":"Rechercher",
|
||||
"search": "Rechercher",
|
||||
"login": "Se connecter"
|
||||
},
|
||||
"barcode": {
|
||||
@@ -16,22 +16,22 @@
|
||||
"barcode": "Scanner le code-barres"
|
||||
},
|
||||
"addbook": {
|
||||
"title":"Titre",
|
||||
"author":"Auteur",
|
||||
"submit":"Confirmer",
|
||||
"coverupload":"Téléverser la couverture"
|
||||
"title": "Titre",
|
||||
"author": "Auteur",
|
||||
"submit": "Confirmer",
|
||||
"coverupload": "Téléverser la couverture"
|
||||
},
|
||||
"signup": {
|
||||
"title":"Inscription",
|
||||
"username":"Nom d'utilisateur",
|
||||
"password":"Mot de passe",
|
||||
"signup":"S'inscrire"
|
||||
"title": "Inscription",
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"signup": "S'inscrire"
|
||||
},
|
||||
"login": {
|
||||
"title":"Connexion",
|
||||
"username":"Nom d'utilisateur",
|
||||
"password":"Mot de passe",
|
||||
"login":"Se connecter"
|
||||
"title": "Connexion",
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"login": "Se connecter"
|
||||
},
|
||||
"bookbrowser": {
|
||||
"error": "Erreur pendant le chargement des livres: {error}",
|
||||
@@ -63,21 +63,21 @@
|
||||
"wantread": "À lire"
|
||||
},
|
||||
"pagination": {
|
||||
"previous":"Précédent",
|
||||
"next":"Suivant",
|
||||
"goto":"Aller à la page {pageNumber}",
|
||||
"page":"Page {pageNumber}"
|
||||
"previous": "Précédent",
|
||||
"next": "Suivant",
|
||||
"goto": "Aller à la page {pageNumber}",
|
||||
"page": "Page {pageNumber}"
|
||||
},
|
||||
"bookdatewidget": {
|
||||
"started": "Commencé le :",
|
||||
"finished": "Fini le :"
|
||||
},
|
||||
"importinventaire": {
|
||||
"title":"Sélectionner l'édition à importer"
|
||||
"title": "Sélectionner l'édition à importer"
|
||||
},
|
||||
"importlistelement": {
|
||||
"releasedate":"Date de publication : ",
|
||||
"publisher":"Maison d'édition : ",
|
||||
"importing":"Import en cours..."
|
||||
"releasedate": "Date de publication : ",
|
||||
"publisher": "Maison d'édition : ",
|
||||
"importing": "Import en cours..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { createPinia } from 'pinia'
|
||||
import { BootstrapIconsPlugin } from "bootstrap-icons-vue";
|
||||
import { router } from './router.js';
|
||||
import { BootstrapIconsPlugin } from 'bootstrap-icons-vue'
|
||||
import { router } from './router.js'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
|
||||
import { VRating } from 'vuetify/components/VRating';
|
||||
import { VRating } from 'vuetify/components/VRating'
|
||||
import App from './App.vue'
|
||||
|
||||
import './styles/global.css';
|
||||
|
||||
import fr from './locales/fr.json';
|
||||
import en from './locales/en.json';
|
||||
import './styles/global.css'
|
||||
|
||||
import fr from './locales/fr.json'
|
||||
import en from './locales/en.json'
|
||||
|
||||
// configure i18n
|
||||
const i18n = createI18n({
|
||||
locale: navigator.language,
|
||||
fallbackLocale: "en",
|
||||
messages: { fr, en },
|
||||
});
|
||||
|
||||
locale: navigator.language,
|
||||
fallbackLocale: 'en',
|
||||
messages: { fr, en },
|
||||
})
|
||||
|
||||
const vuetify = createVuetify({
|
||||
VRating,
|
||||
@@ -35,5 +33,4 @@ const vuetify = createVuetify({
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
|
||||
createApp(App).use(i18n).use(vuetify).use(pinia).use(BootstrapIconsPlugin).use(router).mount('#app')
|
||||
|
||||
@@ -31,13 +31,13 @@ export const router = createRouter({
|
||||
})
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
// redirect to login page if not logged in and trying to access a restricted page
|
||||
const publicPages = ['/', '/login', '/signup'];
|
||||
const authRequired = !publicPages.includes(to.path);
|
||||
const auth = useAuthStore();
|
||||
// redirect to login page if not logged in and trying to access a restricted page
|
||||
const publicPages = ['/', '/login', '/signup']
|
||||
const authRequired = !publicPages.includes(to.path)
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (authRequired && !auth.user) {
|
||||
auth.returnUrl = to.fullPath;
|
||||
return '/login';
|
||||
}
|
||||
});
|
||||
if (authRequired && !auth.user) {
|
||||
auth.returnUrl = to.fullPath
|
||||
return '/login'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.clickable {
|
||||
cursor:pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,10 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
plugins: [vue(), vueDevTools()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user