Commit stuff in case my computer explodes

This commit is contained in:
Alexis
2025-03-06 20:34:59 +01:00
parent d46da12745
commit 31a4316186
10 changed files with 333 additions and 28 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhCalendarDots, PhFilePlus, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue"; import { PhCalendarDots, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "~/models/CalendarConfig";
@@ -9,7 +9,7 @@ const props = defineProps<{
showActions?: boolean, showActions?: boolean,
}>() }>()
const emit = defineEmits(["on-delete"]) const emit = defineEmits(["on-edit", "on-delete"])
const { locale } = useI18n(); const { locale } = useI18n();
@@ -36,15 +36,18 @@ const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calen
<span>{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span> <span>{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span>
</p> </p>
<UiButton <div
v-if="isOwner && showActions" v-if="isOwner && showActions"
size="icon" class="flex gap-1 absolute top-4 right-4 z-20"
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 size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="emit('on-edit')">
</UiButton> <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> </UiCardContent>
<UiCardFooter> <UiCardFooter>

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue";
import type { Calendar } from "~/models/CalendarConfig";
const props = defineProps<{
calendarId: number,
modalState?: boolean
}>()
const { data: calendar } = await useFetch<{ data: Calendar }>("/api/calendars/query", { query: { id: props.calendarId } })
const calendarSkeletonName = ref<string>("")
function onChangedName(newName: string) {
calendarSkeletonName.value = newName
}
const emit = defineEmits(["on-close"])
function handleClose() {
emit("on-close")
setTimeout(() => calendarSkeletonName.value = "", 100)
}
</script>
<template>
<UiAlertDialog :open="modalState">
<UiAlertDialogContent class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl gap-6" @close-auto-focus="(e) => e.preventDefault()">
<UiAlertDialogTitle>
<span class="text-2xl">
<strong class="font-bold">{{ calendar?.data.world?.name }}</strong>
<span class="opacity-30"> — </span>
<span v-if="calendarSkeletonName">
{{ calendarSkeletonName }}
</span>
<span v-else>
{{ $t('entity.calendar.updateDialog.title') }}
</span>
</span>
</UiAlertDialogTitle>
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClose">
<PhX size="20" />
</UiButton>
<CalendarFormUpdate v-if="calendar?.data" :calendar="calendar.data" @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -79,21 +79,21 @@ function handleFormCancel() {
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<PhWrench size="18" weight="fill" /> <PhWrench size="18" weight="fill" />
{{ $t('entity.calendar.createDialog.tabs.general.title') }} {{ $t('entity.calendar.actionDialog.tabs.general.title') }}
</div> </div>
</UiTabsTrigger> </UiTabsTrigger>
<UiTabsTrigger value="months" class="font-bold"> <UiTabsTrigger value="months" class="font-bold">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<PhCalendarDots size="18" weight="fill" /> <PhCalendarDots size="18" weight="fill" />
{{ $t('entity.calendar.createDialog.tabs.months.title') }} {{ $t('entity.calendar.actionDialog.tabs.months.title') }}
</div> </div>
</UiTabsTrigger> </UiTabsTrigger>
<UiTabsTrigger value="today" class="font-bold"> <UiTabsTrigger value="today" class="font-bold">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<PhAlarm size="18" weight="fill" /> <PhAlarm size="18" weight="fill" />
{{ $t('entity.calendar.createDialog.tabs.today.title') }} {{ $t('entity.calendar.actionDialog.tabs.today.title') }}
</div> </div>
</UiTabsTrigger> </UiTabsTrigger>
</UiTabsList> </UiTabsList>

View File

@@ -0,0 +1,145 @@
<script lang="ts" setup>
import type { Calendar } from "~/models/CalendarConfig";
import { PhAlarm, PhCalendarDots, PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
const props = defineProps<{
calendar: Calendar | null,
}>()
const calendarSkeleton = ref<Calendar>({ ...props.calendar } as Calendar)
onMounted(() => {
calendarSkeleton.value = { ...props.calendar } as Calendar
})
type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global")
/**
* === Form Validation ===
*/
/** Whether the skeleton has valid month data */
const validSkeletonMonths = computed(() => calendarSkeleton.value.months.length > 0)
/** Whether the skeleton has a valid name */
const validSkeletonGeneral = computed(() => calendarSkeleton.value.name)
/** Whether all the data checks above are a-ok */
const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeletonMonths.value)
/** Send the data to the store for validation */
const isUpdatingCalendar = ref<boolean>(false)
async function handleSubmit() {
try {
isUpdatingCalendar.value = true
await $fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: { ...calendarSkeleton.value, worldId: props.calendar?.world?.id } })
emit("on-close")
} catch (err) {
console.log(err)
} finally {
isUpdatingCalendar.value = false
}
}
/**
* === Watch for name changes to display above ===
*/
const emit = defineEmits<{
// eslint-disable-next-line no-unused-vars
(e: "on-changed-name", calendarName: string): void
// eslint-disable-next-line no-unused-vars
(e: "on-close"): void
}>()
/** Hook to emit a debounced event for the changed skeleton name */
const handleNameChange = useDebounceFn(() => {
emit("on-changed-name", calendarSkeleton.value.name)
}, 400)
function handleFormCancel() {
emit("on-close")
}
</script>
<template>
<template v-if="calendarSkeleton">
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<UiTabs v-model:model-value="activeTab">
<UiTabsList class="grid w-full grid-cols-3 mb-4">
<UiTabsTrigger value="global" class="font-bold">
<div class="flex items-center gap-1">
<PhWrench size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.general.title') }}
</div>
</UiTabsTrigger>
<UiTabsTrigger value="months" class="font-bold">
<div class="flex items-center gap-1">
<PhCalendarDots size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.months.title') }}
</div>
</UiTabsTrigger>
<UiTabsTrigger value="today" class="font-bold">
<div class="flex items-center gap-1">
<PhAlarm size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.today.title') }}
</div>
</UiTabsTrigger>
</UiTabsList>
<UiTabsContent value="global" class="grid gap-4">
<input
id="new-calendar-name"
v-model="calendarSkeleton.name"
type="text"
name="new-calendar-name"
required
:placeholder="$t('common.title')"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
@input="handleNameChange"
>
<div class="-mx-1 px-1 grid gap-3">
<UiLabel for="new-calendar-state">
{{ $t('ui.contentState.label') }}
</UiLabel>
<InputContentState id="new-calendar-state" v-model="calendarSkeleton.state" />
</div>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-calendar-color">
{{ $t('ui.colors.label') }}
</UiLabel>
<InputColor id="new-calendar-color" v-model="calendarSkeleton.color" />
</div>
</UiTabsContent>
<UiTabsContent value="months">
<CalendarInputMonthList v-model:model-value="calendarSkeleton.months" />
</UiTabsContent>
<UiTabsContent value="today">
<CalendarInputTodaySelect v-model:model-value="calendarSkeleton.today" :available-months="calendarSkeleton.months"/>
</UiTabsContent>
</UiTabs>
<footer class="flex justify-end gap-2 mt-6">
<UiButton type="button" variant="destructive" @click="handleFormCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
<UiButton type="submit" :disabled="!validSkeleton || isUpdatingCalendar">
<Transition name="fade">
<PhCircleNotch v-if="isUpdatingCalendar" size="20" class="opacity-50 animate-spin"/>
</Transition>
{{ $t('ui.action.save') }}
</UiButton>
</footer>
</form>
</template>
</template>

View File

@@ -197,8 +197,7 @@ export default defineI18nConfig(() => ({
title: "The event \"{event}\" has been successfuly deleted.", title: "The event \"{event}\" has been successfuly deleted.",
}, },
}, },
createDialog: { actionDialog: {
title: "Create a calendar",
tabs: { tabs: {
general: { general: {
title: "General", title: "General",
@@ -211,6 +210,9 @@ export default defineI18nConfig(() => ({
}, },
} }
}, },
createDialog: {
title: "Create a calendar",
},
deleteDialog: { deleteDialog: {
title: "Are you sure you want to delete this calendar ?", title: "Are you sure you want to delete this calendar ?",
subtitle: "Its events won't be accessible anymore and you won't be able to retrieve the deleted data !", subtitle: "Its events won't be accessible anymore and you won't be able to retrieve the deleted data !",
@@ -460,8 +462,7 @@ export default defineI18nConfig(() => ({
title: "L'évènement \"{event}\" a été supprimé avec succès.", title: "L'évènement \"{event}\" a été supprimé avec succès.",
}, },
}, },
createDialog: { actionDialog: {
title: "Créer un calendrier",
tabs: { tabs: {
general: { general: {
title: "Général", title: "Général",
@@ -474,6 +475,12 @@ export default defineI18nConfig(() => ({
}, },
} }
}, },
createDialog: {
title: "Créer un calendrier",
},
updateDialog: {
title: "Modifier le calendrier",
},
deleteDialog: { deleteDialog: {
title: "Êtes-vous sûr de supprimer ce calendrier ?", title: "Êtes-vous sûr de supprimer ce calendrier ?",
subtitle: "Les évènements ne seront plus accessibles et vous ne pourrez plus récupérer les données !", subtitle: "Les évènements ne seront plus accessibles et vous ne pourrez plus récupérer les données !",

View File

@@ -34,4 +34,5 @@ export const postCalendarSchema = z.object({
color: z.string().optional().nullable(), color: z.string().optional().nullable(),
months: z.array(calendarMonthSchema).min(1), months: z.array(calendarMonthSchema).min(1),
worldId: z.number().int(), worldId: z.number().int(),
state: z.string().optional().nullable().default("draft"),
}) })

View File

@@ -24,12 +24,6 @@ watch(user, (n) => {
} }
}) })
const isCreateCalendarModalOpen = ref<boolean>(false)
function hideCreateDialog() {
isCreateCalendarModalOpen.value = false
}
/** /**
* === Subscriptions === * === Subscriptions ===
*/ */
@@ -120,14 +114,28 @@ onUnmounted(() => {
}) })
const markedCalendar = ref<Calendar | null>(null) const markedCalendar = ref<Calendar | null>(null)
const isDeleteCalendarModalOpen = ref<boolean>(false)
const isEditWorldModalOpen = ref<boolean>(false) const isEditWorldModalOpen = ref<boolean>(false)
const isCreateCalendarModalOpen = ref<boolean>(false)
const isUpdateCalendarModalOpen = ref<boolean>(false)
const isDeleteCalendarModalOpen = ref<boolean>(false)
function hideCreateDialog() {
isCreateCalendarModalOpen.value = false
}
function deployUpdateDialog(calendar: Calendar) {
markedCalendar.value = calendar
isUpdateCalendarModalOpen.value = true
}
function hideUpdateDialog() {
isUpdateCalendarModalOpen.value = false
}
function deployDeleteCalendarModal(calendar: Calendar) { function deployDeleteCalendarModal(calendar: Calendar) {
isDeleteCalendarModalOpen.value = true isDeleteCalendarModalOpen.value = true
markedCalendar.value = calendar markedCalendar.value = calendar
} }
function hideDeleteCalendarModal() { function hideDeleteCalendarModal() {
isDeleteCalendarModalOpen.value = false isDeleteCalendarModalOpen.value = false
markedCalendar.value = null markedCalendar.value = null
@@ -136,7 +144,6 @@ function hideDeleteCalendarModal() {
function deployEditModal() { function deployEditModal() {
isEditWorldModalOpen.value = true isEditWorldModalOpen.value = true
} }
function hideEditModal() { function hideEditModal() {
isEditWorldModalOpen.value = false isEditWorldModalOpen.value = false
} }
@@ -193,7 +200,12 @@ function hideEditModal() {
<ul class="grid md:grid-cols-3 gap-2"> <ul class="grid md:grid-cols-3 gap-2">
<li v-for="calendar in sortedCalendars" :key="calendar.id"> <li v-for="calendar in sortedCalendars" :key="calendar.id">
<CalendarPreviewCard :calendar="calendar" :gm-id="world.data.gmId" show-actions @on-delete="() => deployDeleteCalendarModal(calendar)" /> <CalendarPreviewCard
:calendar="calendar"
:gm-id="world.data.gmId"
show-actions
@on-edit="() => deployUpdateDialog(calendar)"
@on-delete="() => deployDeleteCalendarModal(calendar)" />
</li> </li>
<li class="md:w-fit"> <li class="md:w-fit">
@@ -212,6 +224,7 @@ function hideEditModal() {
<WorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" /> <WorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" />
<CalendarDialogCreate :world="world.data" :modal-state="isCreateCalendarModalOpen" @on-close="hideCreateDialog" /> <CalendarDialogCreate :world="world.data" :modal-state="isCreateCalendarModalOpen" @on-close="hideCreateDialog" />
<CalendarDialogUpdate v-if="markedCalendar?.id" :calendar-id="markedCalendar.id" :modal-state="isUpdateCalendarModalOpen" @on-close="hideUpdateDialog" />
<CalendarDialogDelete :calendar="markedCalendar" :modal-state="isDeleteCalendarModalOpen" @on-close="hideDeleteCalendarModal"/> <CalendarDialogDelete :calendar="markedCalendar" :modal-state="isDeleteCalendarModalOpen" @on-close="hideDeleteCalendarModal"/>
</template> </template>
<template v-else> <template v-else>

View File

@@ -0,0 +1,78 @@
import { z } from "zod"
import { serverSupabaseClient } from "#supabase/server"
import type { Calendar} from "~/models/CalendarConfig";
import { postCalendarSchema } from "~/models/CalendarConfig"
const paramsSchema = z.object({
id: z.number({ coerce: true }).positive().int()
})
export default defineEventHandler(async (event) => {
const client = await serverSupabaseClient(event)
const { data: params, error: paramsError} = await getValidatedRouterParams(event, paramsSchema.safeParse)
const { data: bodyData, error: bodyError } = await readValidatedBody(event, body => postCalendarSchema.safeParse(body))
if (paramsError) {
throw createError({
cause: "Utilisateur",
fatal: false,
message: "L'identifiant du calendrier est manquant ou mal renseigné.",
status: 401,
})
}
if (bodyError) {
const error = createError({
cause: "Utilisateur",
fatal: false,
statusCode: 401,
statusMessage: "Validation Error",
message: "Erreur de validation du schéma, probablement dûe à une erreur utilisateur.",
data: {
errors: bodyError.issues.map(issue => ({
path: issue.path,
message: issue.message,
code: issue.code
}))
}
})
throw error
}
try {
const { data, error } = await client
.from("calendars")
.update(
{
name: bodyData.name,
world_id: bodyData.worldId,
today: bodyData.today,
state: bodyData.state
} as never
)
.eq("id", params.id)
.select(`
id,
name,
color,
state,
createdAt:created_at,
updatedAt:updated_at
`)
.single<Calendar>()
if (error) throw error
return data
} catch (err) {
console.log(err)
throw createError({
cause: "Serveur",
status: 500,
fatal: false,
message: "Une erreur inconnue est survenue."
})
}
})

View File

@@ -21,7 +21,12 @@ export default defineEventHandler(async (event) => {
state, state,
createdAt:created_at, createdAt:created_at,
updatedAt:updated_at, updatedAt:updated_at,
eventNb:calendar_events(count) eventNb:calendar_events(count),
world:worlds (
id,
name,
gmId:gm_id
)
` `
const fullFields = ` const fullFields = `
@@ -48,6 +53,7 @@ export default defineEventHandler(async (event) => {
eventNb:calendar_events(count), eventNb:calendar_events(count),
world:worlds ( world:worlds (
id, id,
name,
gmId:gm_id gmId:gm_id
) )
` `

View File

@@ -242,22 +242,25 @@ create policy "Allow GMs to see their calendars" on public.calendars for select
and worlds.gm_id = auth.uid() and worlds.gm_id = auth.uid()
) )
); );
create policy "Allow GMs to add calendars to their worldd" on public.calendars for insert with check ( create policy "Allow GMs to add calendars to their world" on public.calendars for insert with check (
exists ( exists (
select 1 from worlds select 1 from worlds
where worlds.id = calendars.world_id where worlds.id = calendars.world_id
and worlds.gm_id = auth.uid()
) )
); );
create policy "Allow GMs to edit their calendars" on public.calendars for update with check ( create policy "Allow GMs to edit their calendars" on public.calendars for update with check (
exists ( exists (
select 1 from worlds select 1 from worlds
where worlds.id = calendars.world_id where worlds.id = calendars.world_id
and worlds.gm_id = auth.uid()
) )
); );
create policy "Allow GMs to delete their calendars" on public.calendars for delete using ( create policy "Allow GMs to delete their calendars" on public.calendars for delete using (
exists ( exists (
select 1 from worlds select 1 from worlds
where worlds.id = calendars.world_id where worlds.id = calendars.world_id
and worlds.gm_id = auth.uid()
) )
); );