Book form: add a field to write a review

This commit is contained in:
2026-03-15 15:23:14 +01:00
parent 524e517066
commit 2d0bce143a
6 changed files with 153 additions and 83 deletions

View File

@@ -3,8 +3,7 @@ import { ref, computed } from 'vue'
import { import {
getBook, getBook,
getImagePathOrDefault, getImagePathOrDefault,
putWantReadBook, putUpdateBook,
putRateBook,
putStartReadDate, putStartReadDate,
putStartReadDateUnset, putStartReadDateUnset,
putEndReadDate, putEndReadDate,
@@ -14,7 +13,7 @@ import {
import { useRouter, onBeforeRouteUpdate } from 'vue-router' import { useRouter, onBeforeRouteUpdate } from 'vue-router'
import { VRating } from 'vuetify/components/VRating' import { VRating } from 'vuetify/components/VRating'
import BookFormIcons from './BookFormIcons.vue' import BookFormIcons from './BookFormIcons.vue'
import ReviewModal from './ReviewModal.vue' import ReviewWidget from './ReviewWidget.vue'
const router = useRouter() const router = useRouter()
const props = defineProps({ const props = defineProps({
@@ -37,7 +36,12 @@ function onRatingUpdate(rating) {
data.value.read = true data.value.read = true
data.value.wantread = false data.value.wantread = false
} }
putRateBook(props.id, { rating: data.value.rating }) putUpdateBook(props.id, { rating: data.value.rating })
}
function onReviewUpdate(review) {
data.value.review = review
putUpdateBook(props.id, { review: data.value.review })
} }
async function onReadIconClick() { async function onReadIconClick() {
@@ -53,7 +57,7 @@ async function onReadIconClick() {
function onWantReadIconClick() { function onWantReadIconClick() {
data.value.wantread = !data.value.wantread data.value.wantread = !data.value.wantread
putWantReadBook(props.id, { wantread: data.value.wantread }) putUpdateBook(props.id, { wantread: data.value.wantread })
} }
async function onStartReadIconClick() { async function onStartReadIconClick() {
@@ -100,18 +104,6 @@ function goToAuthor() {
<figure class="image"> <figure class="image">
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" /> <img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" />
</figure> </figure>
<VRating
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 mt-2"
/>
<ReviewModal button-parent-class="centered mt-3" />
</div> </div>
<div class="column"> <div class="column">
<h3 class="title">{{ data.title }}</h3> <h3 class="title">{{ data.title }}</h3>
@@ -120,6 +112,7 @@ function goToAuthor() {
<div class="my-5" v-if="data.isbn">ISBN: {{ data.isbn }}</div> <div class="my-5" v-if="data.isbn">ISBN: {{ data.isbn }}</div>
<div class="my-5" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div> <div class="my-5" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div>
<div class="my-5" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div> <div class="my-5" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div>
<ReviewWidget :reviewtext="data.review" :rating="data.rating" @on-review-update="onReviewUpdate" @on-rating-update="onRatingUpdate"/>
</div> </div>
<div class="column"> <div class="column">
<BookFormIcons <BookFormIcons

View File

@@ -1,61 +0,0 @@
<script setup>
import { ref } from 'vue'
const props = defineProps({
buttonParentClass: String,
})
const open = ref(false)
</script>
<template>
<div :class="buttonParentClass">
<button @click="open = true" class="button is-large is-responsive">
<span class="icon">
<b-icon-pen/>
</span>
<span>{{$t('bookform.reviewbtn')}}</span>
</button>
</div>
<Teleport to="body">
<div v-if="open">
<div @click="open = false" class="modal-backdrop"></div>
<div class="modal has-background-dark">
<h2 class="subtitle">{{$t('bookform.reviewbtn')}}</h2>
<textarea class="textarea" :placeholder="$t('bookform.reviewbtn')"></textarea>
</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 {
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>

134
front/src/ReviewWidget.vue Normal file
View File

@@ -0,0 +1,134 @@
<script setup>
import { ref } from 'vue'
import { VRating } from 'vuetify/components/VRating'
const props = defineProps({
rating: Number,
reviewtext: String,
})
const isTextareaExpanded = ref(false)
const isTextareaTransitionEnabled = ref(true)
defineEmits('onRatingUpdate', 'onReviewUpdate')
function computeTextareaClass() {
let classAttr = isTextareaExpanded && isTextareaExpanded.value ? 'textarea-expanded' : 'textarea-normal'
if (isTextareaTransitionEnabled && isTextareaTransitionEnabled.value) {
classAttr += ' transition-height'
}
return classAttr
}
function onTextAreaFocus() {
isTextareaExpanded.value = true
setTimeout(() => {
isTextareaTransitionEnabled.value = false
}, 500)
}
</script>
<template>
<div class="maincontainer py-5">
<div class="widget-header mb-5 full-width">
<div class="widget-title ml-3">
<h2>{{$t('review.title')}}</h2>
<span class="ml-3">
<b-icon-pen/>
</span>
</div>
<VRating
half-increments
hover
:length="5"
size="x-large"
density="compact"
:model-value="rating / 2"
@update:modelValue="(r) => $emit('onRatingUpdate', r)"
active-color="bulma-body-color"
class="widget-rating centered"
/>
</div>
<div class="full-width centered">
<textarea
:placeholder="$t('review.textplaceholder')"
class="widget-textarea mx-4"
@change="(e) => $emit('onReviewUpdate', e.target.value)"
@focus="onTextAreaFocus"
:class="computeTextareaClass()">{{reviewtext}}</textarea>
</div>
</div>
</template>
<style scoped>
.maincontainer {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
border: solid;
border-radius: 20px;
padding: 10px;
}
.widget-header {
display: flex;
}
.widget-title {
flex: 2;
font-size: 2em;
display: flex;
justify-content: center;
align-items: center;
}
.widget-title h2 {
font-weight: bold;
}
.widget-rating {
flex: 2;
}
.widget-textarea {
color: var(--bulma-body-color);
background-color: var(--bulma-text-20);
width: 95%;
border-radius: 30px;
border: none;
padding: 15px;
}
.textarea-normal {
height: 80px;
resize: none;
}
.textarea-expanded {
height: 350px;
resize: vertical;
}
.transition-height {
transition: height 0.5s;
}
.full-width {
width: 100%;
}
@media (max-width: 1024px) {
.widget-header {
display: flex;
flex-wrap: wrap;
}
.widget-title {
font-size: 1.5em;
width: 100%;
margin-bottom: 10px;
}
}
</style>

View File

@@ -122,11 +122,7 @@ export async function putStartReadDate(bookId, startdate) {
return genericPayloadCall('/ws/book/' + bookId, { startDate: startdate }, 'PUT') return genericPayloadCall('/ws/book/' + bookId, { startDate: startdate }, 'PUT')
} }
export async function putWantReadBook(bookId, payload) { export async function putUpdateBook(bookId, payload) {
return genericPayloadCall('/ws/book/' + bookId, payload, 'PUT')
}
export async function putRateBook(bookId, payload) {
return genericPayloadCall('/ws/book/' + bookId, payload, 'PUT') return genericPayloadCall('/ws/book/' + bookId, payload, 'PUT')
} }

View File

@@ -81,5 +81,9 @@
"releasedate": "Release date:", "releasedate": "Release date:",
"publisher": "Publisher:", "publisher": "Publisher:",
"importing": "Importing..." "importing": "Importing..."
},
"review": {
"title": "My review",
"textplaceholder": "Write my review..."
} }
} }

View File

@@ -81,5 +81,9 @@
"releasedate": "Date de publication : ", "releasedate": "Date de publication : ",
"publisher": "Maison d'édition : ", "publisher": "Maison d'édition : ",
"importing": "Import en cours..." "importing": "Import en cours..."
},
"review": {
"title": "Ma critique",
"textplaceholder": "Écrire ma critique..."
} }
} }