Added a bunch of mini features

Sorted worlds, add preview and add cards, added timestamps...
This commit is contained in:
Alexis
2025-03-03 22:27:42 +01:00
parent a83bc240b1
commit 922bcdc64b
15 changed files with 204 additions and 102 deletions

View File

@@ -0,0 +1,37 @@
<script lang="ts" setup>
import { PhCalendarDots, PhTrash } from "@phosphor-icons/vue";
import type { Calendar } from "~/models/CalendarConfig";
defineProps<{
calendar: Calendar
}>()
const emit = defineEmits(["on-delete"])
</script>
<template>
<UiCard
class="w-full h-full transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900"
:link="`/my/calendars/${calendar.id}`"
>
<UiCardHeader>
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<p class="flex items-center gap-1">
<PhCalendarDots size="24" />
<span>{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span>
</p>
<UiButton
size="icon"
variant="ghost"
class="absolute top-2 right-2 z-20 hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700"
@click="emit('on-delete')"
>
<PhTrash size="16" />
</UiButton>
</UiCardContent>
</UiCard>
</template>

View File

@@ -6,7 +6,7 @@ const emit = defineEmits(["on-click"])
<template>
<UiCard
class="h-full w-fit transition-all bg-transparent dark:hover:bg-gray-950 dark:focus-within:outline-gray-900 hover:-translate-y-0"
class="h-full md:w-fit transition-all bg-transparent dark:hover:bg-gray-950 dark:focus-within:outline-gray-900 hover:-translate-y-0"
has-click
@on-click="emit('on-click')"
>

View File

@@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-indigo-200 bg-background dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)
"

View File

@@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-4xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-4xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background border-indigo-200 dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class
)
"

View File

@@ -20,7 +20,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
:class="
cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background dark:bg-slate-950 contrast-more:dark:bg-slate-900 px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
props.class
)
"

View File

@@ -0,0 +1,73 @@
<script lang="ts" setup>
import { PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
import type { World } from "~/models/World";
import { DateTime } from "luxon";
const props = defineProps<{
world: World
}>();
const emit = defineEmits(["on-edit", "on-delete"]);
const { locale } = useI18n();
const createdAt = DateTime.fromISO(props.world.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value });
const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromISO(props.world.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "");
</script>
<template>
<UiCard
class="w-full h-full transition-all"
:link="`/my/worlds/${world.id}`"
:class="{
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !world.color,
'bg-red-100 dark:bg-red-950 border-red-200 hover:bg-red-50 dark:hover:bg-red-900 dark:border-red-900 dark:focus-within:outline-red-900': world.color === 'red',
'bg-orange-100 dark:bg-orange-950 border-orange-200 hover:bg-orange-50 dark:hover:bg-orange-900 dark:border-orange-900 dark:focus-within:outline-orange-900': world.color === 'orange',
'bg-amber-100 dark:bg-amber-950 border-amber-200 hover:bg-amber-50 dark:hover:bg-amber-900 dark:border-amber-900 dark:focus-within:outline-amber-900': world.color === 'amber',
'bg-yellow-100 dark:bg-yellow-950 border-yellow-200 hover:bg-yellow-50 dark:hover:bg-yellow-900 dark:border-yellow-900 dark:focus-within:outline-yellow-900': world.color === 'yellow',
'bg-lime-100 dark:bg-lime-950 border-lime-200 hover:bg-lime-50 dark:hover:bg-lime-900 dark:border-lime-900 dark:focus-within:outline-lime-900': world.color === 'lime',
' bg-green-100 dark:bg-green-950 border-green-200 hover:bg-green-50 dark:hover:bg-green-900 dark:border-green-900 dark:focus-within:outline-green-900': world.color === 'green',
'bg-emerald-100 dark:bg-emerald-950 border-emerald-200 hover:bg-emerald-50 dark:hover:bg-emerald-900 dark:border-emerald-900 dark:focus-within:outline-emerald-900': world.color === 'emerald',
'bg-teal-100 dark:bg-teal-950 border-teal-200 hover:bg-teal-50 dark:hover:bg-teal-900 dark:border-teal-900 dark:focus-within:outline-teal-900': world.color === 'teal',
'bg-cyan-100 dark:bg-cyan-950 border-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-900 dark:border-cyan-900 dark:focus-within:outline-cyan-900': world.color === 'cyan',
'bg-sky-100 dark:bg-sky-950 border-sky-200 hover:bg-sky-50 dark:hover:bg-sky-900 dark:border-sky-900 dark:focus-within:outline-sky-900': world.color === 'sky',
'bg-blue-100 dark:bg-blue-950 border-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900 dark:border-blue-900 dark:focus-within:outline-blue-900': world.color === 'blue',
'bg-indigo-100 dark:bg-indigo-950 border-indigo-200 hover:bg-indigo-50 dark:hover:bg-indigo-900 dark:border-indigo-900 dark:focus-within:outline-indigo-900': world.color === 'indigo',
'bg-violet-100 dark:bg-violet-950 border-violet-200 hover:bg-violet-50 dark:hover:bg-violet-900 dark:border-violet-900 dark:focus-within:outline-violet-900': world.color === 'violet',
'bg-purple-100 dark:bg-purple-950 border-purple-200 hover:bg-purple-50 dark:hover:bg-purple-900 dark:border-purple-900 dark:focus-within:outline-purple-900': world.color === 'purple',
'bg-fuchsia-100 dark:bg-fuchsia-950 border-fuchsia-200 hover:bg-fuchsia-50 dark:hover:bg-fuchsia-900 dark:border-fuchsia-900 dark:focus-within:outline-fuchsia-900': world.color === 'fuchsia',
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': world.color === 'pink',
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': world.color === 'rose',
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': world.color === 'black',
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-300 dark:border-slate-500 dark:focus-within:outline-slate-100': world.color === 'white',
}"
>
<UiCardHeader>
<UiCardTitle>{{ world.name }}</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<p class="italic">{{ world.description }}</p>
<div class="flex gap-1 absolute top-4 right-4 z-20">
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="emit('on-edit')">
<PhPencil size="16" />
</UiButton>
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="emit('on-delete')">
<PhTrash size="16" />
</UiButton>
</div>
</UiCardContent>
<UiCardFooter>
<ul class="grid gap-2">
<li class="flex gap-1 items-center">
<PhFilePlus size="20" />
<span>{{ $t('common.createdAt', { createdAt }) }}</span>
</li>
<li v-if="updatedAt" class="flex gap-1 items-center">
<PhPencilSimpleLine size="20" />
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
</li>
</ul>
</UiCardFooter>
</UiCard>
</template>

View File

@@ -56,7 +56,9 @@ export default defineI18nConfig(() => ({
displayMode: "Display mode"
},
common: {
title: "Title"
title: "Title",
createdAt: "Created {createdAt}",
updatedAt: "Last updated {updatedAt}",
},
entity: {
category: {
@@ -81,6 +83,7 @@ export default defineI18nConfig(() => ({
nameSingular: "World",
namePlural: "Worlds",
addSingle: "Add a world",
addSingleFirst: "Add your first world !",
editSingle: "Edit world",
notFound: "World not found",
notFoundDescription: "The link is not valid or the world has been deleted / archived.",
@@ -110,12 +113,13 @@ export default defineI18nConfig(() => ({
namePublicSingular: "Public Calendar",
namePublicPlural: "Public Calendars",
addSingle: "Add a calendar",
addSingleFirst: "Add your first calendar !",
notFound: "Calendar not found",
notFoundDescription: "The link is not valid or the calendar has been deleted / archived.",
notFoundForWorld: "No calendar for this world… yet !",
backToList: "Go back to calendars",
isLoading: "Calendar is loading…",
hasXEvents: "This calendar has {count} public events",
hasXEvents: "{count} available events",
date: {
start: "Start date",
end: "End date",
@@ -307,7 +311,9 @@ export default defineI18nConfig(() => ({
displayMode: "Mode d'affichage",
},
common: {
title: "Titre"
title: "Titre",
createdAt: "Créé le {createdAt}",
updatedAt: "Dernière modification le {updatedAt}",
},
entity: {
category: {
@@ -332,6 +338,7 @@ export default defineI18nConfig(() => ({
nameSingular: "Monde",
namePlural: "Mondes",
addSingle: "Ajouter un monde",
addSingleFirst: "Ajouter votre premier monde !",
editSingle: "Modifier le monde",
notFound: "Aucun monde trouvé",
notFoundDescription: "Le lien n'est pas valide ou le monde a été supprimé / archivé.",
@@ -356,17 +363,18 @@ export default defineI18nConfig(() => ({
},
},
calendar: {
nameSingular: "Calendriers",
namePlural: "Calendrier",
nameSingular: "Calendrier",
namePlural: "Calendriers",
namePublicSingular: "Calendrier Public",
namePublicPlural: "Calendriers Publics",
addSingle: "Ajouter un calendrier",
addSingleFirst: "Ajouter votre premier calendrier !",
notFound: "Aucun calendrier trouvé",
notFoundDescription: "Le lien n'est pas valide ou le calendrier a été supprimé / archivé.",
notFoundForWorld: "Aucun calendrier pour ce monde… pour l'instant !",
backToList: "Retourner aux calendriers",
isLoading: "Chargement du calendrier…",
hasXEvents: "Ce calendrier contient {count} évènements publiques",
hasXEvents: "{count} évènements disponibles",
date: {
start: "Date de début",
end: "Date de fin",

View File

@@ -8,10 +8,12 @@ export interface World {
id?: number
name: string
description?: string
color?: RPGColor,
calendars?: Calendar[],
color?: RPGColor
calendars?: Calendar[]
gmId?: string
state?: WorldState
createdAt?: string
updatedAt?: string
}
export const postWorldSchema = z.object({

View File

@@ -24,6 +24,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.475.0",
"luxon": "^3.5.0",
"nuxt": "^3.15.4",
"pinia": "^3.0.1",
"radix-vue": "^1.9.16",
@@ -40,6 +41,7 @@
"@nuxtjs/supabase": "^1.4.6",
"@nuxtjs/tailwindcss": "^6.13.1",
"@stylistic/eslint-plugin-js": "^3.1.0",
"@types/luxon": "^3.4.2",
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"eslint": "^9.21.0",

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup>
import { PhCalendarDots } from "@phosphor-icons/vue";
import type { Calendar } from "~/models/CalendarConfig";
const { data: availableCalendars } = await useLazyFetch<{ data: Calendar[] }>("/api/calendars/query", { key: "explore-calendars" })
@@ -31,8 +32,9 @@ const { data: availableCalendars } = await useLazyFetch<{ data: Calendar[] }>("/
</UiCardHeader>
<UiCardContent>
<p>
{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}
<p class="flex items-center gap-1">
<PhCalendarDots size="24" />
<span>{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span>
</p>
</UiCardContent>
</UiCard>

View File

@@ -1,17 +1,16 @@
<script lang="ts" setup>
import { PhPencil, PhTrash } from "@phosphor-icons/vue";
import type { RealtimeChannel } from "@supabase/supabase-js"
import type { World } from "~/models/World";
const supabase = useSupabaseClient()
const { data: worlds } = await useLazyFetch<{ data: World[] }>("/api/worlds/query")
const user = useSupabaseUser()
definePageMeta({
middleware: ["auth-guard"]
})
const user = useSupabaseUser()
const { data: worlds } = await useLazyFetch<{ data: World[] }>("/api/worlds/query", { query: { gmId: user?.value!.id } })
const sortedWorlds = computed(() => worlds.value?.data ? [...worlds.value.data].sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
// Redirect user back home when they log out on the page
watch(user, (n) => {
@@ -48,7 +47,7 @@ function handleDeletedWorld(id: number) {
if (!worlds.value?.data) return
try {
worlds.value.data.splice(worlds.value.data.findIndex(w => w.id === id))
worlds.value.data.splice(worlds.value.data.findIndex(w => w.id === id), 1)
} catch (err) {
console.log(err)
}
@@ -72,7 +71,7 @@ onMounted(() => {
case "UPDATE":
if (!worlds.value?.data) return
worlds.value.data = (await $fetch("/api/worlds/query")).data as World[]
worlds.value.data = (await $fetch("/api/worlds/query", { query: { gmId: user?.value!.id } })).data as World[]
break
default:
@@ -132,54 +131,17 @@ function hideEditModal() {
</div>
<ul v-if="worlds?.data" class="grid lg:grid-cols-3 gap-2">
<li v-for="world in worlds.data" :key="world.id">
<UiCard
class="w-full transition-all"
:link="`/my/worlds/${world.id}`"
:class="{
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !world.color,
'bg-red-100 dark:bg-red-950 border-red-200 hover:bg-red-50 dark:hover:bg-red-900 dark:border-red-900 dark:focus-within:outline-red-900': world.color === 'red',
'bg-orange-100 dark:bg-orange-950 border-orange-200 hover:bg-orange-50 dark:hover:bg-orange-900 dark:border-orange-900 dark:focus-within:outline-orange-900': world.color === 'orange',
'bg-amber-100 dark:bg-amber-950 border-amber-200 hover:bg-amber-50 dark:hover:bg-amber-900 dark:border-amber-900 dark:focus-within:outline-amber-900': world.color === 'amber',
'bg-yellow-100 dark:bg-yellow-950 border-yellow-200 hover:bg-yellow-50 dark:hover:bg-yellow-900 dark:border-yellow-900 dark:focus-within:outline-yellow-900': world.color === 'yellow',
'bg-lime-100 dark:bg-lime-950 border-lime-200 hover:bg-lime-50 dark:hover:bg-lime-900 dark:border-lime-900 dark:focus-within:outline-lime-900': world.color === 'lime',
' bg-green-100 dark:bg-green-950 border-green-200 hover:bg-green-50 dark:hover:bg-green-900 dark:border-green-900 dark:focus-within:outline-green-900': world.color === 'green',
'bg-emerald-100 dark:bg-emerald-950 border-emerald-200 hover:bg-emerald-50 dark:hover:bg-emerald-900 dark:border-emerald-900 dark:focus-within:outline-emerald-900': world.color === 'emerald',
'bg-teal-100 dark:bg-teal-950 border-teal-200 hover:bg-teal-50 dark:hover:bg-teal-900 dark:border-teal-900 dark:focus-within:outline-teal-900': world.color === 'teal',
'bg-cyan-100 dark:bg-cyan-950 border-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-900 dark:border-cyan-900 dark:focus-within:outline-cyan-900': world.color === 'cyan',
'bg-sky-100 dark:bg-sky-950 border-sky-200 hover:bg-sky-50 dark:hover:bg-sky-900 dark:border-sky-900 dark:focus-within:outline-sky-900': world.color === 'sky',
'bg-blue-100 dark:bg-blue-950 border-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900 dark:border-blue-900 dark:focus-within:outline-blue-900': world.color === 'blue',
'bg-indigo-100 dark:bg-indigo-950 border-indigo-200 hover:bg-indigo-50 dark:hover:bg-indigo-900 dark:border-indigo-900 dark:focus-within:outline-indigo-900': world.color === 'indigo',
'bg-violet-100 dark:bg-violet-950 border-violet-200 hover:bg-violet-50 dark:hover:bg-violet-900 dark:border-violet-900 dark:focus-within:outline-violet-900': world.color === 'violet',
'bg-purple-100 dark:bg-purple-950 border-purple-200 hover:bg-purple-50 dark:hover:bg-purple-900 dark:border-purple-900 dark:focus-within:outline-purple-900': world.color === 'purple',
'bg-fuchsia-100 dark:bg-fuchsia-950 border-fuchsia-200 hover:bg-fuchsia-50 dark:hover:bg-fuchsia-900 dark:border-fuchsia-900 dark:focus-within:outline-fuchsia-900': world.color === 'fuchsia',
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': world.color === 'pink',
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': world.color === 'rose',
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': world.color === 'black',
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-300 dark:border-slate-500 dark:focus-within:outline-slate-100': world.color === 'white',
}"
>
<UiCardHeader>
<UiCardTitle>{{ world.name }}</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<p class="italic">{{ world.description }}</p>
<div class="flex gap-1 absolute top-4 right-4 z-20">
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="deployEditModal(world)">
<PhPencil size="16" />
</UiButton>
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="deployDeleteModal(world)">
<PhTrash size="16" />
</UiButton>
</div>
</UiCardContent>
</UiCard>
<li v-for="world in sortedWorlds" :key="world.id">
<WorldPreviewCard :world="world" @on-edit="() => deployEditModal(world)" @on-delete="() => deployDeleteModal(world)" />
</li>
<li class="w-fit">
<li class="md:w-fit">
<AddCard @on-click="() => isCreateWorldModalOpen = true">
<template v-if="worlds?.data?.length > 0">
{{ $t('entity.world.addSingle') }}
</template>
<template v-else>
{{ $t('entity.world.addSingleFirst') }}
</template>
</AddCard>
</li>
</ul>

View File

@@ -2,13 +2,14 @@
import type { RealtimeChannel } from "@supabase/supabase-js"
import type { World } from "~/models/World";
import type { Calendar } from "~/models/CalendarConfig";
import { PhArrowBendDoubleUpLeft, PhGlobeHemisphereWest, PhPencil, PhTrash } from "@phosphor-icons/vue";
import { PhArrowBendDoubleUpLeft, PhGlobeHemisphereWest, PhPencil } from "@phosphor-icons/vue";
const supabase = useSupabaseClient()
const route = useRoute()
const id = route.params.id
const { data: world, status } = await useFetch<{ data: World }>("/api/worlds/query", { query: { id, full: true } })
const sortedCalendars = computed(() => world.value?.data.calendars ? [...world.value.data.calendars].sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
definePageMeta({
middleware: ["auth-guard"]
@@ -54,7 +55,7 @@ function handleDeletedCalendar(id: number) {
if (!world.value) return
try {
world.value.data.calendars?.splice(world.value.data.calendars.findIndex(c => c.id === id))
world.value.data.calendars?.splice(world.value.data.calendars.findIndex(c => c.id === id), 1)
} catch (err) {
console.log(err)
}
@@ -187,36 +188,22 @@ function hideEditModal() {
</Heading>
</div>
<ul v-if="world.data.calendars && world.data.calendars?.length > 0" class="grid md:grid-cols-3 gap-2">
<li v-for="calendar in world.data.calendars" :key="calendar.id">
<UiCard
class="w-full transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900"
:link="`/my/calendars/${calendar.id}`"
>
<UiCardHeader>
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<p class="italic">Description future (ou alors des informations sur le nb d'évènements)</p>
<UiButton size="icon" variant="ghost" class="absolute top-2 right-2 z-20 hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="deployDeleteCalendarModal(calendar)">
<PhTrash size="16" />
</UiButton>
</UiCardContent>
</UiCard>
<ul class="grid md:grid-cols-3 gap-2">
<li v-for="calendar in sortedCalendars" :key="calendar.id">
<CalendarPreviewCard :calendar="calendar" @on-delete="() => deployDeleteCalendarModal(calendar)" />
</li>
<li class="w-fit">
<li class="md:w-fit">
<AddCard @on-click="() => isCreateCalendarModalOpen = true">
<template v-if="sortedCalendars.length > 0">
{{ $t('entity.calendar.addSingle') }}
</template>
<template v-else>
{{ $t('entity.calendar.addSingleFirst') }}
</template>
</AddCard>
</li>
</ul>
<template v-else>
<p class="pl-6 opacity-75 italic">
{{ $t('entity.calendar.notFoundForWorld') }}
</p>
</template>
</Spacing>
</section>

17
pnpm-lock.yaml generated
View File

@@ -41,6 +41,9 @@ importers:
lucide-vue-next:
specifier: ^0.475.0
version: 0.475.0(vue@3.5.13(typescript@5.7.3))
luxon:
specifier: ^3.5.0
version: 3.5.0
nuxt:
specifier: ^3.15.4
version: 3.15.4(@parcel/watcher@2.5.1)(@types/node@22.13.5)(db0@0.2.4)(encoding@0.1.13)(eslint@9.21.0(jiti@2.4.2))(ioredis@5.5.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.8)(sass@1.85.1)(terser@5.39.0)(typescript@5.7.3)(vite@6.2.0(@types/node@22.13.5)(jiti@2.4.2)(sass@1.85.1)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
@@ -84,6 +87,9 @@ importers:
'@stylistic/eslint-plugin-js':
specifier: ^3.1.0
version: 3.1.0(eslint@9.21.0(jiti@2.4.2))
'@types/luxon':
specifier: ^3.4.2
version: 3.4.2
'@typescript-eslint/eslint-plugin':
specifier: ^8.25.0
version: 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)
@@ -1632,6 +1638,9 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/luxon@3.4.2':
resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==}
'@types/node@22.13.5':
resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==}
@@ -3411,6 +3420,10 @@ packages:
peerDependencies:
vue: '>=3.0.1'
luxon@3.5.0:
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
engines: {node: '>=12'}
magic-string-ast@0.7.0:
resolution: {integrity: sha512-686fgAHaJY7wLTFEq7nnKqeQrhqmXB19d1HnqT35Ci7BN6hbAYLZUezTQ062uUHM7ggZEQlqJ94Ftls+KDXU8Q==}
engines: {node: '>=16.14.0'}
@@ -6786,6 +6799,8 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/luxon@3.4.2': {}
'@types/node@22.13.5':
dependencies:
undici-types: 6.20.0
@@ -8798,6 +8813,8 @@ snapshots:
dependencies:
vue: 3.5.13(typescript@5.7.3)
luxon@3.5.0: {}
magic-string-ast@0.7.0:
dependencies:
magic-string: 0.30.17

View File

@@ -4,15 +4,16 @@ import type { World } from "~/models/World";
const querySchema = z.object({
id: z.number({ coerce: true }).positive().int().optional(),
full: z.boolean({ coerce: true }).optional()
full: z.boolean({ coerce: true }).optional(),
gmId: z.string().optional(),
})
export default defineEventHandler(async (event) => {
const client = await serverSupabaseClient(event)
const query = await getValidatedQuery(event, querySchema.parse)
const fullFields = "id, name, description, color, calendars (id, name, color, today)"
const partialFields = "id, name, description, color"
const fullFields = "id, name, description, color, createdAt:created_at, updatedAt:updated_at, calendars (id, name, color, today, eventNb:calendar_events(count))"
const partialFields = "id, name, description, color, createdAt:created_at, updatedAt:updated_at, calendars (eventNb:calendar_events(count))"
let output
@@ -23,10 +24,11 @@ export default defineEventHandler(async (event) => {
}
if (query.id) {
// const eventNb = await client.from('worlds').select('events:calendars(calendar_events(id))').order('events_count')
return output.eq("id", query.id).single<World>()
}
if (query.gmId) {
return output.eq("gm_id", query.gmId).overrideTypes<World[]>()
}
return output.returns<World[]>()
return output.overrideTypes<World[]>()
})

View File

@@ -44,6 +44,8 @@ create table public.worlds (
description text,
color app_colors default 'black',
state world_state default 'draft',
created_at timestamptz default now(),
updated_at timestamptz,
gm_id uuid references public.users on delete cascade
);
comment on table public.worlds is 'Worlds belonging to a single user ; a game master.';
@@ -66,6 +68,8 @@ create table public.calendars (
today json not null,
color app_colors default 'black',
state calendar_state default 'draft',
created_at timestamptz default now(),
updated_at timestamptz,
world_id bigint references public.worlds on delete cascade not null
);
comment on table public.calendars is 'Calendar settings and configuration attached to a single world.';
@@ -100,6 +104,8 @@ create table public.calendar_events (
category bigint references public.calendar_event_categories on delete cascade,
hidden boolean default false,
wiki text,
created_at timestamptz default now(),
updated_at timestamptz,
calendar_id bigint references public.calendars on delete cascade not null
);
comment on table public.calendar_events is 'Events linked to a world';
@@ -126,6 +132,8 @@ create table public.character_categories (
id bigint generated by default as identity primary key,
name text not null,
color app_colors default 'black',
created_at timestamptz default now(),
updated_at timestamptz,
unique (name)
);
comment on table public.character_categories is 'Categories describing characters';
@@ -141,6 +149,8 @@ create table public.characters (
hidden_birth boolean,
hidden_death boolean,
wiki text,
created_at timestamptz default now(),
updated_at timestamptz,
world_id bigint references public.worlds on delete cascade not null
);
comment on table public.characters is 'Characters linked to a world';