Added post component for events
This needs RLS disabled, will work on that later, I don't like postgre
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 } = 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,7 @@ onMounted(() => {
|
|||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<CalendarSearch v-model:model-value="isAdvancedSearchOpen" />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
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,47 +99,53 @@ const eventsNotDisplayed = computed(
|
|||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul
|
<ClientOnly>
|
||||||
ref="calendarEventsList"
|
<ul
|
||||||
class="absolute top-12 bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
|
ref="calendarEventsList"
|
||||||
:class="{
|
class="absolute top-12 bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
|
||||||
'opacity-40': props.faded && !isSelectedDate
|
: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 v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
|
||||||
</li>
|
<CalendarEventButton :event :tile-date="date" />
|
||||||
|
</li>
|
||||||
|
|
||||||
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
|
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
|
||||||
<UiPopover>
|
<UiPopover>
|
||||||
<UiPopoverTrigger as-child>
|
<UiPopoverTrigger as-child>
|
||||||
<button
|
<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"
|
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"
|
|
||||||
>
|
>
|
||||||
{{ date.day }}
|
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</UiPopoverTrigger>
|
||||||
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
|
<UiPopoverContent class="w-80" :align="'center'" :side="'right'">
|
||||||
<li
|
<div class="text-center mb-4">
|
||||||
v-for="event in eventsForTheDay"
|
<span
|
||||||
:key="event.title"
|
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"
|
||||||
class="grid pointer-events-auto"
|
>
|
||||||
>
|
{{ date.day }}
|
||||||
<CalendarEventButton :event :tile-date="date" />
|
</span>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
|
||||||
</UiPopoverContent>
|
<li
|
||||||
</UiPopover>
|
v-for="event in eventsForTheDay"
|
||||||
</li>
|
:key="event.title"
|
||||||
</ul>
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
startDate: dateSchema.required(),
|
||||||
|
endDate: dateSchema.optional()
|
||||||
|
}),
|
||||||
|
calendarId: z.number().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>
|
||||||
|
|||||||
36
server/api/calendars/events/create.post.ts
Normal file
36
server/api/calendars/events/create.post.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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()
|
||||||
|
.returns<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
|
||||||
|
|
||||||
@@ -598,6 +602,8 @@ export const useCalendar = defineStore('calendar', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
calendarId,
|
||||||
|
setCalendarId,
|
||||||
months,
|
months,
|
||||||
setMonths,
|
setMonths,
|
||||||
sortedMonths,
|
sortedMonths,
|
||||||
|
|||||||
@@ -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[]>([])
|
||||||
|
|
||||||
@@ -97,8 +98,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 +120,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 +163,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 +185,29 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { allEvents, setEvents, currentEvents, getRelativeEventFromDate, getRelativeEventFromEvent }
|
/**
|
||||||
|
* EVENT CREATION FUNCTIONS
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
return await $fetch(`/api/calendars/events/create`, { method: 'POST', body: { event : eventSkeleton.value, calendarId: calendarId.value }})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allEvents, setEvents, currentEvents, getRelativeEventFromDate, getRelativeEventFromEvent, eventSkeleton, resetSkeleton, submitSkeleton }
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user