Commit stuff in case my computer explodes
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<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 type { Calendar } from "~/models/CalendarConfig";
|
||||
|
||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
showActions?: boolean,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["on-delete"])
|
||||
const emit = defineEmits(["on-edit", "on-delete"])
|
||||
|
||||
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>
|
||||
</p>
|
||||
|
||||
<UiButton
|
||||
<div
|
||||
v-if="isOwner && showActions"
|
||||
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')"
|
||||
class="flex gap-1 absolute top-4 right-4 z-20"
|
||||
>
|
||||
<PhTrash size="16" />
|
||||
</UiButton>
|
||||
<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>
|
||||
|
||||
49
components/calendar/dialog/Update.vue
Normal file
49
components/calendar/dialog/Update.vue
Normal 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>
|
||||
@@ -79,21 +79,21 @@ function handleFormCancel() {
|
||||
<div class="flex items-center gap-1">
|
||||
<PhWrench size="18" weight="fill" />
|
||||
|
||||
{{ $t('entity.calendar.createDialog.tabs.general.title') }}
|
||||
{{ $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.createDialog.tabs.months.title') }}
|
||||
{{ $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.createDialog.tabs.today.title') }}
|
||||
{{ $t('entity.calendar.actionDialog.tabs.today.title') }}
|
||||
</div>
|
||||
</UiTabsTrigger>
|
||||
</UiTabsList>
|
||||
|
||||
145
components/calendar/form/Update.vue
Normal file
145
components/calendar/form/Update.vue
Normal 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>
|
||||
@@ -197,8 +197,7 @@ export default defineI18nConfig(() => ({
|
||||
title: "The event \"{event}\" has been successfuly deleted.",
|
||||
},
|
||||
},
|
||||
createDialog: {
|
||||
title: "Create a calendar",
|
||||
actionDialog: {
|
||||
tabs: {
|
||||
general: {
|
||||
title: "General",
|
||||
@@ -211,6 +210,9 @@ export default defineI18nConfig(() => ({
|
||||
},
|
||||
}
|
||||
},
|
||||
createDialog: {
|
||||
title: "Create a calendar",
|
||||
},
|
||||
deleteDialog: {
|
||||
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 !",
|
||||
@@ -460,8 +462,7 @@ export default defineI18nConfig(() => ({
|
||||
title: "L'évènement \"{event}\" a été supprimé avec succès.",
|
||||
},
|
||||
},
|
||||
createDialog: {
|
||||
title: "Créer un calendrier",
|
||||
actionDialog: {
|
||||
tabs: {
|
||||
general: {
|
||||
title: "Général",
|
||||
@@ -474,6 +475,12 @@ export default defineI18nConfig(() => ({
|
||||
},
|
||||
}
|
||||
},
|
||||
createDialog: {
|
||||
title: "Créer un calendrier",
|
||||
},
|
||||
updateDialog: {
|
||||
title: "Modifier le calendrier",
|
||||
},
|
||||
deleteDialog: {
|
||||
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 !",
|
||||
|
||||
@@ -34,4 +34,5 @@ export const postCalendarSchema = z.object({
|
||||
color: z.string().optional().nullable(),
|
||||
months: z.array(calendarMonthSchema).min(1),
|
||||
worldId: z.number().int(),
|
||||
state: z.string().optional().nullable().default("draft"),
|
||||
})
|
||||
|
||||
@@ -24,12 +24,6 @@ watch(user, (n) => {
|
||||
}
|
||||
})
|
||||
|
||||
const isCreateCalendarModalOpen = ref<boolean>(false)
|
||||
|
||||
function hideCreateDialog() {
|
||||
isCreateCalendarModalOpen.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* === Subscriptions ===
|
||||
*/
|
||||
@@ -120,14 +114,28 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
const markedCalendar = ref<Calendar | null>(null)
|
||||
const isDeleteCalendarModalOpen = 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) {
|
||||
isDeleteCalendarModalOpen.value = true
|
||||
markedCalendar.value = calendar
|
||||
}
|
||||
|
||||
function hideDeleteCalendarModal() {
|
||||
isDeleteCalendarModalOpen.value = false
|
||||
markedCalendar.value = null
|
||||
@@ -136,7 +144,6 @@ function hideDeleteCalendarModal() {
|
||||
function deployEditModal() {
|
||||
isEditWorldModalOpen.value = true
|
||||
}
|
||||
|
||||
function hideEditModal() {
|
||||
isEditWorldModalOpen.value = false
|
||||
}
|
||||
@@ -193,7 +200,12 @@ function hideEditModal() {
|
||||
|
||||
<ul class="grid md:grid-cols-3 gap-2">
|
||||
<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 class="md:w-fit">
|
||||
@@ -212,6 +224,7 @@ function hideEditModal() {
|
||||
|
||||
<WorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" />
|
||||
<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"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
78
server/api/calendars/[id].patch.ts
Normal file
78
server/api/calendars/[id].patch.ts
Normal 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."
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -21,7 +21,12 @@ export default defineEventHandler(async (event) => {
|
||||
state,
|
||||
createdAt:created_at,
|
||||
updatedAt:updated_at,
|
||||
eventNb:calendar_events(count)
|
||||
eventNb:calendar_events(count),
|
||||
world:worlds (
|
||||
id,
|
||||
name,
|
||||
gmId:gm_id
|
||||
)
|
||||
`
|
||||
|
||||
const fullFields = `
|
||||
@@ -48,6 +53,7 @@ export default defineEventHandler(async (event) => {
|
||||
eventNb:calendar_events(count),
|
||||
world:worlds (
|
||||
id,
|
||||
name,
|
||||
gmId:gm_id
|
||||
)
|
||||
`
|
||||
|
||||
@@ -242,22 +242,25 @@ create policy "Allow GMs to see their calendars" on public.calendars for select
|
||||
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 (
|
||||
select 1 from worlds
|
||||
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 (
|
||||
exists (
|
||||
select 1 from worlds
|
||||
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 (
|
||||
exists (
|
||||
select 1 from worlds
|
||||
where worlds.id = calendars.world_id
|
||||
and worlds.gm_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user