generated from Flycro/laravel-nuxt
feat: Multiple Systems
This commit is contained in:
@@ -4,12 +4,12 @@ export default defineAppConfig({
|
||||
primary: 'sky',
|
||||
gray: 'cool',
|
||||
container: {
|
||||
constrained: 'max-w-7xl w-full'
|
||||
constrained: 'max-w-full w-full',
|
||||
},
|
||||
avatar: {
|
||||
default: {
|
||||
icon: 'i-heroicons-user',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
const links = [
|
||||
{
|
||||
label: 'Company',
|
||||
icon: 'i-heroicons-building-office-2',
|
||||
label: 'Bücher',
|
||||
icon: 'i-heroicons-book-open',
|
||||
children: [
|
||||
{
|
||||
label: 'Overview',
|
||||
to: '/login',
|
||||
label: 'Übersicht',
|
||||
to: '/book-recommendations',
|
||||
icon: 'i-heroicons-eye',
|
||||
},
|
||||
{
|
||||
label: 'Add Company',
|
||||
to: '/login',
|
||||
icon: 'i-heroicons-plus-circle',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||
import DeleteBookRecommendation from '~/components/modal/DeleteBookRecommendation.vue'
|
||||
import EditBookRecommendation from '~/components/modal/EditBookRecommendation.vue'
|
||||
import NewBookRecommendation from '~/components/modal/NewBookRecommendation.vue'
|
||||
import CastVote from '~/components/modal/CastVote.vue'
|
||||
|
||||
const dayjs = useDayjs()
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
bookRecommendationStore.fetchRecommendations()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'book_name',
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
key: 'author',
|
||||
label: 'Autor',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: 'Beschreibung',
|
||||
},
|
||||
{
|
||||
key: 'pages',
|
||||
label: 'Seiten',
|
||||
},
|
||||
{
|
||||
key: 'recommender.name',
|
||||
label: 'Empfohlen von',
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
label: 'Erstellt am',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
},
|
||||
{
|
||||
key: 'votes',
|
||||
label: 'Votes',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NewBookRecommendation />
|
||||
<UTable :loading="bookRecommendationStore.fetchRecommendationsStatus === 'pending'" :columns="columns" :rows="bookRecommendationStore.recommendations">
|
||||
<template #created_at-data="{ row }">
|
||||
<div>{{ dayjs(row.created_at).format('DD.MM.YYYY') }}</div>
|
||||
</template>
|
||||
<template #description-data="{ row }">
|
||||
{{ `${row.description.substring(0, 50)}...` }}
|
||||
</template>
|
||||
<template #votes-data="{ row }">
|
||||
{{ row.votes.length }}
|
||||
</template>
|
||||
<template #actions-data="{ row }">
|
||||
<div class="flex space-x-2">
|
||||
<CastVote :row="row" />
|
||||
<EditBookRecommendation :row="row" />
|
||||
<DeleteBookRecommendation :row="row" />
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
</template>
|
||||
58
nuxt/components/modal/CastVote.vue
Normal file
58
nuxt/components/modal/CastVote.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||
|
||||
const props = defineProps<{
|
||||
row: {
|
||||
id: number
|
||||
book_name: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const state = reactive({
|
||||
book_recommendation_id: props.row.id,
|
||||
})
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
|
||||
const { refresh: onVote, status } = useFetch<any>(`vote`, {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'Abstimmung erfolgreich.',
|
||||
color: 'emerald',
|
||||
})
|
||||
await bookRecommendationStore.fetchRecommendations()
|
||||
await authStore.fetchUser()
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton icon="i-heroicons-star" size="sm" color="green" variant="solid" square :disabled="authStore.user.total_votes === 0" @click="isOpen = true" />
|
||||
<UDashboardModal
|
||||
v-model="isOpen"
|
||||
title="Buch Empfehlung löschen"
|
||||
:description="`Bist du dir sicher das du für die Buchempfehlung ${row.book_name} abstimmen möchtest?`"
|
||||
icon="i-heroicons-star"
|
||||
:ui="{
|
||||
icon: { base: 'text-green-500 dark:text-green-400' } as any,
|
||||
footer: { base: 'ml-16' } as any,
|
||||
}"
|
||||
>
|
||||
<template #footer>
|
||||
<UButton color="primary" label="Abstimmung" :loading="status === 'pending'" @click="onVote" />
|
||||
<UButton color="white" label="Abbrechen" @click="isOpen = false" />
|
||||
</template>
|
||||
</UDashboardModal>
|
||||
</template>
|
||||
50
nuxt/components/modal/DeleteBookRecommendation.vue
Normal file
50
nuxt/components/modal/DeleteBookRecommendation.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||
|
||||
const props = defineProps<{
|
||||
row: {
|
||||
id: number
|
||||
book_name: string
|
||||
}
|
||||
}>()
|
||||
const isOpen = ref(false)
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
|
||||
const { refresh: onDelete, status } = useFetch<any>(`book-recommendations/${props.row.id}`, {
|
||||
method: 'DELETE',
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'Buchempfehlung wurde gelöscht.',
|
||||
color: 'emerald',
|
||||
})
|
||||
await bookRecommendationStore.fetchRecommendations()
|
||||
}
|
||||
|
||||
isOpen.value = false
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton icon="i-heroicons-trash" size="sm" color="red" variant="solid" square @click="isOpen = true" />
|
||||
<UDashboardModal
|
||||
v-model="isOpen"
|
||||
title="Buch Empfehlung löschen"
|
||||
:description="`Möchtest du die Buchempfehlung ${row.book_name} wirklich löschen?`"
|
||||
icon="i-heroicons-exclamation-circle"
|
||||
:ui="{
|
||||
icon: { base: 'text-red-500 dark:text-red-400' } as any,
|
||||
footer: { base: 'ml-16' } as any,
|
||||
}"
|
||||
>
|
||||
<template #footer>
|
||||
<UButton color="red" label="Löschen" :loading="status === 'pending'" @click="onDelete" />
|
||||
<UButton color="white" label="Abbrechen" @click="isOpen = false" />
|
||||
</template>
|
||||
</UDashboardModal>
|
||||
</template>
|
||||
97
nuxt/components/modal/EditBookRecommendation.vue
Normal file
97
nuxt/components/modal/EditBookRecommendation.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||
|
||||
const props = defineProps<{
|
||||
row: {
|
||||
id: number
|
||||
book_name: string
|
||||
author: string
|
||||
description: string
|
||||
isbn: string
|
||||
pages: number
|
||||
cover_image?: string
|
||||
status: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const form = ref()
|
||||
|
||||
const state = reactive({
|
||||
book_name: props.row.book_name,
|
||||
author: props.row.author,
|
||||
description: props.row.description,
|
||||
isbn: props.row.isbn,
|
||||
pages: props.row.pages,
|
||||
cover_image: props.row.cover_image,
|
||||
status: props.row.status,
|
||||
})
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
|
||||
const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations/${props.row.id}`, {
|
||||
method: 'PUT',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
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()
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton icon="i-heroicons-pencil-square" size="sm" variant="solid" square @click="isOpen = true" />
|
||||
|
||||
<UModal v-model="isOpen">
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Bearbeiten
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UForm ref="form" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UFormGroup label="Name" name="book_name">
|
||||
<UInput v-model="state.book_name" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Autor" name="author">
|
||||
<UInput v-model="state.author" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Beschreibung" name="description">
|
||||
<UTextarea v-model="state.description" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="ISBN" name="isbn">
|
||||
<UInput v-model="state.isbn" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Seiten" name="pages">
|
||||
<UInput v-model="state.pages" type="number" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Status" name="status">
|
||||
<USelect v-model="state.status" :options="bookRecommendationStore.statusOptions" option-attribute="name" />
|
||||
</UFormGroup>
|
||||
<UButton size="md" type="submit" :loading="status === 'pending'">
|
||||
Speichern
|
||||
</UButton>
|
||||
<UButton size="md" class="mx-4" color="white" label="Abbrechen" @click="isOpen = false" />
|
||||
</UForm>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
95
nuxt/components/modal/NewBookRecommendation.vue
Normal file
95
nuxt/components/modal/NewBookRecommendation.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const form = ref()
|
||||
|
||||
const state = reactive({
|
||||
book_name: null,
|
||||
author: null,
|
||||
description: null,
|
||||
isbn: null,
|
||||
pages: null,
|
||||
cover_image: null,
|
||||
status: null,
|
||||
})
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
|
||||
const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations`, {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
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
|
||||
state.author = null
|
||||
state.description = null
|
||||
state.isbn = null
|
||||
state.pages = null
|
||||
state.cover_image = null
|
||||
state.status = null
|
||||
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-end">
|
||||
<UButton icon="i-heroicons-plus" label="Neues Buch" size="sm" variant="solid" color="green" square @click="isOpen = true" />
|
||||
</div>
|
||||
|
||||
<UModal v-model="isOpen">
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Neues Buch
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UForm ref="form" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UFormGroup label="Name" name="book_name">
|
||||
<UInput v-model="state.book_name" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Autor" name="author">
|
||||
<UInput v-model="state.author" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Beschreibung" name="description">
|
||||
<UTextarea v-model="state.description" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="ISBN" name="isbn">
|
||||
<UInput v-model="state.isbn" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Seiten" name="pages">
|
||||
<UInput v-model="state.pages" type="number" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Status" name="status">
|
||||
<USelect v-model="state.status" :options="bookRecommendationStore.statusOptions" option-attribute="name" />
|
||||
</UFormGroup>
|
||||
<UButton size="md" type="submit" :loading="status === 'pending'">
|
||||
Erstellen
|
||||
</UButton>
|
||||
<UButton size="md" class="mx-4" color="white" label="Abbrechen" @click="isOpen = false" />
|
||||
</UForm>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
13
nuxt/pages/book-recommendations/index.vue
Normal file
13
nuxt/pages/book-recommendations/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import BookRecommendationTable from '~/components/book-recommendations/BookRecommendationTable.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BookRecommendationTable />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,26 +1,27 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export type User = {
|
||||
ulid: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
must_verify_email: boolean;
|
||||
has_password: boolean;
|
||||
roles: string[];
|
||||
providers: string[];
|
||||
export interface User {
|
||||
ulid: string
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
must_verify_email: boolean
|
||||
has_password: boolean
|
||||
roles: string[]
|
||||
providers: string[]
|
||||
total_votes: number
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const config = useRuntimeConfig()
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
const user = ref(<User>{});
|
||||
const user = ref(<User>{})
|
||||
const token = useCookie('token', {
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: config.public.apiBase.startsWith('https://'),
|
||||
maxAge: 60 * 60 * 24 * 365
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
})
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
|
||||
@@ -36,7 +37,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return navigateTo('/')
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { refresh: fetchUser } = useFetch<any>('user', {
|
||||
@@ -45,7 +46,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
if (response.status === 200) {
|
||||
user.value = response._data.user
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return { user, isLoggedIn, logout, fetchUser, token }
|
||||
|
||||
85
nuxt/stores/book-recommendations.ts
Normal file
85
nuxt/stores/book-recommendations.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
enum BookRecommendationStatusEnum {
|
||||
PENDING = 'PENDING',
|
||||
REJECTED = 'REJECTED',
|
||||
ACTIVE = 'ACTIVE',
|
||||
COMPLETED = 'COMPLETED',
|
||||
}
|
||||
|
||||
export interface BookRecommendation {
|
||||
id: number
|
||||
book_name: string
|
||||
author: string
|
||||
description: string
|
||||
isbn: string
|
||||
pages: number
|
||||
recommended_by?: number
|
||||
recommender?: {
|
||||
ulid: number
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
status: BookRecommendationStatusEnum
|
||||
cover_image?: string
|
||||
}
|
||||
|
||||
export const useBookRecommendationStore = defineStore('bookRecommendations', () => {
|
||||
const recommendations = ref<BookRecommendation[]>([])
|
||||
|
||||
const statusOptions = [
|
||||
{
|
||||
name: 'Pending',
|
||||
value: BookRecommendationStatusEnum.PENDING,
|
||||
},
|
||||
{
|
||||
name: 'Rejected',
|
||||
value: BookRecommendationStatusEnum.REJECTED,
|
||||
},
|
||||
{
|
||||
name: 'Active',
|
||||
value: BookRecommendationStatusEnum.ACTIVE,
|
||||
},
|
||||
{
|
||||
name: 'Completed',
|
||||
value: BookRecommendationStatusEnum.COMPLETED,
|
||||
},
|
||||
]
|
||||
|
||||
// Fetch all book recommendations
|
||||
const { refresh: fetchRecommendations, status: fetchRecommendationsStatus } = useFetch<BookRecommendation[]>('book-recommendations?with=recommender,votes', {
|
||||
immediate: false,
|
||||
onResponse({ response }) {
|
||||
if (response.status === 200) {
|
||||
recommendations.value = response._data
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const deleteRecommendation = async (id: number) => {
|
||||
try {
|
||||
const { error } = await useFetch(`book-recommendations/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (error.value) {
|
||||
console.error('Failed to delete book recommendation:', error.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 {
|
||||
recommendations,
|
||||
statusOptions,
|
||||
fetchRecommendations,
|
||||
fetchRecommendationsStatus,
|
||||
deleteRecommendation,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user