Merge branch 'features/nuxt-supabase' into features/nuxt

This commit is contained in:
Alexis
2024-06-05 13:32:51 +02:00
17 changed files with 832 additions and 187 deletions

View File

@@ -7,10 +7,12 @@ import CenturyLayout from './state/centennially/Layout.vue'
import DecadeLayout from './state/decennially/Layout.vue'
import YearLayout from './state/yearly/Layout.vue'
const { isAdvancedSearchOpen, isEditEventModalOpen } = storeToRefs(useCalendar())
const route = useRoute()
const worldId = route.params.id
const { setMonths, setDefaultDate, currentConfig, selectedDate, jumpToDate } = useCalendar()
const { setCalendarId, setMonths, setDefaultDate, currentConfig, selectedDate, jumpToDate } = useCalendar()
const { setEvents } = useCalendarEvents()
const { setCharacters } = useCharacters()
@@ -20,6 +22,12 @@ const { data: characters, pending: charPending, refresh: charRefresh } = await u
if (!calendar.value) {
await calRefresh()
} 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) {
setMonths(calendar.value?.data?.months)
}
@@ -40,6 +48,9 @@ if (!characters.value) {
watch(calPending, (newStatus) => {
if (!newStatus) {
if (calendar.value?.data?.id) {
setCalendarId(calendar.value?.data?.id)
}
if (calendar.value?.data?.months) {
setMonths(calendar.value?.data?.months)
}
@@ -97,5 +108,8 @@ onMounted(() => {
</KeepAlive>
</div>
</template>
<CalendarSearch v-model:model-value="isAdvancedSearchOpen" />
<CalendarFormUpdateEvent v-model:model-value="isEditEventModalOpen" />
</div>
</template>

View File

@@ -17,15 +17,15 @@ const isStartEvent = spansMultipleDays && areDatesIdentical(props.tileDate, prop
const isEndEvent =
spansMultipleDays && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate)
const isPopoverOpen = ref<boolean>(false)
const isPopoverDetailsOpen = ref<boolean>(false)
function handleClosePopover() {
isPopoverOpen.value = false
isPopoverDetailsOpen.value = false
}
</script>
<template>
<UiPopover v-model:open="isPopoverOpen">
<UiPopover v-model:open="isPopoverDetailsOpen">
<UiPopoverTrigger as-child>
<button
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>
<CalendarEventDetails
v-once
:event
:spans-multiple-days
:is-start-event

View File

@@ -2,19 +2,18 @@
import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/CalendarEvent'
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import {
PhHourglassMedium,
PhAlarm,
PhHourglassHigh,
PhHourglassLow,
PhArrowBendDoubleUpLeft,
PhArrowBendDoubleUpRight
PhDotsThreeOutlineVertical
} from '@phosphor-icons/vue'
const { defaultDate, getFormattedDateTitle, jumpToDate } = useCalendar()
const { getRelativeEventFromEvent } = useCalendarEvents()
const { defaultDate, getFormattedDateTitle, jumpToDate, revealEditEventModal, getRelativeString } = useCalendar()
const { deleteEvent } = useCalendarEvents()
const { lastActiveEvent } = storeToRefs(useCalendarEvents())
const props = defineProps<{
event: CalendarEvent
@@ -23,11 +22,10 @@ const props = defineProps<{
isEndEvent?: boolean
}>()
const emit = defineEmits<{
(e: 'query:close-popover'): void
}>()
// Ref for the popover
const eventDetails = ref<HTMLElement>()
const { getRelativeString } = useCalendar()
const emit = defineEmits(['query:close-popover'])
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
const dateDuration: string | null = props.event.endDate
@@ -39,14 +37,15 @@ function handleJumpToDate(date: RPGDate) {
emit('query:close-popover')
}
function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
try {
const { targetDate } = getRelativeEventFromEvent(props.event, position, props.isEndEvent)
/**
* Edit event
*/
const commandMenuOpened = ref(false)
handleJumpToDate(targetDate)
} catch (err) {
console.log(err)
}
function deployEditModal() {
lastActiveEvent.value = { ...props.event }
revealEditEventModal()
commandMenuOpened.value = false
}
</script>
@@ -80,22 +79,24 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
'border-yellow-600': event.category?.name === 'commerce'
}"
>
<div class="grid gap-1">
<div class="text-lg font-semibold">
{{ event.title }}
</div>
<div ref="eventDetails" class="grid gap-1">
<header class="pr-12">
<div class="text-lg font-semibold">
{{ event.title }}
</div>
<div class="mb-1">
<template v-if="!event.endDate">
<p class="font-semibold">{{ getFormattedDateTitle(event.startDate, true) }}</p>
</template>
<template v-else>
<p class="font-semibold">
Du {{ getFormattedDateTitle(event.startDate, true) }} au
{{ getFormattedDateTitle(event.endDate, true) }}
</p>
</template>
</div>
<div class="mb-1">
<template v-if="!event.endDate">
<p class="font-semibold">{{ getFormattedDateTitle(event.startDate, true) }}</p>
</template>
<template v-else>
<p class="font-semibold">
Du {{ getFormattedDateTitle(event.startDate, true) }} au
{{ getFormattedDateTitle(event.endDate, true) }}
</p>
</template>
</div>
</header>
<div class="mb-1 space-y-1">
<p class="text-sm italic opacity-75 flex items-center gap-1">
@@ -133,7 +134,27 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
</template>
</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>
<button
class="flex gap-1"
@@ -153,27 +174,6 @@ function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
</button>
</UiBadge>
</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>
</template>

View 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>

View 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>

View 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>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/CalendarEvent'
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { useElementBounding } from '@vueuse/core'
@@ -7,7 +8,6 @@ import { storeToRefs } from 'pinia'
import { computed, ref, type ComputedRef } from 'vue'
import CalendarEventButton from '../../CalendarEvent.vue'
import type { CalendarEvent } from '~/models/CalendarEvent'
const props = defineProps<{
date: RPGDate
@@ -21,6 +21,9 @@ const { defaultDate, selectDate, areDatesIdentical } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const { currentEvents } = storeToRefs(useCalendarEvents())
/**
* All events with a startDate / endDate that starts or ends on the tile
*/
const eventsForTheDay = computed(() => {
return currentEvents.value.filter((currentEvent) => {
return (
@@ -30,10 +33,16 @@ const eventsForTheDay = computed(() => {
})
})
/**
* Is the tile on today's date ?
*/
const isDefaultDate = computed(() => {
return areDatesIdentical(props.date, defaultDate)
})
/**
* Is the tile on the hightlighted date ?
*/
const isSelectedDate = computed(() => {
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)
})
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)
})
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>
<template>
@@ -66,9 +82,6 @@ const eventsNotDisplayed = computed(
'text-slate-300': !props.faded
}"
>
<!-- Used for "display all events" -->
<button class="absolute inset-0 w-full h-full cursor-default z-0" />
<button
class="relative z-10 group block w-full text-center cursor-pointer"
@click="selectDate(date)"
@@ -86,47 +99,55 @@ const eventsNotDisplayed = computed(
</ClientOnly>
</button>
<ul
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="{
'opacity-40': props.faded && !isSelectedDate
}"
>
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
<CalendarEventButton :event :tile-date="date" />
</li>
<ClientOnly>
<ul
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="{
'opacity-40': props.faded && !isSelectedDate
}"
>
<TransitionGroup name="event">
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
<CalendarEventButton :event :tile-date="date" />
</li>
</TransitionGroup>
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
<UiPopover>
<UiPopoverTrigger as-child>
<button
class="text-xs px-2 py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:bg-slate-800"
>
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
</button>
</UiPopoverTrigger>
<UiPopoverContent class="w-80" :align="'center'" :side="'right'">
<div class="text-center mb-4">
<span
class="inline-flex w-12 h-12 aspect-square items-center justify-center text-lg font-semibold text-slate-300 bg-slate-800 rounded-full"
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
<UiPopover>
<UiPopoverTrigger as-child>
<button
class="text-xs px-2 py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:bg-slate-800"
>
{{ date.day }}
</span>
</div>
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
<li
v-for="event in eventsForTheDay"
:key="event.title"
class="grid pointer-events-auto"
>
<CalendarEventButton :event :tile-date="date" />
</li>
</ul>
</UiPopoverContent>
</UiPopover>
</li>
</ul>
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
</button>
</UiPopoverTrigger>
<UiPopoverContent class="w-80" :align="'center'" :side="'right'">
<div class="text-center mb-4">
<span
class="inline-flex w-12 h-12 aspect-square items-center justify-center text-lg font-semibold text-slate-300 bg-slate-800 rounded-full"
>
{{ date.day }}
</span>
</div>
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
<li
v-for="event in eventsForTheDay"
:key="event.title"
class="grid pointer-events-auto"
>
<CalendarEventButton :event :tile-date="date" />
</li>
</ul>
</UiPopoverContent>
</UiPopover>
</li>
</ul>
</ClientOnly>
<ClientOnly>
<CalendarFormCreateEvent :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
</ClientOnly>
</div>
</template>
@@ -140,4 +161,21 @@ const eventsNotDisplayed = computed(
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>

View File

@@ -27,7 +27,7 @@ const moveCalendarRight = useThrottleFn(() => {
<div class="grid grid-cols-10" @wheel="handleWheel">
<CalendarStateMonthlyDayTile
v-for="day in currentMonthData.days"
:key="day"
:key="`layout-month-grid-${day}`"
:date="{
day: day,
month: currentDate.currentMonth,

View File

@@ -1,8 +1,9 @@
import { z } from 'zod'
import type { Category } from './Category'
import type { RPGDate } from './Date'
import { dateSchema, type RPGDate } from './Date'
export interface CalendarEvent {
id: number
id?: number
title: string
startDate: RPGDate
endDate?: RPGDate
@@ -13,6 +14,19 @@ export interface CalendarEvent {
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
export function isCalendarEvent(object: any): object is CalendarEvent {
return 'startDate' in object

View File

@@ -1,3 +1,5 @@
import { z } from "zod"
export interface RPGDate {
day: number
month: number
@@ -6,23 +8,8 @@ export interface RPGDate {
export type RPGDateOrder = 'asc' | 'desc'
// export function getRelativeDate(baseDate: RPGDate, relativeDate: RPGDate) {
// let newDay: number
// let newMonth: number
// let newYear: number
// 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
// // }
// }
export const dateSchema = z.object({
day: z.number().int().positive(),
month: z.number().int(),
year: z.number().int()
})

View File

@@ -18,7 +18,6 @@ watch(user, (n, _o) => {
})
const { revealAdvancedSearch } = useCalendar()
const { isAdvancedSearchOpen } = storeToRefs(useCalendar())
const sidebarMenu: MenuItem[] = [
{
@@ -34,7 +33,5 @@ const sidebarMenu: MenuItem[] = [
<Sidebar :menu-items="sidebarMenu" />
<Calendar />
<CalendarSearch v-model:model-value="isAdvancedSearchOpen" />
</div>
</template>

View 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.'
})
}
})

View 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.'
})
}
})

View 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.'
})
}
})

View File

@@ -36,6 +36,11 @@ export const useCalendar = defineStore('calendar', () => {
'century'
])
const calendarId = ref<number>(0)
function setCalendarId(data: number) {
calendarId.value = data
}
/**
* Month list (queried from API)
*/
@@ -178,7 +183,6 @@ export const useCalendar = defineStore('calendar', () => {
/**
* Moves the current date forward one month
*/
function incrementMonth(): void {
let newValue = Number(params.month) + 1
@@ -290,13 +294,19 @@ export const useCalendar = defineStore('calendar', () => {
*/
const isAdvancedSearchOpen: Ref<boolean> = ref<boolean>(false)
/**
* Opens the search modal
*/
function revealAdvancedSearch() {
isAdvancedSearchOpen.value = true
}
/**
* State for advanced search modal
*/
const isEditEventModalOpen: Ref<boolean> = ref<boolean>(false)
function revealEditEventModal() {
isEditEventModalOpen.value = true
}
/**
* Switches the active viewType
*
@@ -432,9 +442,17 @@ export const useCalendar = defineStore('calendar', () => {
* @returns 1 means the first date comes before the second, -1 means the second comes before the first, and 0 if they're identical
*/
function compareDates(a: RPGDate, b: RPGDate, order: RPGDateOrder = 'desc'): number {
// Reverses the order if specified
// Reverses the order if specified
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
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 {
calendarId,
setCalendarId,
months,
setMonths,
sortedMonths,
@@ -628,11 +648,13 @@ export const useCalendar = defineStore('calendar', () => {
getViewTypeTitle,
isCurrentScreenActive,
isAdvancedSearchOpen,
revealAdvancedSearch,
isEditEventModalOpen,
revealEditEventModal,
convertDateToDays,
getDifferenceInDays,
areDatesIdentical,
compareDates,
getRelativeString,
revealAdvancedSearch
}
})

View File

@@ -5,7 +5,8 @@ import { ref, watch, type Ref } from 'vue'
import { useCalendar } from './CalendarStore'
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[]>([])
@@ -13,17 +14,19 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
baseEvents.value = data
}
// Order base events by dates
const allEvents = computed(() => baseEvents.value.sort((a, b) => {
return compareDates(a.startDate, b.startDate, 'desc')
}))
// 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], () => {
currentEvents.value = computeCurrentEvents()
})
}, { deep: true, immediate: true })
/**
* 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
*/
function shouldEventBeDisplayed(event: CalendarEvent): boolean {
// const eventStartDateToDays = convertDateToDays(event.startDate)
// const eventEndDateToDays: number = event.endDate ? convertDateToDays(event.endDate) : 0
const isEventOnCurrentScreen =
(event.startDate.year === currentDate.currentYear &&
event.startDate.month === currentDate.currentMonth) ||
@@ -44,25 +44,6 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
event.endDate.year === currentDate.currentYear &&
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) {
case 'month':
return isEventOnCurrentScreen!
@@ -97,8 +78,7 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
}
/**
* From a base event, gets the next or previous 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**
* From a base event, gets the next or previous one in the timeline
*
* @param event The event at a given position in the data
* @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)
}
/**
* 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(
date: RPGDate,
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)
t = t.filter((i) => {
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 }
})