Merge branch 'features/nuxt-supabase' into features/nuxt
This commit is contained in:
@@ -7,10 +7,12 @@ import CenturyLayout from './state/centennially/Layout.vue'
|
|||||||
import DecadeLayout from './state/decennially/Layout.vue'
|
import DecadeLayout from './state/decennially/Layout.vue'
|
||||||
import YearLayout from './state/yearly/Layout.vue'
|
import YearLayout from './state/yearly/Layout.vue'
|
||||||
|
|
||||||
|
const { isAdvancedSearchOpen, isEditEventModalOpen } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const worldId = route.params.id
|
const worldId = route.params.id
|
||||||
|
|
||||||
const { setMonths, setDefaultDate, currentConfig, selectedDate, jumpToDate } = useCalendar()
|
const { setCalendarId, setMonths, setDefaultDate, currentConfig, selectedDate, jumpToDate } = useCalendar()
|
||||||
const { setEvents } = useCalendarEvents()
|
const { setEvents } = useCalendarEvents()
|
||||||
const { setCharacters } = useCharacters()
|
const { setCharacters } = useCharacters()
|
||||||
|
|
||||||
@@ -20,6 +22,12 @@ const { data: characters, pending: charPending, refresh: charRefresh } = await u
|
|||||||
if (!calendar.value) {
|
if (!calendar.value) {
|
||||||
await calRefresh()
|
await calRefresh()
|
||||||
} else {
|
} else {
|
||||||
|
if (calendar.value?.data?.id) {
|
||||||
|
setCalendarId(calendar.value?.data?.id)
|
||||||
|
}
|
||||||
|
if (calendar.value?.data?.months) {
|
||||||
|
setMonths(calendar.value?.data?.months)
|
||||||
|
}
|
||||||
if (calendar.value?.data?.months) {
|
if (calendar.value?.data?.months) {
|
||||||
setMonths(calendar.value?.data?.months)
|
setMonths(calendar.value?.data?.months)
|
||||||
}
|
}
|
||||||
@@ -40,6 +48,9 @@ if (!characters.value) {
|
|||||||
|
|
||||||
watch(calPending, (newStatus) => {
|
watch(calPending, (newStatus) => {
|
||||||
if (!newStatus) {
|
if (!newStatus) {
|
||||||
|
if (calendar.value?.data?.id) {
|
||||||
|
setCalendarId(calendar.value?.data?.id)
|
||||||
|
}
|
||||||
if (calendar.value?.data?.months) {
|
if (calendar.value?.data?.months) {
|
||||||
setMonths(calendar.value?.data?.months)
|
setMonths(calendar.value?.data?.months)
|
||||||
}
|
}
|
||||||
@@ -97,5 +108,8 @@ onMounted(() => {
|
|||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<CalendarSearch v-model:model-value="isAdvancedSearchOpen" />
|
||||||
|
<CalendarFormUpdateEvent v-model:model-value="isEditEventModalOpen" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ const isStartEvent = spansMultipleDays && areDatesIdentical(props.tileDate, prop
|
|||||||
const isEndEvent =
|
const isEndEvent =
|
||||||
spansMultipleDays && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate)
|
spansMultipleDays && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate)
|
||||||
|
|
||||||
const isPopoverOpen = ref<boolean>(false)
|
const isPopoverDetailsOpen = ref<boolean>(false)
|
||||||
|
|
||||||
function handleClosePopover() {
|
function handleClosePopover() {
|
||||||
isPopoverOpen.value = false
|
isPopoverDetailsOpen.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiPopover v-model:open="isPopoverOpen">
|
<UiPopover v-model:open="isPopoverDetailsOpen">
|
||||||
<UiPopoverTrigger as-child>
|
<UiPopoverTrigger as-child>
|
||||||
<button
|
<button
|
||||||
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
|
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
|
||||||
@@ -60,7 +60,6 @@ function handleClosePopover() {
|
|||||||
</UiPopoverTrigger>
|
</UiPopoverTrigger>
|
||||||
|
|
||||||
<CalendarEventDetails
|
<CalendarEventDetails
|
||||||
v-once
|
|
||||||
:event
|
:event
|
||||||
:spans-multiple-days
|
:spans-multiple-days
|
||||||
:is-start-event
|
:is-start-event
|
||||||
|
|||||||
@@ -2,19 +2,18 @@
|
|||||||
import type { RPGDate } from '@/models/Date'
|
import type { RPGDate } from '@/models/Date'
|
||||||
import type { CalendarEvent } from '@/models/CalendarEvent'
|
import type { CalendarEvent } from '@/models/CalendarEvent'
|
||||||
import { useCalendar } from '@/stores/CalendarStore'
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
import { useCalendarEvents } from '@/stores/EventStore'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PhHourglassMedium,
|
PhHourglassMedium,
|
||||||
PhAlarm,
|
PhAlarm,
|
||||||
PhHourglassHigh,
|
PhHourglassHigh,
|
||||||
PhHourglassLow,
|
PhHourglassLow,
|
||||||
PhArrowBendDoubleUpLeft,
|
PhDotsThreeOutlineVertical
|
||||||
PhArrowBendDoubleUpRight
|
|
||||||
} from '@phosphor-icons/vue'
|
} from '@phosphor-icons/vue'
|
||||||
|
|
||||||
const { defaultDate, getFormattedDateTitle, jumpToDate } = useCalendar()
|
const { defaultDate, getFormattedDateTitle, jumpToDate, revealEditEventModal, getRelativeString } = useCalendar()
|
||||||
const { getRelativeEventFromEvent } = useCalendarEvents()
|
const { deleteEvent } = useCalendarEvents()
|
||||||
|
const { lastActiveEvent } = storeToRefs(useCalendarEvents())
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
event: CalendarEvent
|
event: CalendarEvent
|
||||||
@@ -23,11 +22,10 @@ const props = defineProps<{
|
|||||||
isEndEvent?: boolean
|
isEndEvent?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
// Ref for the popover
|
||||||
(e: 'query:close-popover'): void
|
const eventDetails = ref<HTMLElement>()
|
||||||
}>()
|
|
||||||
|
|
||||||
const { getRelativeString } = useCalendar()
|
const emit = defineEmits(['query:close-popover'])
|
||||||
|
|
||||||
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
|
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
|
||||||
const dateDuration: string | null = props.event.endDate
|
const dateDuration: string | null = props.event.endDate
|
||||||
@@ -39,14 +37,15 @@ function handleJumpToDate(date: RPGDate) {
|
|||||||
emit('query:close-popover')
|
emit('query:close-popover')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
|
/**
|
||||||
try {
|
* Edit event
|
||||||
const { targetDate } = getRelativeEventFromEvent(props.event, position, props.isEndEvent)
|
*/
|
||||||
|
const commandMenuOpened = ref(false)
|
||||||
|
|
||||||
handleJumpToDate(targetDate)
|
function deployEditModal() {
|
||||||
} catch (err) {
|
lastActiveEvent.value = { ...props.event }
|
||||||
console.log(err)
|
revealEditEventModal()
|
||||||
}
|
commandMenuOpened.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -80,7 +79,8 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
|
|||||||
'border-yellow-600': event.category?.name === 'commerce'
|
'border-yellow-600': event.category?.name === 'commerce'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="grid gap-1">
|
<div ref="eventDetails" class="grid gap-1">
|
||||||
|
<header class="pr-12">
|
||||||
<div class="text-lg font-semibold">
|
<div class="text-lg font-semibold">
|
||||||
{{ event.title }}
|
{{ event.title }}
|
||||||
</div>
|
</div>
|
||||||
@@ -96,6 +96,7 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
|
|||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="mb-1 space-y-1">
|
<div class="mb-1 space-y-1">
|
||||||
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||||
@@ -133,7 +134,27 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav v-if="event.startDate && event.endDate" class="absolute -top-2 right-2 flex gap-2">
|
<menu class="absolute top-4 right-4">
|
||||||
|
<UiPopover v-model:open="commandMenuOpened">
|
||||||
|
<UiPopoverTrigger as-child>
|
||||||
|
<UiButton size="icon" variant="ghost">
|
||||||
|
<PhDotsThreeOutlineVertical size="20" weight="fill" />
|
||||||
|
</UiButton>
|
||||||
|
</UiPopoverTrigger>
|
||||||
|
<UiPopoverContent class="w-fit p-0" align="start" side="right" :collision-padding="20" >
|
||||||
|
<UiCommand>
|
||||||
|
<UiCommandList>
|
||||||
|
<UiCommandGroup>
|
||||||
|
<UiCommandItem value="edit-event" @select="deployEditModal"> Modifier </UiCommandItem>
|
||||||
|
<UiCommandItem value="delete-event" @select="deleteEvent(event.id!)"> Supprimer </UiCommandItem>
|
||||||
|
</UiCommandGroup>
|
||||||
|
</UiCommandList>
|
||||||
|
</UiCommand>
|
||||||
|
</UiPopoverContent>
|
||||||
|
</UiPopover>
|
||||||
|
</menu>
|
||||||
|
|
||||||
|
<nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2">
|
||||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
|
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
|
||||||
<button
|
<button
|
||||||
class="flex gap-1"
|
class="flex gap-1"
|
||||||
@@ -153,27 +174,6 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
|
|||||||
</button>
|
</button>
|
||||||
</UiBadge>
|
</UiBadge>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<nav class="mt-2 flex gap-2">
|
|
||||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
|
|
||||||
<button
|
|
||||||
class="flex gap-1"
|
|
||||||
title="Évènement précédent"
|
|
||||||
@click="handleGotoRelativeEvent('prev')"
|
|
||||||
>
|
|
||||||
<PhArrowBendDoubleUpLeft size="16" weight="fill" /> Précédent
|
|
||||||
</button>
|
|
||||||
</UiBadge>
|
|
||||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
|
|
||||||
<button
|
|
||||||
class="flex gap-1"
|
|
||||||
title="Évènement suivant"
|
|
||||||
@click="handleGotoRelativeEvent('next')"
|
|
||||||
>
|
|
||||||
<PhArrowBendDoubleUpRight size="16" weight="fill" /> Suivant
|
|
||||||
</button>
|
|
||||||
</UiBadge>
|
|
||||||
</nav>
|
|
||||||
</UiPopoverContent>
|
</UiPopoverContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
126
components/calendar/form/CreateEvent.vue
Normal file
126
components/calendar/form/CreateEvent.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { RPGDate } from '~/models/Date';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PhAlarm
|
||||||
|
} from '@phosphor-icons/vue'
|
||||||
|
|
||||||
|
const { eventSkeleton } = storeToRefs(useCalendarEvents())
|
||||||
|
const { resetSkeleton, submitSkeleton } = useCalendarEvents()
|
||||||
|
const popoverOpen = ref(false)
|
||||||
|
|
||||||
|
const formErrors = reactive<{ message: string | null }>({
|
||||||
|
message: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
date?: RPGDate
|
||||||
|
btnClass?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens event creation's popover
|
||||||
|
*/
|
||||||
|
function openEventCreatePopover() {
|
||||||
|
resetSkeleton()
|
||||||
|
|
||||||
|
popoverOpen.value = true
|
||||||
|
|
||||||
|
// Set skeleton initial startDate if it's known
|
||||||
|
if (props.date) {
|
||||||
|
eventSkeleton.value.startDate = { ...props.date } // We need to clone it otherwise the props ends up mutating (?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch the popover state
|
||||||
|
// And reset the skeleton if it has been dismissed
|
||||||
|
watch(popoverOpen, (hasOpened, _o) => {
|
||||||
|
if (!hasOpened) {
|
||||||
|
resetSkeleton()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
await submitSkeleton()
|
||||||
|
|
||||||
|
popoverOpen.value = false
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
formErrors.message = err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiPopover v-model:open="popoverOpen">
|
||||||
|
<UiPopoverTrigger as-child>
|
||||||
|
<button :class="btnClass" @click="openEventCreatePopover()" />
|
||||||
|
</UiPopoverTrigger>
|
||||||
|
<UiPopoverContent
|
||||||
|
:align="'center'"
|
||||||
|
:side="'right'"
|
||||||
|
:collision-padding="60"
|
||||||
|
class="pl-3 min-w-96 bg-slate-900 border-slate-800"
|
||||||
|
>
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<div class="grid grid-cols-2 gap-y-4">
|
||||||
|
<div class="col-span-2 pl-8">
|
||||||
|
<input
|
||||||
|
id="new-event-title"
|
||||||
|
v-model="eventSkeleton.title"
|
||||||
|
type="text"
|
||||||
|
name="new-event-title"
|
||||||
|
required
|
||||||
|
placeholder="Titre de l'évènement"
|
||||||
|
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2 pl-8">
|
||||||
|
<textarea
|
||||||
|
id="new-event-description"
|
||||||
|
v-model="eventSkeleton.description"
|
||||||
|
name="new-event-description"
|
||||||
|
placeholder="Description brève de l'évènement"
|
||||||
|
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<PhAlarm size="18" weight="fill" />
|
||||||
|
|
||||||
|
<CalendarInputRPGDate
|
||||||
|
v-model:model-value="eventSkeleton.startDate"
|
||||||
|
placeholder="Date de début"
|
||||||
|
:initial-date="props.date"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>—</span>
|
||||||
|
|
||||||
|
<CalendarInputRPGDate
|
||||||
|
v-model:model-value="eventSkeleton.endDate"
|
||||||
|
placeholder="Date de fin"
|
||||||
|
:initial-date="props.date"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-red-500 pl-8">
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ formErrors.message }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<UiButton size="sm">
|
||||||
|
Sauvegarder
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</UiPopoverContent>
|
||||||
|
</UiPopover>
|
||||||
|
</template>
|
||||||
112
components/calendar/form/UpdateEvent.vue
Normal file
112
components/calendar/form/UpdateEvent.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhAlarm } from '@phosphor-icons/vue'
|
||||||
|
import { VisuallyHidden } from 'radix-vue'
|
||||||
|
|
||||||
|
const isModalOpened = defineModel<boolean>({ default: false })
|
||||||
|
const { resetSkeleton, updateEventFromSkeleton } = useCalendarEvents()
|
||||||
|
const { eventSkeleton, lastActiveEvent } = storeToRefs(useCalendarEvents())
|
||||||
|
|
||||||
|
const formErrors = reactive<{ message: string | null }>({
|
||||||
|
message: null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch the popover state
|
||||||
|
watch(isModalOpened, (hasOpened, _o) => {
|
||||||
|
if (hasOpened && lastActiveEvent.value) {
|
||||||
|
eventSkeleton.value = { ...lastActiveEvent.value }
|
||||||
|
} else {
|
||||||
|
resetSkeleton()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
await updateEventFromSkeleton()
|
||||||
|
|
||||||
|
isModalOpened.value = false
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
formErrors.message = err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiDialog v-model:open="isModalOpened" :modal="true">
|
||||||
|
<UiDialogContent
|
||||||
|
:align="'center'"
|
||||||
|
:side="'right'"
|
||||||
|
:collision-padding="60"
|
||||||
|
:disable-outside-pointer-events="true"
|
||||||
|
:trap-focus="true"
|
||||||
|
class="pl-3 min-w-96 bg-slate-900 border-slate-800"
|
||||||
|
>
|
||||||
|
<VisuallyHidden>
|
||||||
|
<UiDialogTitle> Modifier l'évènement</UiDialogTitle>
|
||||||
|
|
||||||
|
<UiDialogDescription>
|
||||||
|
Mettre à jour les données de l'évènement
|
||||||
|
</UiDialogDescription>
|
||||||
|
</VisuallyHidden>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<div class="grid grid-cols-2 gap-y-4">
|
||||||
|
<div class="col-span-2 ml-8">
|
||||||
|
<input
|
||||||
|
id="new-event-title"
|
||||||
|
v-model="eventSkeleton.title"
|
||||||
|
type="text"
|
||||||
|
name="new-event-title"
|
||||||
|
required
|
||||||
|
placeholder="Titre de l'évènement"
|
||||||
|
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2 ml-8">
|
||||||
|
<textarea
|
||||||
|
id="new-event-description"
|
||||||
|
v-model="eventSkeleton.description"
|
||||||
|
name="new-event-description"
|
||||||
|
placeholder="Description brève de l'évènement"
|
||||||
|
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<PhAlarm size="18" weight="fill" />
|
||||||
|
|
||||||
|
<CalendarInputRPGDate
|
||||||
|
v-model:model-value="eventSkeleton.startDate"
|
||||||
|
placeholder="Date de début"
|
||||||
|
:initial-date="lastActiveEvent?.startDate"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>—</span>
|
||||||
|
|
||||||
|
<CalendarInputRPGDate
|
||||||
|
v-model:model-value="eventSkeleton.endDate"
|
||||||
|
placeholder="Date de fin"
|
||||||
|
:initial-date="lastActiveEvent?.endDate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-red-500 ml-8">
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ formErrors.message }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<UiButton size="sm" type="submit">
|
||||||
|
Sauvegarder
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</UiDialogContent>
|
||||||
|
</UiDialog>
|
||||||
|
</template>
|
||||||
166
components/calendar/input/RPGDate.vue
Normal file
166
components/calendar/input/RPGDate.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { RPGDate } from '~/models/Date';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PhXCircle
|
||||||
|
} from '@phosphor-icons/vue'
|
||||||
|
|
||||||
|
const model = defineModel<RPGDate | null>()
|
||||||
|
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
initialDate?: RPGDate
|
||||||
|
placeholder?: string
|
||||||
|
required?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { getFormattedDateTitle, getMonthName, defaultDate } = useCalendar()
|
||||||
|
const { sortedMonths } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
|
const popoverOpened = ref<boolean>(false)
|
||||||
|
|
||||||
|
const inputPlaceholder = computed<string>(() => {
|
||||||
|
if (model.value) {
|
||||||
|
return getFormattedDateTitle(model.value, true)
|
||||||
|
} else if (props.placeholder) {
|
||||||
|
return props.placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialDateValue = props.initialDate ? { ...props.initialDate } : null
|
||||||
|
|
||||||
|
const monthTitle = computed<string>(() => {
|
||||||
|
if (model.value) {
|
||||||
|
return getMonthName(model.value.month)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasValue = computed<boolean>(() => !!model.value)
|
||||||
|
const monthData = computed(() => {
|
||||||
|
if (model.value) {
|
||||||
|
return sortedMonths.value[model.value.month]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
function setModelDay(day: number) {
|
||||||
|
if (model.value) {
|
||||||
|
model.value.day = day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMonthChange(e: string) {
|
||||||
|
const monthId = Number(e)
|
||||||
|
|
||||||
|
if (model.value) {
|
||||||
|
model.value.month = monthId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click if the date is null
|
||||||
|
*/
|
||||||
|
function handleNullClick() {
|
||||||
|
// Early return
|
||||||
|
if (model.value) return
|
||||||
|
|
||||||
|
if (!model.value) {
|
||||||
|
if (props.initialDate) {
|
||||||
|
model.value = initialDateValue
|
||||||
|
} else {
|
||||||
|
model.value = { ...defaultDate }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleValueReset() {
|
||||||
|
// Early return
|
||||||
|
if (!model.value) return
|
||||||
|
|
||||||
|
model.value = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiPopover v-model:open="popoverOpened">
|
||||||
|
<UiPopoverTrigger>
|
||||||
|
<div class="flex">
|
||||||
|
<UiButton
|
||||||
|
size='sm'
|
||||||
|
variant="outline"
|
||||||
|
:class="{
|
||||||
|
'text-slate-500': !hasValue,
|
||||||
|
'rounded-tr-none rounded-br-none': model
|
||||||
|
}"
|
||||||
|
@click.prevent="handleNullClick"
|
||||||
|
>
|
||||||
|
{{ inputPlaceholder }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-if="model && !required"
|
||||||
|
size='sm'
|
||||||
|
variant="outline"
|
||||||
|
class="rounded-tl-none rounded-bl-none px-2"
|
||||||
|
@click.prevent="handleValueReset"
|
||||||
|
>
|
||||||
|
<PhXCircle size="14" />
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</UiPopoverTrigger>
|
||||||
|
<UiPopoverContent
|
||||||
|
v-if="model"
|
||||||
|
align="start"
|
||||||
|
side="bottom"
|
||||||
|
class="bg-slate-900 border-slate-800"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 items-center gap-x-2 gap-y-3">
|
||||||
|
<UiSelect @update:model-value="handleMonthChange">
|
||||||
|
<UiSelectTrigger>
|
||||||
|
<UiSelectValue :placeholder="monthTitle" />
|
||||||
|
</UiSelectTrigger>
|
||||||
|
<UiSelectContent>
|
||||||
|
<UiSelectGroup class="max-h-[50vh]">
|
||||||
|
<UiSelectItem v-for="(month, i) in sortedMonths" :key="`popover-select-item-${id}-${i}`" :value="i.toString()">
|
||||||
|
{{ month.name }}
|
||||||
|
</UiSelectItem>
|
||||||
|
</UiSelectGroup>
|
||||||
|
</UiSelectContent>
|
||||||
|
</UiSelect>
|
||||||
|
|
||||||
|
<UiInput v-model="model.year" type="number" />
|
||||||
|
|
||||||
|
<!-- <nav class="flex items-center justify-end gap-2">
|
||||||
|
<UiButton variant="outline" size="icon" class="w-8 h-8" @click="decrementModelMonth()">
|
||||||
|
<PhCaretLeft size="14" />
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="outline" size="icon" class="w-8 h-8" @click="incrementModelMonth()">
|
||||||
|
<PhCaretRight size="14" />
|
||||||
|
</UiButton>
|
||||||
|
</nav> -->
|
||||||
|
|
||||||
|
<hr class="col-span-2" >
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<div v-if="monthData" class="grid grid-cols-7 gap-1">
|
||||||
|
<button
|
||||||
|
v-for="day in monthData.days"
|
||||||
|
:key="`popover-day-grid-${id}-${day}`"
|
||||||
|
class="aspect-square rounded-full text-[.8em] transition-colors hover:bg-slate-700"
|
||||||
|
:class="{'bg-blue-500 hover:bg-blue-700': day === model.day}"
|
||||||
|
@click="setModelDay(day)"
|
||||||
|
>
|
||||||
|
{{ day }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</UiPopoverContent>
|
||||||
|
</UiPopover>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { RPGDate } from '@/models/Date'
|
import type { RPGDate } from '@/models/Date'
|
||||||
|
import type { CalendarEvent } from '@/models/CalendarEvent'
|
||||||
import { useCalendar } from '@/stores/CalendarStore'
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
import { useCalendarEvents } from '@/stores/EventStore'
|
import { useCalendarEvents } from '@/stores/EventStore'
|
||||||
import { useElementBounding } from '@vueuse/core'
|
import { useElementBounding } from '@vueuse/core'
|
||||||
@@ -7,7 +8,6 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { computed, ref, type ComputedRef } from 'vue'
|
import { computed, ref, type ComputedRef } from 'vue'
|
||||||
|
|
||||||
import CalendarEventButton from '../../CalendarEvent.vue'
|
import CalendarEventButton from '../../CalendarEvent.vue'
|
||||||
import type { CalendarEvent } from '~/models/CalendarEvent'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
date: RPGDate
|
date: RPGDate
|
||||||
@@ -21,6 +21,9 @@ const { defaultDate, selectDate, areDatesIdentical } = useCalendar()
|
|||||||
const { selectedDate } = storeToRefs(useCalendar())
|
const { selectedDate } = storeToRefs(useCalendar())
|
||||||
const { currentEvents } = storeToRefs(useCalendarEvents())
|
const { currentEvents } = storeToRefs(useCalendarEvents())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All events with a startDate / endDate that starts or ends on the tile
|
||||||
|
*/
|
||||||
const eventsForTheDay = computed(() => {
|
const eventsForTheDay = computed(() => {
|
||||||
return currentEvents.value.filter((currentEvent) => {
|
return currentEvents.value.filter((currentEvent) => {
|
||||||
return (
|
return (
|
||||||
@@ -30,10 +33,16 @@ const eventsForTheDay = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the tile on today's date ?
|
||||||
|
*/
|
||||||
const isDefaultDate = computed(() => {
|
const isDefaultDate = computed(() => {
|
||||||
return areDatesIdentical(props.date, defaultDate)
|
return areDatesIdentical(props.date, defaultDate)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the tile on the hightlighted date ?
|
||||||
|
*/
|
||||||
const isSelectedDate = computed(() => {
|
const isSelectedDate = computed(() => {
|
||||||
return areDatesIdentical(props.date, selectedDate.value)
|
return areDatesIdentical(props.date, selectedDate.value)
|
||||||
})
|
})
|
||||||
@@ -49,12 +58,19 @@ const numberOfEventsToFit: ComputedRef<number> = computed(() => {
|
|||||||
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40)
|
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40)
|
||||||
})
|
})
|
||||||
|
|
||||||
const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed(() => {
|
/**
|
||||||
|
* Events that can fit in the tile's space
|
||||||
|
*/
|
||||||
|
const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed<CalendarEvent[]>(() => {
|
||||||
return [...eventsForTheDay.value].splice(0, numberOfEventsToFit.value)
|
return [...eventsForTheDay.value].splice(0, numberOfEventsToFit.value)
|
||||||
})
|
})
|
||||||
const eventsNotDisplayed = computed(
|
|
||||||
() => eventsForTheDay.value.length - eventsToDisplay.value.length
|
/**
|
||||||
)
|
* Remaining events that are not displayed
|
||||||
|
*
|
||||||
|
* Used if not all events can be seen in the tile's space
|
||||||
|
*/
|
||||||
|
const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsForTheDay.value.length - eventsToDisplay.value.length)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -66,9 +82,6 @@ const eventsNotDisplayed = computed(
|
|||||||
'text-slate-300': !props.faded
|
'text-slate-300': !props.faded
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- Used for "display all events" -->
|
|
||||||
<button class="absolute inset-0 w-full h-full cursor-default z-0" />
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="relative z-10 group block w-full text-center cursor-pointer"
|
class="relative z-10 group block w-full text-center cursor-pointer"
|
||||||
@click="selectDate(date)"
|
@click="selectDate(date)"
|
||||||
@@ -86,6 +99,7 @@ const eventsNotDisplayed = computed(
|
|||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
<ul
|
<ul
|
||||||
ref="calendarEventsList"
|
ref="calendarEventsList"
|
||||||
class="absolute top-12 bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
|
class="absolute top-12 bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
|
||||||
@@ -93,9 +107,11 @@ const eventsNotDisplayed = computed(
|
|||||||
'opacity-40': props.faded && !isSelectedDate
|
'opacity-40': props.faded && !isSelectedDate
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<TransitionGroup name="event">
|
||||||
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
|
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
|
||||||
<CalendarEventButton :event :tile-date="date" />
|
<CalendarEventButton :event :tile-date="date" />
|
||||||
</li>
|
</li>
|
||||||
|
</TransitionGroup>
|
||||||
|
|
||||||
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
|
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
|
||||||
<UiPopover>
|
<UiPopover>
|
||||||
@@ -127,6 +143,11 @@ const eventsNotDisplayed = computed(
|
|||||||
</UiPopover>
|
</UiPopover>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<CalendarFormCreateEvent :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||||
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -140,4 +161,21 @@ const eventsNotDisplayed = computed(
|
|||||||
border-top-width: 1px;
|
border-top-width: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-enter-active {
|
||||||
|
transition: all 120ms ease-in-out;
|
||||||
|
}
|
||||||
|
.event-leave-active {
|
||||||
|
transition: all 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
.event-enter-from,
|
||||||
|
.event-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.event-enter-from {
|
||||||
|
transform: translateX(.15rem);
|
||||||
|
}
|
||||||
|
.event-leave-to {
|
||||||
|
transform: translateX(-.5rem);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const moveCalendarRight = useThrottleFn(() => {
|
|||||||
<div class="grid grid-cols-10" @wheel="handleWheel">
|
<div class="grid grid-cols-10" @wheel="handleWheel">
|
||||||
<CalendarStateMonthlyDayTile
|
<CalendarStateMonthlyDayTile
|
||||||
v-for="day in currentMonthData.days"
|
v-for="day in currentMonthData.days"
|
||||||
:key="day"
|
:key="`layout-month-grid-${day}`"
|
||||||
:date="{
|
:date="{
|
||||||
day: day,
|
day: day,
|
||||||
month: currentDate.currentMonth,
|
month: currentDate.currentMonth,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
import type { Category } from './Category'
|
import type { Category } from './Category'
|
||||||
import type { RPGDate } from './Date'
|
import { dateSchema, type RPGDate } from './Date'
|
||||||
|
|
||||||
export interface CalendarEvent {
|
export interface CalendarEvent {
|
||||||
id: number
|
id?: number
|
||||||
title: string
|
title: string
|
||||||
startDate: RPGDate
|
startDate: RPGDate
|
||||||
endDate?: RPGDate
|
endDate?: RPGDate
|
||||||
@@ -13,6 +14,19 @@ export interface CalendarEvent {
|
|||||||
wiki?: string
|
wiki?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body validation schema for post requests (insert and update)
|
||||||
|
*/
|
||||||
|
export const postEventBodySchema = z.object({
|
||||||
|
event: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional().nullable(),
|
||||||
|
startDate: dateSchema.required(),
|
||||||
|
endDate: dateSchema.optional().nullable()
|
||||||
|
}),
|
||||||
|
calendarId: z.number({ coerce: true }).int().positive()
|
||||||
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function isCalendarEvent(object: any): object is CalendarEvent {
|
export function isCalendarEvent(object: any): object is CalendarEvent {
|
||||||
return 'startDate' in object
|
return 'startDate' in object
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
export interface RPGDate {
|
export interface RPGDate {
|
||||||
day: number
|
day: number
|
||||||
month: number
|
month: number
|
||||||
@@ -6,23 +8,8 @@ export interface RPGDate {
|
|||||||
|
|
||||||
export type RPGDateOrder = 'asc' | 'desc'
|
export type RPGDateOrder = 'asc' | 'desc'
|
||||||
|
|
||||||
// export function getRelativeDate(baseDate: RPGDate, relativeDate: RPGDate) {
|
export const dateSchema = z.object({
|
||||||
// let newDay: number
|
day: z.number().int().positive(),
|
||||||
// let newMonth: number
|
month: z.number().int(),
|
||||||
// let newYear: number
|
year: z.number().int()
|
||||||
// let newPeriod: RPGPeriod
|
})
|
||||||
|
|
||||||
// const differenceInDays = getDifferenceInDays(baseDate, relativeDate)
|
|
||||||
|
|
||||||
// // console.log(baseDate, substractionDate)
|
|
||||||
// console.log(getRelativeString(baseDate, relativeDate))
|
|
||||||
|
|
||||||
// return differenceInDays
|
|
||||||
|
|
||||||
// // return {
|
|
||||||
// // day: newDay,
|
|
||||||
// // month: newMonth,
|
|
||||||
// // year: newYear,
|
|
||||||
// // period: newPeriod
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ watch(user, (n, _o) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { revealAdvancedSearch } = useCalendar()
|
const { revealAdvancedSearch } = useCalendar()
|
||||||
const { isAdvancedSearchOpen } = storeToRefs(useCalendar())
|
|
||||||
|
|
||||||
const sidebarMenu: MenuItem[] = [
|
const sidebarMenu: MenuItem[] = [
|
||||||
{
|
{
|
||||||
@@ -34,7 +33,5 @@ const sidebarMenu: MenuItem[] = [
|
|||||||
<Sidebar :menu-items="sidebarMenu" />
|
<Sidebar :menu-items="sidebarMenu" />
|
||||||
|
|
||||||
<Calendar />
|
<Calendar />
|
||||||
|
|
||||||
<CalendarSearch v-model:model-value="isAdvancedSearchOpen" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
41
server/api/calendars/events/[id].delete.ts
Normal file
41
server/api/calendars/events/[id].delete.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { serverSupabaseClient } from "#supabase/server"
|
||||||
|
import type { CalendarEvent } from "~/models/CalendarEvent"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (paramsError) {
|
||||||
|
throw createError({
|
||||||
|
cause: 'Utilisateur',
|
||||||
|
fatal: false,
|
||||||
|
message: "L'identifiant de l'évènement est manquant ou mal renseigné.",
|
||||||
|
status: 401,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('calendar_events')
|
||||||
|
.delete()
|
||||||
|
.eq('id', params.id)
|
||||||
|
.single<CalendarEvent>()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
throw createError({
|
||||||
|
cause: 'Serveur',
|
||||||
|
status: 500,
|
||||||
|
fatal: false,
|
||||||
|
message: 'Une erreur inconnue est survenue.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
63
server/api/calendars/events/[id].patch.ts
Normal file
63
server/api/calendars/events/[id].patch.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { serverSupabaseClient } from "#supabase/server"
|
||||||
|
import { postEventBodySchema, type CalendarEvent } from "~/models/CalendarEvent"
|
||||||
|
|
||||||
|
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 => postEventBodySchema.safeParse(body))
|
||||||
|
|
||||||
|
if (paramsError) {
|
||||||
|
throw createError({
|
||||||
|
cause: 'Utilisateur',
|
||||||
|
fatal: false,
|
||||||
|
message: "L'identifiant de l'évènement est manquant ou mal renseigné.",
|
||||||
|
status: 401,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyError) {
|
||||||
|
console.log(bodyData)
|
||||||
|
throw createError({
|
||||||
|
cause: 'Utilisateur',
|
||||||
|
fatal: false,
|
||||||
|
message: "Le schéma de la requête n'est pas complet ou mal renseigné.",
|
||||||
|
status: 401,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('calendar_events')
|
||||||
|
.update({ start_date: bodyData?.event.startDate, end_date: bodyData.event.endDate, title: bodyData?.event.title, description: bodyData.event.description, calendar_id: bodyData?.calendarId } as never)
|
||||||
|
.eq('id', params.id)
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
hidden,
|
||||||
|
startDate:start_date,
|
||||||
|
endDate:end_date,
|
||||||
|
wiki,
|
||||||
|
category:calendar_event_categories!calendar_events_category_fkey (*),
|
||||||
|
secondaryCategories:calendar_event_categories!calendar_event_categories_links (*)
|
||||||
|
`)
|
||||||
|
.single<CalendarEvent>()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
throw createError({
|
||||||
|
cause: 'Serveur',
|
||||||
|
status: 500,
|
||||||
|
fatal: false,
|
||||||
|
message: 'Une erreur inconnue est survenue.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
46
server/api/calendars/events/create.post.ts
Normal file
46
server/api/calendars/events/create.post.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { serverSupabaseClient } from "#supabase/server";
|
||||||
|
import type { CalendarEvent } from "@/models/CalendarEvent";
|
||||||
|
import { postEventBodySchema } from "@/models/CalendarEvent";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const client = await serverSupabaseClient(event)
|
||||||
|
const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => postEventBodySchema.safeParse(body))
|
||||||
|
|
||||||
|
if (schemaError) {
|
||||||
|
throw createError({
|
||||||
|
cause: 'Utilisateur',
|
||||||
|
fatal: false,
|
||||||
|
message: "Le schéma de la requête n'est pas complet ou mal renseigné.",
|
||||||
|
status: 401,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('calendar_events')
|
||||||
|
.insert({ start_date: bodyData?.event.startDate, end_date: bodyData.event.endDate, title: bodyData?.event.title, description: bodyData.event.description, calendar_id: bodyData?.calendarId } as never)
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
hidden,
|
||||||
|
startDate:start_date,
|
||||||
|
endDate:end_date,
|
||||||
|
wiki,
|
||||||
|
category:calendar_event_categories!calendar_events_category_fkey (*),
|
||||||
|
secondaryCategories:calendar_event_categories!calendar_event_categories_links (*)
|
||||||
|
`)
|
||||||
|
.single<CalendarEvent>()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
throw createError({
|
||||||
|
cause: 'Serveur',
|
||||||
|
status: 500,
|
||||||
|
fatal: false,
|
||||||
|
message: 'Une erreur inconnue est survenue.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -36,6 +36,11 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
'century'
|
'century'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const calendarId = ref<number>(0)
|
||||||
|
|
||||||
|
function setCalendarId(data: number) {
|
||||||
|
calendarId.value = data
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Month list (queried from API)
|
* Month list (queried from API)
|
||||||
*/
|
*/
|
||||||
@@ -178,7 +183,6 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
/**
|
/**
|
||||||
* Moves the current date forward one month
|
* Moves the current date forward one month
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function incrementMonth(): void {
|
function incrementMonth(): void {
|
||||||
let newValue = Number(params.month) + 1
|
let newValue = Number(params.month) + 1
|
||||||
|
|
||||||
@@ -290,13 +294,19 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
*/
|
*/
|
||||||
const isAdvancedSearchOpen: Ref<boolean> = ref<boolean>(false)
|
const isAdvancedSearchOpen: Ref<boolean> = ref<boolean>(false)
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the search modal
|
|
||||||
*/
|
|
||||||
function revealAdvancedSearch() {
|
function revealAdvancedSearch() {
|
||||||
isAdvancedSearchOpen.value = true
|
isAdvancedSearchOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State for advanced search modal
|
||||||
|
*/
|
||||||
|
const isEditEventModalOpen: Ref<boolean> = ref<boolean>(false)
|
||||||
|
|
||||||
|
function revealEditEventModal() {
|
||||||
|
isEditEventModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches the active viewType
|
* Switches the active viewType
|
||||||
*
|
*
|
||||||
@@ -435,6 +445,14 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
// Reverses the order if specified
|
// Reverses the order if specified
|
||||||
const orderFactor: number = order === 'desc' ? 1 : -1
|
const orderFactor: number = order === 'desc' ? 1 : -1
|
||||||
|
|
||||||
|
// If somehow one of the date isn't available
|
||||||
|
if (!b) {
|
||||||
|
return -1 * orderFactor
|
||||||
|
}
|
||||||
|
if (!a) {
|
||||||
|
return 1 * orderFactor
|
||||||
|
}
|
||||||
|
|
||||||
// Compare years
|
// Compare years
|
||||||
if (a.year < b.year) return -1 * orderFactor
|
if (a.year < b.year) return -1 * orderFactor
|
||||||
if (a.year > b.year) return 1 * orderFactor
|
if (a.year > b.year) return 1 * orderFactor
|
||||||
@@ -598,6 +616,8 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
calendarId,
|
||||||
|
setCalendarId,
|
||||||
months,
|
months,
|
||||||
setMonths,
|
setMonths,
|
||||||
sortedMonths,
|
sortedMonths,
|
||||||
@@ -628,11 +648,13 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
getViewTypeTitle,
|
getViewTypeTitle,
|
||||||
isCurrentScreenActive,
|
isCurrentScreenActive,
|
||||||
isAdvancedSearchOpen,
|
isAdvancedSearchOpen,
|
||||||
|
revealAdvancedSearch,
|
||||||
|
isEditEventModalOpen,
|
||||||
|
revealEditEventModal,
|
||||||
convertDateToDays,
|
convertDateToDays,
|
||||||
getDifferenceInDays,
|
getDifferenceInDays,
|
||||||
areDatesIdentical,
|
areDatesIdentical,
|
||||||
compareDates,
|
compareDates,
|
||||||
getRelativeString,
|
getRelativeString,
|
||||||
revealAdvancedSearch
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { ref, watch, type Ref } from 'vue'
|
|||||||
import { useCalendar } from './CalendarStore'
|
import { useCalendar } from './CalendarStore'
|
||||||
|
|
||||||
export const useCalendarEvents = defineStore('calendar-events', () => {
|
export const useCalendarEvents = defineStore('calendar-events', () => {
|
||||||
const { currentDate, currentConfig, convertDateToDays, compareDates } = useCalendar()
|
const { currentDate, defaultDate, currentConfig, convertDateToDays, compareDates } = useCalendar()
|
||||||
|
const { calendarId } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
const baseEvents = ref<CalendarEvent[]>([])
|
const baseEvents = ref<CalendarEvent[]>([])
|
||||||
|
|
||||||
@@ -13,17 +14,19 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
baseEvents.value = data
|
baseEvents.value = data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Order base events by dates
|
||||||
const allEvents = computed(() => baseEvents.value.sort((a, b) => {
|
const allEvents = computed(() => baseEvents.value.sort((a, b) => {
|
||||||
return compareDates(a.startDate, b.startDate, 'desc')
|
return compareDates(a.startDate, b.startDate, 'desc')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Gets all current event in its default state
|
// Gets all current event in its default state
|
||||||
const currentEvents: Ref<CalendarEvent[]> = ref(computeCurrentEvents())
|
const currentEvents: Ref<CalendarEvent[]> = ref([])
|
||||||
|
|
||||||
// Watch for currentDate changes
|
// Watch for currentDate or events' list changes
|
||||||
|
// This is deep because we're watching an array, and changes need to trigger and mutations like .push and .splice
|
||||||
watch([currentDate, allEvents], () => {
|
watch([currentDate, allEvents], () => {
|
||||||
currentEvents.value = computeCurrentEvents()
|
currentEvents.value = computeCurrentEvents()
|
||||||
})
|
}, { deep: true, immediate: true })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the event can appear in the front end
|
* Determines if the event can appear in the front end
|
||||||
@@ -34,9 +37,6 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
* @returns Whether the event should appear in the current view
|
* @returns Whether the event should appear in the current view
|
||||||
*/
|
*/
|
||||||
function shouldEventBeDisplayed(event: CalendarEvent): boolean {
|
function shouldEventBeDisplayed(event: CalendarEvent): boolean {
|
||||||
// const eventStartDateToDays = convertDateToDays(event.startDate)
|
|
||||||
// const eventEndDateToDays: number = event.endDate ? convertDateToDays(event.endDate) : 0
|
|
||||||
|
|
||||||
const isEventOnCurrentScreen =
|
const isEventOnCurrentScreen =
|
||||||
(event.startDate.year === currentDate.currentYear &&
|
(event.startDate.year === currentDate.currentYear &&
|
||||||
event.startDate.month === currentDate.currentMonth) ||
|
event.startDate.month === currentDate.currentMonth) ||
|
||||||
@@ -44,25 +44,6 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
event.endDate.year === currentDate.currentYear &&
|
event.endDate.year === currentDate.currentYear &&
|
||||||
event.endDate.month === currentDate.currentMonth)
|
event.endDate.month === currentDate.currentMonth)
|
||||||
|
|
||||||
// Check whether the event is on the last 8 tiles
|
|
||||||
// This is to allow leap events from appearing on the last 8 tiles
|
|
||||||
//
|
|
||||||
// This is not used for now
|
|
||||||
//
|
|
||||||
// const firstDayOfCurrentMonth = convertDateToDays({
|
|
||||||
// day: 1,
|
|
||||||
// month: currentDate.currentMonth,
|
|
||||||
// year: currentDate.currentYear
|
|
||||||
// })
|
|
||||||
// const lastDayOfCurrentMonth = firstDayOfCurrentMonth + daysPerMonth
|
|
||||||
// const last8Tiles = lastDayOfCurrentMonth + 8
|
|
||||||
|
|
||||||
// const isEventOnNext8Tiles =
|
|
||||||
// (eventStartDateToDays <= last8Tiles && eventStartDateToDays >= lastDayOfCurrentMonth) ||
|
|
||||||
// (Boolean(event.endDate) &&
|
|
||||||
// eventEndDateToDays <= last8Tiles &&
|
|
||||||
// eventEndDateToDays >= lastDayOfCurrentMonth)
|
|
||||||
|
|
||||||
switch (currentConfig.viewType) {
|
switch (currentConfig.viewType) {
|
||||||
case 'month':
|
case 'month':
|
||||||
return isEventOnCurrentScreen!
|
return isEventOnCurrentScreen!
|
||||||
@@ -97,8 +78,7 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From a base event, gets the next or previous in the timeline
|
* From a base event, gets the next or previous one in the timeline
|
||||||
* @todo **This should probably be extracted to function FIRST with dates only, as the initialIsEnd param is only used at the beggining to establish a pivot**
|
|
||||||
*
|
*
|
||||||
* @param event The event at a given position in the data
|
* @param event The event at a given position in the data
|
||||||
* @param position Whether we should get the next or previous event
|
* @param position Whether we should get the next or previous event
|
||||||
@@ -120,6 +100,13 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
return getRelativeEventFromDate(dateToParse, position)
|
return getRelativeEventFromDate(dateToParse, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From a date, gets the next or previous event in the timeline
|
||||||
|
*
|
||||||
|
* @param date The starting date from which to get the next event
|
||||||
|
* @param position Whether we should get the next or previous event
|
||||||
|
* @returns The next event in chronological order
|
||||||
|
*/
|
||||||
function getRelativeEventFromDate(
|
function getRelativeEventFromDate(
|
||||||
date: RPGDate,
|
date: RPGDate,
|
||||||
position: 'next' | 'prev' = 'next'
|
position: 'next' | 'prev' = 'next'
|
||||||
@@ -156,27 +143,6 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Special case : If we query the first one but it already is
|
|
||||||
// if (position === 'prev' && t[0].distance === 0) {
|
|
||||||
// const targetDate =
|
|
||||||
// t[0].targetKey === 'startDate' ? t[0].eventData.startDate : t[0].eventData.endDate!
|
|
||||||
// return {
|
|
||||||
// event: t[0].eventData,
|
|
||||||
// targetDate: targetDate
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // Special case : If we query the last one but it already is
|
|
||||||
// if (position === 'next' && t[t.length - 1].distance === 0) {
|
|
||||||
// const targetDate =
|
|
||||||
// t[t.length - 1].targetKey === 'startDate'
|
|
||||||
// ? t[t.length - 1].eventData.startDate
|
|
||||||
// : t[t.length - 1].eventData.endDate!
|
|
||||||
// return {
|
|
||||||
// event: t[t.length - 1].eventData,
|
|
||||||
// targetDate: targetDate
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Based on the direction, either ignore negative distance (past) or positive distance (future)
|
// Based on the direction, either ignore negative distance (past) or positive distance (future)
|
||||||
t = t.filter((i) => {
|
t = t.filter((i) => {
|
||||||
return position === 'next' ? i.distance > 0 : i.distance < 0
|
return position === 'next' ? i.distance > 0 : i.distance < 0
|
||||||
@@ -199,5 +165,59 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { allEvents, setEvents, currentEvents, getRelativeEventFromDate, getRelativeEventFromEvent }
|
/**
|
||||||
|
* EVENT CREATION FUNCTIONS
|
||||||
|
*/
|
||||||
|
const lastActiveEvent = ref<CalendarEvent | null>()
|
||||||
|
/**
|
||||||
|
* Dummy event to hold creation data
|
||||||
|
*/
|
||||||
|
const eventSkeleton: Ref<CalendarEvent> = ref<CalendarEvent>({ title: '', startDate: defaultDate })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the dummy event data
|
||||||
|
*/
|
||||||
|
function resetSkeleton() {
|
||||||
|
eventSkeleton.value = { title: '', startDate: defaultDate }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the skeleton event and creates a real event from its data
|
||||||
|
*
|
||||||
|
* We assume it's been sanitized by the caller
|
||||||
|
*/
|
||||||
|
async function submitSkeleton() {
|
||||||
|
try {
|
||||||
|
const res = await $fetch('/api/calendars/events/create', { method: 'POST', body: { event : eventSkeleton.value, calendarId: calendarId.value }})
|
||||||
|
baseEvents.value.push(res)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEventFromSkeleton() {
|
||||||
|
try {
|
||||||
|
const res = await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: 'PATCH', body: { event : eventSkeleton.value, calendarId: calendarId.value }})
|
||||||
|
const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
|
||||||
|
baseEvents.value[eventIndex] = res
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteEvent(eventId: number) {
|
||||||
|
if (!eventId) {
|
||||||
|
throw new Error('ID of the event is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch(`/api/calendars/events/${eventId}`, { method: 'DELETE' })
|
||||||
|
const eventIndex = baseEvents.value.findIndex(e => e.id === eventId)
|
||||||
|
baseEvents.value.splice(eventIndex, 1)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allEvents, setEvents, currentEvents, getRelativeEventFromDate, getRelativeEventFromEvent, eventSkeleton, resetSkeleton, submitSkeleton, lastActiveEvent, updateEventFromSkeleton, deleteEvent }
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user