feat: BR Overview + Modal for New Recommendations

main
Flycro 2024-03-20 18:10:02 +01:00
parent 923e41b396
commit fffe1b4717
3 changed files with 139 additions and 58 deletions

View File

@ -19,6 +19,10 @@ const columns = [
key: 'author', key: 'author',
label: 'Autor', label: 'Autor',
}, },
{
key: 'published_at',
label: 'Erstveröffentlichung',
},
{ {
key: 'description', key: 'description',
label: 'Beschreibung', label: 'Beschreibung',
@ -42,27 +46,46 @@ const columns = [
{ {
key: 'votes', key: 'votes',
label: 'Votes', label: 'Votes',
sortable: true,
}, },
{ {
key: 'actions', key: 'actions',
label: '', label: '',
}, },
] ]
const sort = ref({
column: 'votes',
direction: 'desc',
})
function resolveStatus(status: string) {
return bookRecommendationStore.statusOptions.find(option => option.value === status)
}
</script> </script>
<template> <template>
<div> <div>
<NewBookRecommendation /> <NewBookRecommendation />
<UTable :loading="bookRecommendationStore.fetchRecommendationsStatus === 'pending'" :columns="columns" :rows="bookRecommendationStore.recommendations"> <UTable :sort="sort" :loading="bookRecommendationStore.fetchRecommendationsStatus === 'pending'" :columns="columns" :rows="bookRecommendationStore.recommendations">
<template #created_at-data="{ row }"> <template #created_at-data="{ row }">
<div>{{ dayjs(row.created_at).format('DD.MM.YYYY') }}</div> <div>{{ dayjs(row.created_at).format('DD.MM.YYYY') }}</div>
</template> </template>
<template #published_at-data="{ row }">
<div>{{ dayjs(row.published_at).format('DD.MM.YYYY') }}</div>
</template>
<template #description-data="{ row }"> <template #description-data="{ row }">
{{ `${row.description.substring(0, 50)}...` }} <div v-if="row.description">
{{ `${row.description.substring(0, 50)}...` }}
</div>
</template> </template>
<template #votes-data="{ row }"> <template #votes-data="{ row }">
{{ row.votes.length }} {{ row.votes.length }}
</template> </template>
<template #status-data="{ row }">
<UBadge :color="resolveStatus(row.status)?.color">
{{ resolveStatus(row.status)?.name }}
</UBadge>
</template>
<template #actions-data="{ row }"> <template #actions-data="{ row }">
<div class="flex space-x-2"> <div class="flex space-x-2">
<CastVote :row="row" /> <CastVote :row="row" />

View File

@ -2,50 +2,95 @@
import { useBookRecommendationStore } from '~/stores/book-recommendations' import { useBookRecommendationStore } from '~/stores/book-recommendations'
const isOpen = ref(false) const isOpen = ref(false)
const loading = ref(false)
const dayjs = useDayjs()
const form = ref() const form = ref()
const state = reactive({ interface State {
book_name: null, book_name: string
author: null, author: string
description: null, description: string
isbn: null, isbn: string
pages: null, pages: number
cover_image: null, cover_image?: File | string
status: null, status: string
published_at: string
// Index signature
[key: string]: string | number | File | undefined
}
const state: State = reactive({
book_name: '',
author: '',
description: '',
isbn: '',
pages: 0,
cover_image: '',
status: 'PENDING',
published_at: dayjs().format('YYYY-MM-DD'),
}) })
const bookRecommendationStore = useBookRecommendationStore() const bookRecommendationStore = useBookRecommendationStore()
const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations`, { function handleCoverImageInput(event: Event) {
method: 'POST', const file = (event.target as HTMLInputElement).files?.[0]
body: state, if (file) {
immediate: false, // Update the state with the selected file
watch: false, state.cover_image = file
async onResponse({ response }) { }
if (response?.status === 422) { }
form.value.setErrors(response._data?.errors)
}
else if (response.ok) {
useToast().add({
icon: 'i-heroicons-check-circle-20-solid',
title: 'Buchempfehlung wurde erfolgreich aktualisiert.',
color: 'emerald',
})
await bookRecommendationStore.fetchRecommendations()
state.book_name = null async function onSubmit() {
state.author = null loading.value = true
state.description = null const formData = new FormData()
state.isbn = null for (const key in state) {
state.pages = null const item = state[key]
state.cover_image = null if (item === undefined) {
state.status = null continue
isOpen.value = false
} }
}, if (key === 'cover_image' && state[key] instanceof File) {
}) formData.append(key, item as Blob, (state[key] as File).name)
}
else {
if (key === 'cover_image') {
continue
}
const value = typeof item === 'string' ? item : String(item)
formData.append(key, value)
}
}
await $fetch<any>(`book-recommendations`, {
method: 'POST',
body: formData,
async onResponse({ response }) {
loading.value = false
if (response?.status === 422) {
form.value.setErrors(response._data?.errors)
}
else if (response.ok) {
useToast().add({
icon: 'i-heroicons-check-circle-20-solid',
title: 'Buchempfehlung wurde erfolgreich angelegt.',
color: 'emerald',
})
await bookRecommendationStore.fetchRecommendations()
state.book_name = ''
state.author = ''
state.description = ''
state.isbn = ''
state.pages = 0
state.cover_image = ''
state.status = 'PENDING'
state.published_at = dayjs().format('YYYY-MM-DD')
isOpen.value = false
}
},
})
}
</script> </script>
<template> <template>
@ -72,9 +117,15 @@ const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations`, {
<UFormGroup label="Autor" name="author"> <UFormGroup label="Autor" name="author">
<UInput v-model="state.author" /> <UInput v-model="state.author" />
</UFormGroup> </UFormGroup>
<UFormGroup label="Erstveröffentlichung">
<UInput v-model="state.published_at" type="date" />
</UFormGroup>
<UFormGroup label="Beschreibung" name="description"> <UFormGroup label="Beschreibung" name="description">
<UTextarea v-model="state.description" /> <UTextarea v-model="state.description" />
</UFormGroup> </UFormGroup>
<UFormGroup label="Cover" name="cover_image">
<UInput type="file" @change="handleCoverImageInput" />
</UFormGroup>
<UFormGroup label="ISBN" name="isbn"> <UFormGroup label="ISBN" name="isbn">
<UInput v-model="state.isbn" /> <UInput v-model="state.isbn" />
</UFormGroup> </UFormGroup>

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
enum BookRecommendationStatusEnum { export enum BookRecommendationStatusEnum {
PENDING = 'PENDING', PENDING = 'PENDING',
REJECTED = 'REJECTED', REJECTED = 'REJECTED',
ACTIVE = 'ACTIVE', ACTIVE = 'ACTIVE',
@ -23,6 +23,7 @@ export interface BookRecommendation {
} }
status: BookRecommendationStatusEnum status: BookRecommendationStatusEnum
cover_image?: string cover_image?: string
published_at?: string
} }
export const useBookRecommendationStore = defineStore('bookRecommendations', () => { export const useBookRecommendationStore = defineStore('bookRecommendations', () => {
@ -30,20 +31,25 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
const statusOptions = [ const statusOptions = [
{ {
name: 'Pending', name: 'Ausstehend',
value: BookRecommendationStatusEnum.PENDING, value: BookRecommendationStatusEnum.PENDING,
color: 'orange',
}, },
{ {
name: 'Rejected', name: 'Abgelehnt',
value: BookRecommendationStatusEnum.REJECTED, value: BookRecommendationStatusEnum.REJECTED,
color: 'red',
}, },
{ {
name: 'Active', name: 'Aktiv',
value: BookRecommendationStatusEnum.ACTIVE, value: BookRecommendationStatusEnum.ACTIVE,
color: 'green',
}, },
{ {
name: 'Completed', name: 'Abgeschlossen',
value: BookRecommendationStatusEnum.COMPLETED, value: BookRecommendationStatusEnum.COMPLETED,
color: 'primary',
}, },
] ]
@ -57,29 +63,30 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
}, },
}) })
const deleteRecommendation = async (id: number) => { const { refresh: fetchActiveRecommendations, status: fetchActiveRecommendationsStatus } = useFetch<BookRecommendation[]>('book-recommendations?with=recommender,votes&status=ACTIVE', {
try { immediate: false,
const { error } = await useFetch(`book-recommendations/${id}`, { onResponse({ response }) {
method: 'DELETE', if (response.status === 200) {
}) recommendations.value = response._data
}
},
})
if (error.value) { function resetRecommendations() {
console.error('Failed to delete book recommendation:', error.value) recommendations.value = []
}
else {
recommendations.value = recommendations.value.filter(rec => rec.id !== id)
}
}
catch (e) {
console.error('An error occurred while deleting a book recommendation:', e)
}
} }
return { return {
recommendations, recommendations,
resetRecommendations,
statusOptions, statusOptions,
fetchRecommendations, fetchRecommendations,
fetchRecommendationsStatus, fetchRecommendationsStatus,
deleteRecommendation, fetchActiveRecommendations,
fetchActiveRecommendationsStatus,
} }
}) })
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useBookRecommendationStore, import.meta.hot))
}