Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eecad6488 | ||
|
|
8ff5d5941a | ||
|
|
fe9a58b0ea | ||
|
|
dcb637021e | ||
|
|
5689d2689d | ||
|
|
91048d1b8e | ||
|
|
3db1654682 | ||
|
|
9f6d318a15 | ||
|
|
eedbf1decb | ||
|
|
bc6cbd839f | ||
|
|
bf8257e60f | ||
|
|
199cfb6634 | ||
|
|
df8af541c1 | ||
|
|
dde3f8504a | ||
|
|
01fe9f3281 | ||
|
|
bc7a400826 | ||
|
|
c99059d9a8 | ||
|
|
cd965f61e8 | ||
|
|
9d5ff6a0b8 | ||
|
|
06fccd83ca | ||
|
|
5f8ae1fb6c | ||
|
|
535a73162c | ||
|
|
49574d8ef1 | ||
|
|
323eedec56 | ||
|
|
4ddbfd9855 | ||
|
|
bb6df1d440 | ||
|
|
dd1cb1f420 | ||
|
|
11252c168e | ||
|
|
acd0476970 | ||
|
|
24c048df4d | ||
|
|
c55d690f8b | ||
|
|
39e4ac33b7 | ||
|
|
568efa2251 | ||
|
|
6a68105a0f | ||
|
|
bba01fdcf1 | ||
|
|
76a6468a04 |
@@ -20,8 +20,7 @@ const { setActiveCalendar } = useCalendar()
|
||||
setActiveCalendar(props.calendarData, props.categories)
|
||||
|
||||
const { currentConfig, jumpToDate, selectedDate } = useCalendar()
|
||||
|
||||
// const { setCharacters } = useCharacters()
|
||||
const { isReadOnly } = storeToRefs(useCalendar())
|
||||
|
||||
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
|
||||
switch (currentConfig.viewType) {
|
||||
@@ -53,8 +52,8 @@ onMounted(() => {
|
||||
<component :is="currentViewComponent" />
|
||||
|
||||
<CalendarSearch />
|
||||
<CalendarFormUpdateEvent />
|
||||
<CalendarFormDeleteEvent />
|
||||
<CalendarDialogUpdateEvent v-if="!isReadOnly" />
|
||||
<CalendarDialogDeleteEvent v-if="!isReadOnly" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@phosphor-icons/vue"
|
||||
|
||||
const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar()
|
||||
const { lastActiveEvent } = storeToRefs(useCalendar())
|
||||
const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar())
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
@@ -46,6 +46,7 @@ function deployEditModal() {
|
||||
lastActiveEvent.value = { ...props.event }
|
||||
revealEditEventModal()
|
||||
commandMenuOpened.value = false
|
||||
emit("query:close-popover")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,12 +56,13 @@ function deployDeleteModal() {
|
||||
lastActiveEvent.value = { ...props.event }
|
||||
revealDeleteEventModal()
|
||||
commandMenuOpened.value = false
|
||||
emit("query:close-popover")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopoverContent
|
||||
class=" w-96 "
|
||||
class="w-96"
|
||||
:align="'center'"
|
||||
:align-offset="50"
|
||||
:side="'left'"
|
||||
@@ -72,6 +74,7 @@ function deployDeleteModal() {
|
||||
'event-details': event.category
|
||||
}
|
||||
)"
|
||||
@focus-outside.prevent
|
||||
>
|
||||
<div ref="eventDetails" class="grid gap-1">
|
||||
<header class="pr-12">
|
||||
@@ -148,7 +151,7 @@ function deployDeleteModal() {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<menu class="absolute top-4 right-4" :class="cn({ 'top-6': event.hidden })">
|
||||
<menu v-if="!isReadOnly" class="absolute top-4 right-4" :class="cn({ 'top-6': event.hidden })">
|
||||
<UiPopover v-model:open="commandMenuOpened">
|
||||
<UiPopoverTrigger as-child>
|
||||
<UiButton size="icon" variant="outline" class="mix-blend-luminosity bg-gray-300 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-600">
|
||||
|
||||
@@ -4,13 +4,17 @@ import { useCalendar } from "@/stores/CalendarStore"
|
||||
import { PhMagnifyingGlass } from "@phosphor-icons/vue"
|
||||
|
||||
const { revealAdvancedSearch } = useCalendar()
|
||||
const { isReadOnly } = storeToRefs(useCalendar())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="pt-4 border-slate-400 dark:border-slate-700 border-b-[1px]">
|
||||
<div class="px-6 flex justify-between">
|
||||
<menu class="flex items-center gap-2">
|
||||
<li class="flex items-center">
|
||||
<li v-if="!isReadOnly">
|
||||
<CalendarDialogQuickCreateEvent />
|
||||
</li>
|
||||
<li>
|
||||
<CalendarMenuToday />
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -1,63 +1,66 @@
|
||||
<script lang="ts" setup>
|
||||
import { type RPGDate } from "@/models/Date"
|
||||
const { currentDate } = useCalendar()
|
||||
|
||||
import { PhArrowLineLeft, PhArrowLineRight } from "@phosphor-icons/vue"
|
||||
// const { toast } = useToast()
|
||||
// const { t } = useI18n()
|
||||
|
||||
const { currentDate, currentConfig, jumpToDate, getRelativeEventFromDate } = useCalendar()
|
||||
// function handleGotoPreviousEventPage(position: "next" | "prev" = "next") {
|
||||
// let fromDate: RPGDate
|
||||
|
||||
function handleGotoPreviousEventPage(position: "next" | "prev" = "next") {
|
||||
let fromDate: RPGDate
|
||||
// // To modify, obviously
|
||||
// const daysPerMonth = 32
|
||||
|
||||
// To modify, obviously
|
||||
const daysPerMonth = 32
|
||||
const monthsPerYear = 10
|
||||
// const toDay = position === "next" ? daysPerMonth : 1
|
||||
// const toMonth = position === "next" ? monthsPerYear : 0
|
||||
|
||||
const toDay = position === "next" ? daysPerMonth : 1
|
||||
const toMonth = position === "next" ? monthsPerYear : 0
|
||||
// switch (currentConfig.viewType) {
|
||||
// case "month":
|
||||
// fromDate = {
|
||||
// day: toDay,
|
||||
// month: currentDate.currentMonth,
|
||||
// year: currentDate.currentYear
|
||||
// }
|
||||
// break
|
||||
|
||||
switch (currentConfig.viewType) {
|
||||
case "month":
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
// case "year":
|
||||
// fromDate = {
|
||||
// day: toDay,
|
||||
// month: toMonth,
|
||||
// year: currentDate.currentYear
|
||||
// }
|
||||
// break
|
||||
|
||||
case "year":
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: toMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
// case "decade":
|
||||
// fromDate = {
|
||||
// day: toDay,
|
||||
// month: currentDate.currentMonth,
|
||||
// year: currentDate.currentYear
|
||||
// }
|
||||
// break
|
||||
|
||||
case "decade":
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
// case "century":
|
||||
// default:
|
||||
// fromDate = {
|
||||
// day: toDay,
|
||||
// month: currentDate.currentMonth,
|
||||
// year: currentDate.currentYear
|
||||
// }
|
||||
// break
|
||||
// }
|
||||
|
||||
case "century":
|
||||
default:
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
}
|
||||
// try {
|
||||
// const { targetDate } = getRelativeEventFromDate(fromDate, position)
|
||||
|
||||
try {
|
||||
const { targetDate } = getRelativeEventFromDate(fromDate, position)
|
||||
|
||||
jumpToDate(targetDate)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
// jumpToDate(targetDate)
|
||||
// } catch (err) {
|
||||
// toast({
|
||||
// title: t("entity.calendar.event.outOfBoundsTitle"),
|
||||
// variant: "default",
|
||||
// description: t("entity.calendar.event.outOfBoundsMessage"),
|
||||
// duration: 4000,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -73,7 +76,7 @@ function handleGotoPreviousEventPage(position: "next" | "prev" = "next") {
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<div>
|
||||
<!-- <div>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
@@ -114,6 +117,6 @@ function handleGotoPreviousEventPage(position: "next" | "prev" = "next") {
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,7 +14,7 @@ const buttonDisabledState: ComputedRef<boolean> = computed<boolean>(() => {
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton size="sm" :disabled="buttonDisabledState" @click="jumpToDefaultDate">
|
||||
<UiButton size="sm" variant="secondary" :disabled="buttonDisabledState" @click="jumpToDefaultDate">
|
||||
{{ $t('entity.calendar.date.today') }}
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
|
||||
62
components/calendar/dialog/CreateEvent.vue
Normal file
62
components/calendar/dialog/CreateEvent.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RPGDate } from "~/models/Date";
|
||||
|
||||
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar())
|
||||
const { resetSkeleton } = useCalendar()
|
||||
const popoverOpen = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
date?: RPGDate
|
||||
btnClass?: string
|
||||
}>()
|
||||
|
||||
/**
|
||||
* Opens event creation's popover
|
||||
*/
|
||||
function openEventCreatePopover() {
|
||||
// If another operation is in progress, whether it's another create popup or a modal, don't bother opening it
|
||||
if (operationInProgress.value) {
|
||||
popoverOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
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 (?)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing(e: Event) {
|
||||
e.preventDefault()
|
||||
popoverOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="popoverOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<button :class="btnClass" @click="openEventCreatePopover()" />
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent
|
||||
:align="'center'"
|
||||
:side="'right'"
|
||||
:collision-padding="60"
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="pl-3 w-[30rem] max-w-full border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="handleClosing"
|
||||
@pointer-down-outside="handleClosing"
|
||||
>
|
||||
<CalendarFormCreateEvent @event-created="handleClosing" />
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</template>
|
||||
45
components/calendar/dialog/DeleteEvent.vue
Normal file
45
components/calendar/dialog/DeleteEvent.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
const { isDeleteEventModalOpen, eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
// Watch the popover state
|
||||
watch(isDeleteEventModalOpen, (hasOpened, _o) => {
|
||||
if (hasOpened && lastActiveEvent.value) {
|
||||
eventSkeleton.value = { ...lastActiveEvent.value }
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing(e: Event): void {
|
||||
if (isLoading.value) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog v-model:open="isDeleteEventModalOpen">
|
||||
<UiAlertDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="handleClosing"
|
||||
>
|
||||
<UiAlertDialogTitle>{{ $t('entity.calendar.event.deleteDialog.title') }}</UiAlertDialogTitle>
|
||||
|
||||
<UiAlertDialogDescription>
|
||||
{{ $t('entity.calendar.event.deleteDialog.subtitle') }}
|
||||
</UiAlertDialogDescription>
|
||||
|
||||
<CalendarFormDeleteEvent />
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
29
components/calendar/dialog/QuickCreateEvent.vue
Normal file
29
components/calendar/dialog/QuickCreateEvent.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { PhPlus } from "@phosphor-icons/vue";
|
||||
|
||||
const isDialogOpen = ref<boolean>(false);
|
||||
|
||||
// Toggles the dialog
|
||||
function toggleDialog() {
|
||||
isDialogOpen.value = !isDialogOpen.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiButton @click="toggleDialog">
|
||||
<PhPlus size="18" weight="bold" />
|
||||
|
||||
<strong class="font-semibold">
|
||||
{{ $t("entity.calendar.event.newEvent") }}
|
||||
</strong>
|
||||
</UiButton>
|
||||
|
||||
<UiDialog v-model:open="isDialogOpen">
|
||||
<UiDialogContent class="border-indigo-200 dark:bg-slate-950 dark:border-indigo-950">
|
||||
<UiDialogTitle>
|
||||
{{ $t("entity.calendar.event.addSingle") }}
|
||||
</UiDialogTitle>
|
||||
<CalendarFormCreateEvent @event-created="toggleDialog" />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
45
components/calendar/dialog/UpdateEvent.vue
Normal file
45
components/calendar/dialog/UpdateEvent.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
const { eventSkeleton, lastActiveEvent, isEditEventModalOpen } = storeToRefs(useCalendar())
|
||||
|
||||
// Watch the popover state
|
||||
watch(isEditEventModalOpen, (hasOpened, _o) => {
|
||||
if (hasOpened && lastActiveEvent.value) {
|
||||
eventSkeleton.value = { ...lastActiveEvent.value }
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing() {
|
||||
isEditEventModalOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDialog v-model:open="isEditEventModalOpen">
|
||||
<UiDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="pl-3 min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="(e) => e.preventDefault()"
|
||||
>
|
||||
<header class="pl-8 grid gap-y-2">
|
||||
<UiDialogTitle>
|
||||
{{ $t('entity.calendar.event.editDialog.title') }}
|
||||
</UiDialogTitle>
|
||||
|
||||
<UiDialogDescription>
|
||||
{{ $t('entity.calendar.event.editDialog.subtitle') }}
|
||||
</UiDialogDescription>
|
||||
</header>
|
||||
|
||||
<CalendarFormUpdateEvent @event-updated="handleClosing" />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
import { PhAlarm, PhCalendarDots, PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
|
||||
|
||||
const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: []}
|
||||
const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: [], state: "draft" }
|
||||
const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton })
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import type { RPGDate } from "~/models/Date";
|
||||
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from "@phosphor-icons/vue"
|
||||
|
||||
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar())
|
||||
const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendar()
|
||||
const popoverOpen = ref(false)
|
||||
const emit = defineEmits(["event-created"])
|
||||
|
||||
const { eventSkeleton } = storeToRefs(useCalendar())
|
||||
const { submitSkeleton, cancelLatestRequest } = useCalendar()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -19,26 +20,6 @@ const props = defineProps<{
|
||||
btnClass?: string
|
||||
}>()
|
||||
|
||||
/**
|
||||
* Opens event creation's popover
|
||||
*/
|
||||
function openEventCreatePopover() {
|
||||
// If another operation is in progress, whether it's another create popup or a modal, don't bother opening it
|
||||
if (operationInProgress.value) {
|
||||
popoverOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
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 (?)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
// Prevent form submission if already loading
|
||||
if (isLoading.value) return
|
||||
@@ -48,7 +29,7 @@ async function handleSubmit() {
|
||||
try {
|
||||
await submitSkeleton()
|
||||
|
||||
popoverOpen.value = false
|
||||
emit("event-created")
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
formErrors.message = err.message
|
||||
@@ -58,17 +39,6 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing(e: Event) {
|
||||
if (isLoading.value) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
@@ -81,160 +51,141 @@ function handleCancel() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="popoverOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<button :class="btnClass" @click="openEventCreatePopover()" />
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent
|
||||
:align="'center'"
|
||||
:side="'right'"
|
||||
:collision-padding="60"
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="pl-3 w-[30rem] max-w-full border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="handleClosing"
|
||||
>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="grid grid-cols-2 gap-y-3">
|
||||
<div class="col-span-2 pl-8">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="grid grid-cols-2 gap-y-3">
|
||||
<div class="col-span-2 pl-8">
|
||||
<input
|
||||
id="new-event-title"
|
||||
v-model="eventSkeleton.title"
|
||||
type="text"
|
||||
name="new-event-title"
|
||||
required
|
||||
:placeholder="$t('entity.calendar.event.title')"
|
||||
:maxlength="120"
|
||||
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}"
|
||||
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 invalid:border-red-500"
|
||||
>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.title') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 my-2 pl-8">
|
||||
<textarea
|
||||
id="new-event-description"
|
||||
v-model="eventSkeleton.description"
|
||||
name="new-event-description"
|
||||
:placeholder="$t('entity.addDescription')"
|
||||
:maxlength="1200"
|
||||
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 invalid:border-red-500"
|
||||
/>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.description') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.startDate"
|
||||
:placeholder="$t('entity.calendar.date.start')"
|
||||
:initial-date="props.date"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<span>—</span>
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.endDate"
|
||||
:placeholder="$t('entity.calendar.date.end')"
|
||||
:initial-date="props.date"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<CalendarInputEventCategory
|
||||
v-model="eventSkeleton.category"
|
||||
:placeholder="$t('entity.category.addPrimary')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" :placeholder="$t('entity.category.addSecondaries')" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="col-span-2 mb-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhMapPinArea size="18" weight="fill" />
|
||||
|
||||
<div class="grow">
|
||||
<input
|
||||
id="new-event-title"
|
||||
v-model="eventSkeleton.title"
|
||||
id="new-event-location"
|
||||
v-model="eventSkeleton.location"
|
||||
type="text"
|
||||
name="new-event-title"
|
||||
required
|
||||
:placeholder="$t('entity.calendar.event.title')"
|
||||
:maxlength="120"
|
||||
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}"
|
||||
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 invalid:border-red-500"
|
||||
name="new-event-location"
|
||||
:placeholder="$t('entity.calendar.event.addLocation')"
|
||||
:maxlength="160"
|
||||
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
|
||||
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
|
||||
>
|
||||
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.title') }}
|
||||
{{ t('entity.calendar.event.patterns.location') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 my-2 pl-8">
|
||||
<textarea
|
||||
id="new-event-description"
|
||||
v-model="eventSkeleton.description"
|
||||
name="new-event-description"
|
||||
:placeholder="$t('entity.addDescription')"
|
||||
:maxlength="1200"
|
||||
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 invalid:border-red-500"
|
||||
/>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.description') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.startDate"
|
||||
:placeholder="$t('entity.calendar.date.start')"
|
||||
:initial-date="props.date"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<span>—</span>
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.endDate"
|
||||
:placeholder="$t('entity.calendar.date.end')"
|
||||
:initial-date="props.date"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<CalendarInputEventCategory
|
||||
v-model="eventSkeleton.category"
|
||||
:placeholder="$t('entity.category.addPrimary')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" :placeholder="$t('entity.category.addSecondaries')" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="col-span-2 mb-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhMapPinArea size="18" weight="fill" />
|
||||
|
||||
<div class="grow">
|
||||
<input
|
||||
id="new-event-location"
|
||||
v-model="eventSkeleton.location"
|
||||
type="text"
|
||||
name="new-event-location"
|
||||
:placeholder="$t('entity.calendar.event.addLocation')"
|
||||
:maxlength="160"
|
||||
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
|
||||
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
|
||||
>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.location') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhEye v-if="!eventSkeleton.hidden" size="18" weight="fill" />
|
||||
<PhEyeClosed v-else size="18" />
|
||||
|
||||
<div class="flex items-center gap-x-2">
|
||||
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
|
||||
<UiLabel for="new-event-visibility">
|
||||
<template v-if="!eventSkeleton.hidden">
|
||||
{{ $t('entity.calendar.event.isPublic') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('entity.calendar.event.isHidden') }}
|
||||
</template>
|
||||
</UiLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-red-500 pl-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="opacity-50 animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.save') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhEye v-if="!eventSkeleton.hidden" size="18" weight="fill" />
|
||||
<PhEyeClosed v-else size="18" />
|
||||
|
||||
<div class="flex items-center gap-x-2">
|
||||
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
|
||||
<UiLabel for="new-event-visibility">
|
||||
<template v-if="!eventSkeleton.hidden">
|
||||
{{ $t('entity.calendar.event.isPublic') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('entity.calendar.event.isHidden') }}
|
||||
</template>
|
||||
</UiLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-red-500 pl-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="opacity-50 animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.save') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import { useToast } from "~/components/ui/toast";
|
||||
|
||||
const { resetSkeleton, deleteEventFromSkeleton, cancelLatestRequest } = useCalendar()
|
||||
const { isDeleteEventModalOpen, eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
|
||||
const { isDeleteEventModalOpen, eventSkeleton } = storeToRefs(useCalendar())
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
@@ -14,13 +14,6 @@ const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
|
||||
// Watch the popover state
|
||||
watch(isDeleteEventModalOpen, (hasOpened, _o) => {
|
||||
if (hasOpened && lastActiveEvent.value) {
|
||||
eventSkeleton.value = { ...lastActiveEvent.value }
|
||||
}
|
||||
})
|
||||
|
||||
async function handleAction(): Promise<void> {
|
||||
if (isLoading.value) return
|
||||
|
||||
@@ -48,17 +41,6 @@ async function handleAction(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing(e: Event): void {
|
||||
if (isLoading.value) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
@@ -71,53 +53,35 @@ function handleCancel(): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog v-model:open="isDeleteEventModalOpen">
|
||||
<UiAlertDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="handleClosing"
|
||||
>
|
||||
<UiAlertDialogTitle>{{ $t('entity.calendar.event.deleteDialog.title') }}</UiAlertDialogTitle>
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UiAlertDialogDescription>
|
||||
{{ $t('entity.calendar.event.deleteDialog.subtitle') }}
|
||||
</UiAlertDialogDescription>
|
||||
<footer class="flex gap-2 justify-between">
|
||||
<UiButton type="button" size="sm" variant="outline" @click="() => isDeleteEventModalOpen = false">
|
||||
{{ $t('ui.action.back') }}
|
||||
</UiButton>
|
||||
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-2 justify-between">
|
||||
<UiButton type="button" size="sm" variant="outline" @click="() => isDeleteEventModalOpen = false">
|
||||
{{ $t('ui.action.back') }}
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
<UiButton size="sm" variant="destructive" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" variant="destructive" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.delete') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
{{ $t('ui.action.delete') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -3,8 +3,10 @@ import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpl
|
||||
import { useToast } from "~/components/ui/toast";
|
||||
import type { APIError } from "~/models/Errors";
|
||||
|
||||
const emit = defineEmits(["event-updated"])
|
||||
|
||||
const { resetSkeleton, updateEventFromSkeleton, cancelLatestRequest } = useCalendar()
|
||||
const { eventSkeleton, lastActiveEvent, isEditEventModalOpen } = storeToRefs(useCalendar())
|
||||
const { eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
@@ -15,13 +17,6 @@ const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
|
||||
// Watch the popover state
|
||||
watch(isEditEventModalOpen, (hasOpened, _o) => {
|
||||
if (hasOpened && lastActiveEvent.value) {
|
||||
eventSkeleton.value = { ...lastActiveEvent.value }
|
||||
}
|
||||
})
|
||||
|
||||
async function handleAction() {
|
||||
if (isLoading.value) return
|
||||
|
||||
@@ -29,6 +24,14 @@ async function handleAction() {
|
||||
|
||||
try {
|
||||
await updateEventFromSkeleton()
|
||||
|
||||
emit("event-updated")
|
||||
|
||||
toast({
|
||||
title: t("entity.calendar.event.updatedToast.title", { event: eventSkeleton.value.title }),
|
||||
variant: "success",
|
||||
duration: 3000
|
||||
})
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const apiError = (err as any).data as APIError
|
||||
@@ -44,18 +47,6 @@ async function handleAction() {
|
||||
} finally {
|
||||
resetSkeleton()
|
||||
isLoading.value = false
|
||||
isEditEventModalOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing(e: Event) {
|
||||
if (isLoading.value) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,174 +62,152 @@ function handleCancel() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDialog v-model:open="isEditEventModalOpen">
|
||||
<UiDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="pl-3 min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="(e) => e.preventDefault()"
|
||||
>
|
||||
<header class="pl-8 grid gap-y-2">
|
||||
<UiDialogTitle>
|
||||
{{ $t('entity.calendar.event.editDialog.title') }}
|
||||
</UiDialogTitle>
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-3">
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhPencilSimpleLine size="20" weight="fill" />
|
||||
|
||||
<UiDialogDescription>
|
||||
{{ $t('entity.calendar.event.editDialog.subtitle') }}
|
||||
</UiDialogDescription>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-3">
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhPencilSimpleLine size="20" weight="fill" />
|
||||
|
||||
<div class="grow">
|
||||
<input
|
||||
id="new-event-title"
|
||||
v-model="eventSkeleton.title"
|
||||
type="text"
|
||||
name="new-event-title"
|
||||
required
|
||||
:placeholder="$t('entity.calendar.event.title')"
|
||||
:maxlength="120"
|
||||
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}"
|
||||
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 invalid:border-red-500"
|
||||
>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.title') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 my-2 ml-8">
|
||||
<textarea
|
||||
id="new-event-description"
|
||||
v-model="eventSkeleton.description"
|
||||
name="new-event-description"
|
||||
:placeholder="$t('entity.addDescription')"
|
||||
:maxlength="1200"
|
||||
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 invalid:border-red-500"
|
||||
/>
|
||||
<div class="grow">
|
||||
<input
|
||||
id="new-event-title"
|
||||
v-model="eventSkeleton.title"
|
||||
type="text"
|
||||
name="new-event-title"
|
||||
required
|
||||
:placeholder="$t('entity.calendar.event.title')"
|
||||
:minlength="3"
|
||||
:maxlength="120"
|
||||
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 invalid:border-red-500"
|
||||
>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.description') }}
|
||||
{{ t('entity.calendar.event.patterns.title') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.startDate"
|
||||
:placeholder="$t('entity.calendar.date.start')"
|
||||
:initial-date="lastActiveEvent?.startDate"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<span>—</span>
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.endDate"
|
||||
:placeholder="$t('entity.calendar.date.end')"
|
||||
:initial-date="lastActiveEvent?.endDate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<div class="w-1/2">
|
||||
<CalendarInputEventCategory
|
||||
v-model="eventSkeleton.category"
|
||||
:placeholder="$t('entity.category.addPrimary')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<div class="w-1/2">
|
||||
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" placeholder="Ajouter des catégories secondaires" />
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="col-span-2 mb-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhMapPinArea size="18" weight="fill" />
|
||||
|
||||
<div class="grow">
|
||||
<input
|
||||
id="new-event-location"
|
||||
v-model="eventSkeleton.location"
|
||||
type="text"
|
||||
name="new-event-location"
|
||||
:placeholder="$t('entity.calendar.event.addLocation')"
|
||||
:maxlength="160"
|
||||
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
|
||||
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500"
|
||||
>
|
||||
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.location') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhEye v-if="!eventSkeleton.hidden" size="18" weight="fill" />
|
||||
<PhEyeClosed v-else size="18" />
|
||||
|
||||
<div class="flex items-center gap-x-2">
|
||||
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
|
||||
<UiLabel for="new-event-visibility">
|
||||
<template v-if="!eventSkeleton.hidden">
|
||||
{{ $t('entity.calendar.event.isPublic') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('entity.calendar.event.isHidden') }}
|
||||
</template>
|
||||
</UiLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
<div class="col-span-2 my-2 ml-8">
|
||||
<textarea
|
||||
id="new-event-description"
|
||||
v-model="eventSkeleton.description"
|
||||
name="new-event-description"
|
||||
:placeholder="$t('entity.addDescription')"
|
||||
:maxlength="1200"
|
||||
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 invalid:border-red-500"
|
||||
/>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.description') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UiButton size="sm" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhAlarm size="18" weight="fill" />
|
||||
|
||||
{{ $t('ui.action.save') }}
|
||||
</UiButton>
|
||||
</footer>
|
||||
</form>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.startDate"
|
||||
:placeholder="$t('entity.calendar.date.start')"
|
||||
:initial-date="lastActiveEvent?.startDate"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<span>—</span>
|
||||
|
||||
<CalendarInputRPGDate
|
||||
v-model:model-value="eventSkeleton.endDate"
|
||||
:placeholder="$t('entity.calendar.date.end')"
|
||||
:initial-date="lastActiveEvent?.endDate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<div class="w-1/2">
|
||||
<CalendarInputEventCategory
|
||||
v-model="eventSkeleton.category"
|
||||
:placeholder="$t('entity.category.addPrimary')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhTag size="18" weight="fill" />
|
||||
|
||||
<div class="w-1/2">
|
||||
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" placeholder="Ajouter des catégories secondaires" />
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="col-span-2 mb-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhMapPinArea size="18" weight="fill" />
|
||||
|
||||
<div class="grow">
|
||||
<input
|
||||
id="new-event-location"
|
||||
v-model="eventSkeleton.location"
|
||||
type="text"
|
||||
name="new-event-location"
|
||||
:placeholder="$t('entity.calendar.event.addLocation')"
|
||||
:minlength="3"
|
||||
:maxlength="160"
|
||||
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500"
|
||||
>
|
||||
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.location') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<PhEye v-if="!eventSkeleton.hidden" size="18" weight="fill" />
|
||||
<PhEyeClosed v-else size="18" />
|
||||
|
||||
<div class="flex items-center gap-x-2">
|
||||
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
|
||||
<UiLabel for="new-event-visibility">
|
||||
<template v-if="!eventSkeleton.hidden">
|
||||
{{ $t('entity.calendar.event.isPublic') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('entity.calendar.event.isHidden') }}
|
||||
</template>
|
||||
</UiLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.save') }}
|
||||
</UiButton>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -16,7 +16,7 @@ const calendarTile = ref()
|
||||
const calendarEventsList = ref()
|
||||
|
||||
const { defaultDate, selectDate, areDatesIdentical } = useCalendar()
|
||||
const { selectedDate, currentEvents } = storeToRefs(useCalendar())
|
||||
const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar())
|
||||
|
||||
/**
|
||||
* All events with a startDate / endDate that starts or ends on the tile
|
||||
@@ -143,7 +143,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
||||
</ClientOnly>
|
||||
|
||||
<ClientOnly>
|
||||
<LazyCalendarFormCreateEvent :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||
<LazyCalendarDialogCreateEvent v-if="!isReadOnly" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhGlobeHemisphereWest, PhList } from "@phosphor-icons/vue"
|
||||
import { PhCompass, PhList } from "@phosphor-icons/vue"
|
||||
import type { SidebarMenuActionType } from "./SidebarProps";
|
||||
|
||||
const { revealAdvancedSearch } = useCalendar()
|
||||
const { currentMenu } = storeToRefs(useUiStore())
|
||||
|
||||
const user = useSupabaseUser()
|
||||
// const user = useSupabaseUser()
|
||||
|
||||
function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
||||
if (actionType === "event-search") {
|
||||
@@ -23,19 +23,19 @@ function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
||||
</UiButton>
|
||||
</li>
|
||||
|
||||
<li v-if="user">
|
||||
<li>
|
||||
<UiTooltipProvider :delay-duration="50">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="ghost" size="icon" class="rounded-full" as-child>
|
||||
<RouterLink to="/my">
|
||||
<PhGlobeHemisphereWest size="24" weight="fill" />
|
||||
<RouterLink to="/explore">
|
||||
<PhCompass size="24" weight="fill" />
|
||||
</RouterLink>
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent :side="'right'" :side-offset="6">
|
||||
<p>
|
||||
{{ $t('entity.world.namePlural') }}
|
||||
{{ $t('pages.explore.menuLabel') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
|
||||
import { PhCheckCircle, PhGear, PhGlobeHemisphereWest, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
|
||||
import { PhCheckCircle, PhGear, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
const router = useRouter()
|
||||
@@ -82,9 +82,9 @@ function pushRoute(to: AvailableRoutes) {
|
||||
</p>
|
||||
|
||||
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my')">
|
||||
<PhGlobeHemisphereWest size="20" weight="fill" />
|
||||
<PhUser size="20" weight="fill" />
|
||||
<span>
|
||||
{{ $t('entity.world.namePlural') }}
|
||||
{{ $t('ui.sidebarMenu.profile') }}
|
||||
</span>
|
||||
</UiDropdownMenuItem>
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export default defineI18nConfig(() => ({
|
||||
greeting: "Connected as {user}",
|
||||
anonymousGreeting: "Preferences",
|
||||
sidebarMenu: {
|
||||
profile: "Profile",
|
||||
appearance: "Appearance",
|
||||
language: "Language",
|
||||
account: "Account",
|
||||
@@ -94,9 +95,14 @@ export default defineI18nConfig(() => ({
|
||||
calendar: {
|
||||
nameSingular: "Calendar",
|
||||
namePlural: "Calendars",
|
||||
namePublicSingular: "Public Calendar",
|
||||
namePublicPlural: "Public Calendars",
|
||||
addSingle: "Add a calendar",
|
||||
notFound: "Calendar not found",
|
||||
notFoundDescription: "The link is not valid or the calendar has been deleted / archived.",
|
||||
notFoundForWorld: "No calendar for this world… yet !",
|
||||
isLoading: "Calendar is loading…",
|
||||
hasXEvents: "This calendar has {count} public events",
|
||||
date: {
|
||||
start: "Start date",
|
||||
end: "End date",
|
||||
@@ -127,6 +133,8 @@ export default defineI18nConfig(() => ({
|
||||
event: {
|
||||
nameSingular: "Event",
|
||||
namePlural: "Events",
|
||||
addSingle: "Add an event",
|
||||
newEvent: "New event",
|
||||
title: "Event title",
|
||||
isStart: "Start",
|
||||
isEnd: "End",
|
||||
@@ -136,15 +144,20 @@ export default defineI18nConfig(() => ({
|
||||
addLocation: "Add a place",
|
||||
prevPage: "Previous page with events",
|
||||
nextPage: "Next page with events",
|
||||
outOfBoundsTitle: "No next or previous events were found",
|
||||
outOfBoundsMessage: "Maybe the event is the first or last one ?",
|
||||
patterns: {
|
||||
title: "Between 3 and 120 caracters, without special symbols, except & - ~ ()",
|
||||
title: "Between 3 and 120 caracters.",
|
||||
description: "1200 caracters max.",
|
||||
location: "Between 3 and 160 caracters, without special symbols, except & - ~ ()",
|
||||
location: "Between 3 and 160 caracters.",
|
||||
},
|
||||
editDialog: {
|
||||
title: "Edit event",
|
||||
subtitle: "Update event data",
|
||||
},
|
||||
updatedToast: {
|
||||
title: "Event \"{event}\" has been successfuly updated.",
|
||||
},
|
||||
editErrors: {
|
||||
toastTitle: "Event wasn't updated.",
|
||||
title_too_big: "Title should be less than 120 characters long.",
|
||||
@@ -220,6 +233,12 @@ export default defineI18nConfig(() => ({
|
||||
nameSingular: "Character",
|
||||
namePlural: "Characters",
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
explore: {
|
||||
menuLabel: "Explore",
|
||||
title: "Explore worlds",
|
||||
}
|
||||
}
|
||||
},
|
||||
fr: {
|
||||
@@ -260,6 +279,7 @@ export default defineI18nConfig(() => ({
|
||||
greeting: "Connecté en tant que {user}",
|
||||
anonymousGreeting: "Préférences",
|
||||
sidebarMenu: {
|
||||
profile: "Profil",
|
||||
appearance: "Apparence",
|
||||
language: "Langue",
|
||||
account: "Compte",
|
||||
@@ -313,9 +333,14 @@ export default defineI18nConfig(() => ({
|
||||
calendar: {
|
||||
nameSingular: "Calendriers",
|
||||
namePlural: "Calendrier",
|
||||
namePublicSingular: "Calendrier Public",
|
||||
namePublicPlural: "Calendriers Publics",
|
||||
addSingle: "Ajouter un calendrier",
|
||||
notFound: "Aucun calendrier trouvé",
|
||||
notFoundDescription: "Le lien n'est pas valide ou le calendrier a été supprimé / archivé.",
|
||||
notFoundForWorld: "Aucun calendrier pour ce monde… pour l'instant !",
|
||||
isLoading: "Chargement du calendrier…",
|
||||
hasXEvents: "Ce calendrier contient {count} évènements publiques",
|
||||
date: {
|
||||
start: "Date de début",
|
||||
end: "Date de fin",
|
||||
@@ -346,6 +371,8 @@ export default defineI18nConfig(() => ({
|
||||
event: {
|
||||
nameSingular: "Évènement",
|
||||
namePlural: "Évènements",
|
||||
addSingle: "Ajouter un évènement",
|
||||
newEvent: "Nouvel évènement",
|
||||
title: "Titre de l'évènement",
|
||||
isStart: "Début",
|
||||
isEnd: "Fin",
|
||||
@@ -355,15 +382,20 @@ export default defineI18nConfig(() => ({
|
||||
addLocation: "Ajouter un endroit",
|
||||
prevPage: "Précédente page à évènements",
|
||||
nextPage: "Prochaine page à évènements",
|
||||
outOfBoundsTitle: "Aucun évènement suivant ou précédent trouvé",
|
||||
outOfBoundsMessage: "Peut-être l'évènement se situe au début ou à la fin du calendrier ?",
|
||||
patterns: {
|
||||
title: "Entre 3 et 120 charactères, sans symboles spéciaux, sauf & - ~ ()",
|
||||
title: "Entre 3 et 120 charactères.",
|
||||
description: "Maximum 1200 charactères.",
|
||||
location: "Entre 3 et 160 charactères, sans symboles spéciaux, sauf & - ~ ()",
|
||||
location: "Entre 3 et 160 charactères.",
|
||||
},
|
||||
editDialog: {
|
||||
title: "Modifier l'évènement",
|
||||
subtitle: "Mettre à jour les données de l'évènement",
|
||||
},
|
||||
updatedToast: {
|
||||
title: "L'évènement \"{event}\" a été modifié avec succès.",
|
||||
},
|
||||
editErrors: {
|
||||
toastTitle: "L'évènement n'a pas été modifié",
|
||||
title_too_big: "Le titre doit être inférieur à 120 caractères.",
|
||||
@@ -440,6 +472,12 @@ export default defineI18nConfig(() => ({
|
||||
nameSingular: "Personnage",
|
||||
namePlural: "Personnages",
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
explore: {
|
||||
menuLabel: "Explorer",
|
||||
title: "Explorer les mondes",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { z } from "zod"
|
||||
import type { CalendarEvent } from "./CalendarEvent"
|
||||
import { calendarMonthSchema, type CalendarMonth } from "./CalendarMonth"
|
||||
import { dateSchema, type RPGDate } from "./Date"
|
||||
import type { World } from "./World"
|
||||
|
||||
export type CalendarState = "published" | "draft" | "archived"
|
||||
|
||||
export interface CalendarConfig {
|
||||
months: CalendarMonth[]
|
||||
@@ -10,9 +13,13 @@ export interface CalendarConfig {
|
||||
|
||||
export interface Calendar extends CalendarConfig {
|
||||
id?: number
|
||||
shortId?: string
|
||||
name: string
|
||||
events: CalendarEvent[]
|
||||
eventNb?: Array<{ count: number }>
|
||||
state: CalendarState
|
||||
color?: string
|
||||
world?: World
|
||||
}
|
||||
|
||||
export const postCalendarSchema = z.object({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: false },
|
||||
devtools: { enabled: true },
|
||||
|
||||
modules: [
|
||||
"@nuxtjs/supabase",
|
||||
@@ -13,6 +13,7 @@ export default defineNuxtConfig({
|
||||
"@nuxtjs/i18n",
|
||||
"@nuxthub/core"
|
||||
],
|
||||
|
||||
css: ["~/assets/main.css"],
|
||||
|
||||
i18n: {
|
||||
@@ -37,7 +38,11 @@ export default defineNuxtConfig({
|
||||
supabase: {
|
||||
redirectOptions: {
|
||||
login: "/",
|
||||
callback: "/my/"
|
||||
callback: "/my/",
|
||||
exclude: [
|
||||
"/calendars(/*)?",
|
||||
"/explore(/*)?"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,14 +52,18 @@ export default defineNuxtConfig({
|
||||
autoprefixer: {},
|
||||
},
|
||||
},
|
||||
|
||||
shadcn: {
|
||||
prefix: "Ui",
|
||||
componentDir: "./components/ui"
|
||||
},
|
||||
|
||||
colorMode: {
|
||||
classSuffix: "",
|
||||
preference: "dark",
|
||||
fallback: "dark",
|
||||
},
|
||||
eslint: {}
|
||||
|
||||
eslint: {},
|
||||
compatibilityDate: "2025-02-14"
|
||||
})
|
||||
68
package.json
68
package.json
@@ -13,44 +13,44 @@
|
||||
"lint:prettier": "prettier . --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/eslint": "^0.3.12",
|
||||
"@nuxthub/core": "^0.8.7",
|
||||
"@nuxtjs/i18n": "^8.5.1",
|
||||
"@nuxt/eslint": "^1.1.0",
|
||||
"@nuxthub/core": "^0.8.17",
|
||||
"@nuxtjs/i18n": "^9.2.1",
|
||||
"@phosphor-icons/vue": "^2.2.1",
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@vueuse/integrations": "^10.11.0",
|
||||
"@vueuse/nuxt": "^10.9.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"@pinia/nuxt": "^0.10.1",
|
||||
"@vueuse/core": "^12.7.0",
|
||||
"@vueuse/integrations": "^12.7.0",
|
||||
"@vueuse/nuxt": "^12.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^0.364.0",
|
||||
"nuxt": "^3.7.0",
|
||||
"pinia": "^2.1.7",
|
||||
"radix-vue": "^1.7.4",
|
||||
"shadcn-nuxt": "^0.10.4",
|
||||
"sortablejs": "^1",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"lucide-vue-next": "^0.475.0",
|
||||
"nuxt": "^3.15.4",
|
||||
"pinia": "^3.0.1",
|
||||
"radix-vue": "^1.9.16",
|
||||
"shadcn-nuxt": "^0.11.4",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2",
|
||||
"zod": "^3.23.8"
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/color-mode": "^3.4.1",
|
||||
"@nuxtjs/supabase": "^1.2.2",
|
||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||
"@stylistic/eslint-plugin-js": "^2.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
||||
"@typescript-eslint/parser": "^7.8.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.72.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "~5.4.0",
|
||||
"wrangler": "^3.90.0"
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/supabase": "^1.4.6",
|
||||
"@nuxtjs/tailwindcss": "^6.13.1",
|
||||
"@stylistic/eslint-plugin-js": "^3.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||
"@typescript-eslint/parser": "^8.25.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.2",
|
||||
"sass": "^1.85.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3",
|
||||
"wrangler": "^3.111.0"
|
||||
}
|
||||
}
|
||||
|
||||
59
pages/calendars/[id].vue
Normal file
59
pages/calendars/[id].vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
import type { Category } from "~/models/Category";
|
||||
|
||||
const route = useRoute()
|
||||
const shortId = route.params.id
|
||||
|
||||
const user = useSupabaseUser()
|
||||
|
||||
const { data: calendarData, pending: calPending, refresh: calRefresh } = await useLazyFetch<{ data: Calendar }>("/api/calendars/query", { query: { shortId, full: true } })
|
||||
const { data: catData, pending: catPending, refresh: catRefresh } = await useLazyFetch<{ data: Category[] }>("/api/calendars/categories/query")
|
||||
|
||||
watch(user, () => {
|
||||
calRefresh()
|
||||
catRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="calPending || catPending" class="h-full w-full grid place-items-center">
|
||||
<Head>
|
||||
<Title>{{ $t("entity.calendar.nameSingular") }}</Title>
|
||||
</Head>
|
||||
|
||||
<div class="grid gap-2 justify-items-center opacity-50">
|
||||
<p>
|
||||
{{ $t('entity.calendar.isLoading') }}
|
||||
</p>
|
||||
<PhCircleNotch size="50" class="animate-spin"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="calendarData && catData" class="h-full w-full">
|
||||
<Head>
|
||||
<Title>{{ calendarData.data.name }}</Title>
|
||||
</Head>
|
||||
|
||||
<Calendar :calendar-data="calendarData.data" :categories="catData.data" />
|
||||
</div>
|
||||
|
||||
<div v-else class="h-full w-full grid place-items-center">
|
||||
<Head>
|
||||
<Title>{{ $t("entity.calendar.notFound") }}</Title>
|
||||
</Head>
|
||||
|
||||
<div class="grid justify-items-center opacity-80">
|
||||
<PhCalendarX size="75" class="opacity-60" />
|
||||
|
||||
<Heading level="h1">
|
||||
{{ $t("entity.calendar.notFound") }}
|
||||
</Heading>
|
||||
|
||||
<p>
|
||||
{{ $t('entity.calendar.notFoundDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
11
pages/calendars/index.vue
Normal file
11
pages/calendars/index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<main class="p-8">
|
||||
<Head>
|
||||
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
||||
</Head>
|
||||
|
||||
<Heading level="h1">Calendriers publics</Heading>
|
||||
</main>
|
||||
</template>
|
||||
46
pages/explore.vue
Normal file
46
pages/explore.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
|
||||
const { data } = await useFetch("/api/calendars/query", { key: "explore-calendars" })
|
||||
|
||||
const availableCalendars = data.value?.data as Calendar[]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="p-8">
|
||||
<Head>
|
||||
<Title>{{ $t("pages.explore.title") }}</Title>
|
||||
</Head>
|
||||
|
||||
<Spacing size="lg">
|
||||
<Heading level="h1">
|
||||
{{ $t("pages.explore.title") }}
|
||||
</Heading>
|
||||
|
||||
<Spacing size="lg">
|
||||
<Heading level="h2">
|
||||
{{ $t("entity.calendar.namePublicPlural") }}
|
||||
</Heading>
|
||||
|
||||
<ul v-if="availableCalendars && availableCalendars?.length > 0" class="grid md:grid-cols-3 gap-2">
|
||||
<li v-for="calendar in availableCalendars" :key="calendar.shortId">
|
||||
<UiCard
|
||||
class="w-full transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900"
|
||||
:link="`/calendars/${calendar.shortId}`"
|
||||
>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
|
||||
<UiCardContent>
|
||||
<p>
|
||||
{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</li>
|
||||
</ul>
|
||||
</Spacing>
|
||||
</Spacing>
|
||||
</main>
|
||||
</template>
|
||||
10016
pnpm-lock.yaml
generated
10016
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import type { Calendar } from "~/models/CalendarConfig";
|
||||
|
||||
const querySchema = z.object({
|
||||
id: z.number({ coerce: true }).positive().int().optional(),
|
||||
shortId: z.string().optional(),
|
||||
full: z.boolean({ coerce: true }).optional()
|
||||
})
|
||||
|
||||
@@ -11,20 +12,23 @@ export default defineEventHandler(async (event) => {
|
||||
const client = await serverSupabaseClient(event)
|
||||
const query = await getValidatedQuery(event, querySchema.parse)
|
||||
|
||||
setHeader(event, "Cache-Control", "public, max-age=3600, s-maxage=7200")
|
||||
|
||||
const partialFields = `
|
||||
id,
|
||||
shortId:short_id,
|
||||
name,
|
||||
today,
|
||||
months:calendar_months (*)
|
||||
months:calendar_months (*),
|
||||
state,
|
||||
eventNb:calendar_events(count)
|
||||
`
|
||||
|
||||
const fullFields = `
|
||||
id,
|
||||
shortId:short_id,
|
||||
name,
|
||||
today,
|
||||
months:calendar_months (*),
|
||||
state,
|
||||
events:calendar_events (
|
||||
id,
|
||||
title,
|
||||
@@ -36,6 +40,11 @@ export default defineEventHandler(async (event) => {
|
||||
wiki,
|
||||
category:calendar_event_categories!calendar_events_category_fkey (*),
|
||||
secondaryCategories:calendar_event_categories!calendar_event_categories_links (*)
|
||||
),
|
||||
eventNb:calendar_events(count),
|
||||
world:worlds (
|
||||
id,
|
||||
gmId:gm_id
|
||||
)
|
||||
`
|
||||
|
||||
@@ -47,6 +56,10 @@ export default defineEventHandler(async (event) => {
|
||||
output = client.from("calendars").select(partialFields)
|
||||
}
|
||||
|
||||
if (query.shortId) {
|
||||
return output.eq("short_id", query.shortId).limit(1).single<Calendar>()
|
||||
}
|
||||
|
||||
if (query.id) {
|
||||
return output.eq("id", query.id).limit(1).single<Calendar>()
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type DateDirectionTranslationKeys = {
|
||||
|
||||
export const useCalendar = defineStore("calendar", () => {
|
||||
const { t } = useI18n()
|
||||
const user = useSupabaseUser()
|
||||
|
||||
/**
|
||||
* Static calendar config
|
||||
@@ -45,7 +46,14 @@ export const useCalendar = defineStore("calendar", () => {
|
||||
"year"
|
||||
])
|
||||
|
||||
const activeCalendar = ref<{ id: number; name: string; today: RPGDate} | null>(null)
|
||||
const activeCalendar = ref<{ id: number; name: string; today: RPGDate, gmId: string | undefined } | null>(null)
|
||||
|
||||
const isReadOnly = ref<boolean>(true)
|
||||
|
||||
function setReadStatus(gmId: string) {
|
||||
// If the user is not logged in, or the calendar is not owned by the user, it's read-only
|
||||
isReadOnly.value = (!user) || (gmId !== user.value?.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Month list (queried from API)
|
||||
@@ -59,11 +67,13 @@ export const useCalendar = defineStore("calendar", () => {
|
||||
activeCalendar.value = {
|
||||
id: calendarData.id,
|
||||
name: calendarData.name,
|
||||
today: calendarData.today
|
||||
today: calendarData.today,
|
||||
gmId: calendarData.world?.gmId
|
||||
}
|
||||
|
||||
setDefaultDate(activeCalendar.value.today)
|
||||
selectDate(activeCalendar.value.today)
|
||||
setReadStatus(activeCalendar.value.gmId!)
|
||||
|
||||
if (!params.day) {
|
||||
params.day = defaultDate.value.day.toString()
|
||||
@@ -901,6 +911,8 @@ export const useCalendar = defineStore("calendar", () => {
|
||||
}
|
||||
|
||||
return {
|
||||
isReadOnly,
|
||||
setReadStatus,
|
||||
setActiveCalendar,
|
||||
activeCalendar,
|
||||
months,
|
||||
|
||||
@@ -7,6 +7,9 @@ create type public.app_permission as enum ('events.see.hidden', 'users.ban');
|
||||
create type public.app_role as enum ('sa', 'admin', 'moderator', 'user');
|
||||
create type public.app_colors as enum ('red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose', 'black', 'white');
|
||||
|
||||
create type public.world_state as enum ('published', 'draft', 'archived');
|
||||
create type public.calendar_state as enum ('published', 'draft', 'archived');
|
||||
|
||||
-- DATA STRUCTURES
|
||||
-- Users
|
||||
create table public.users (
|
||||
@@ -40,16 +43,29 @@ create table public.worlds (
|
||||
name text not null,
|
||||
description text,
|
||||
color app_colors default 'black',
|
||||
state world_state default 'draft',
|
||||
gm_id uuid references public.users on delete cascade
|
||||
);
|
||||
comment on table public.worlds is 'Worlds belonging to a single user ; a game master.';
|
||||
|
||||
-- World Players (join table)
|
||||
create table public.world_players (
|
||||
world_id bigint references public.worlds on delete cascade,
|
||||
player_id uuid references public.users on delete cascade,
|
||||
joined_at timestamptz default now(),
|
||||
primary key (world_id, player_id)
|
||||
);
|
||||
comment on table public.world_players is 'Players (users) that belong to specific worlds';
|
||||
comment on column public.world_players.joined_at is 'Timestamp when the player joined the world';
|
||||
|
||||
-- World Calendars
|
||||
create table public.calendars (
|
||||
id bigint generated by default as identity primary key,
|
||||
short_id text unique,
|
||||
name text not null,
|
||||
today json not null,
|
||||
color app_colors default 'black',
|
||||
state calendar_state default 'draft',
|
||||
world_id bigint references public.worlds on delete cascade not null
|
||||
);
|
||||
comment on table public.calendars is 'Calendar settings and configuration attached to a single world.';
|
||||
@@ -82,7 +98,7 @@ create table public.calendar_events (
|
||||
start_date json not null,
|
||||
end_date json,
|
||||
category bigint references public.calendar_event_categories on delete cascade,
|
||||
hidden boolean,
|
||||
hidden boolean default false,
|
||||
wiki text,
|
||||
calendar_id bigint references public.calendars on delete cascade not null
|
||||
);
|
||||
@@ -160,6 +176,7 @@ alter table public.users enable row level security;
|
||||
alter table public.user_roles enable row level security;
|
||||
alter table public.role_permissions enable row level security;
|
||||
alter table public.worlds enable row level security;
|
||||
alter table public.world_players enable row level security;
|
||||
alter table public.character_categories enable row level security;
|
||||
alter table public.character_categories_links enable row level security;
|
||||
alter table public.characters enable row level security;
|
||||
@@ -175,18 +192,26 @@ alter publication supabase_realtime add table worlds;
|
||||
alter publication supabase_realtime add table calendars;
|
||||
|
||||
-- User policies
|
||||
create policy "Allow public read access" on public.users for select to authenticated, anon using ( true );
|
||||
create policy "Allow logged-in read access" on public.users for select using ( auth.role() = 'authenticated' );
|
||||
create policy "Allow individual insert access" on public.users for insert with check ( auth.uid() = id );
|
||||
create policy "Allow individual update access" on public.users for update using ( auth.uid() = id );
|
||||
create policy "Allow individual read access" on public.user_roles for select using ( auth.uid() = user_id );
|
||||
|
||||
-- World policies
|
||||
create policy "Allow anonymous access to published worlds" on public.worlds
|
||||
for select
|
||||
using (state = 'published');
|
||||
create policy "Allow GMs to see their worlds" on public.worlds for select using ( ( auth.uid() = gm_id ) );
|
||||
create policy "Allow GMs to create worlds" on public.worlds for insert with check ( auth.uid() = gm_id );
|
||||
create policy "Allow GMs to edit their worlds" on public.worlds for update using ( auth.uid() = gm_id );
|
||||
create policy "Allow GMs to delete their worlds" on public.worlds for delete using ( auth.uid() = gm_id );
|
||||
|
||||
-- Calendar policies
|
||||
create policy "Allow anonymous access to published calendars" on public.calendars
|
||||
for select
|
||||
using (state = 'published');
|
||||
|
||||
create policy "Allow GMs to see their calendars" on public.calendars for select using (
|
||||
exists (
|
||||
select 1 from worlds
|
||||
@@ -213,6 +238,17 @@ create policy "Allow GMs to delete their calendars" on public.calendars for dele
|
||||
);
|
||||
|
||||
-- Month policies
|
||||
create policy "Allow anonymous access to months in published calendars" ON public.calendar_months
|
||||
for select
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.calendars
|
||||
where calendars.id = calendar_months.calendar_id
|
||||
and calendars.state = 'published'
|
||||
)
|
||||
);
|
||||
|
||||
create policy "Allow GMs to see their calendar's months"
|
||||
on public.calendar_months
|
||||
for select
|
||||
@@ -267,6 +303,17 @@ create policy "Allow GMs to delete their calendar's months"
|
||||
);
|
||||
|
||||
-- Event policies
|
||||
create policy "Allow anonymous access to non-hidden events in published calendars" ON public.calendar_events
|
||||
for select
|
||||
using (
|
||||
not hidden and exists (
|
||||
select 1
|
||||
from public.calendars
|
||||
where calendars.id = calendar_events.calendar_id
|
||||
and calendars.state = 'published'
|
||||
)
|
||||
);
|
||||
|
||||
create policy "Allow GMs to see their events"
|
||||
on public.calendar_events
|
||||
for select
|
||||
@@ -342,8 +389,8 @@ create policy "Allow individual update access for GMs" on public.characters for
|
||||
|
||||
-- Categories are public to view but not to insert
|
||||
-- Needs to be refactored maybe, if in the future we want a default set AND user defined ones
|
||||
create policy "Allow logged-in read access" on public.calendar_event_categories for select using ( auth.role() = 'authenticated' );
|
||||
create policy "Allow logged-in read access" on public.calendar_event_categories_links for select using ( auth.role() = 'authenticated' );
|
||||
create policy "Allow all read access" on public.calendar_event_categories for select to authenticated, anon using ( true );
|
||||
create policy "Allow all read access" on public.calendar_event_categories_links for select to authenticated, anon using ( true );
|
||||
create policy "Allow logged-in read access" on public.character_categories for select using ( auth.role() = 'authenticated' );
|
||||
create policy "Allow logged-in read access" on public.character_categories_links for select using ( auth.role() = 'authenticated' );
|
||||
|
||||
@@ -391,3 +438,48 @@ begin
|
||||
return user_id;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
-- Add short IDs to calendars
|
||||
-- Function to generate YouTube-like IDs
|
||||
CREATE OR REPLACE FUNCTION public.generate_short_id()
|
||||
RETURNS text AS $$
|
||||
DECLARE
|
||||
characters text := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||
result text := '';
|
||||
i integer;
|
||||
BEGIN
|
||||
FOR i IN 1..11 LOOP
|
||||
result := result || substr(characters, floor(random() * length(characters) + 1)::integer, 1);
|
||||
END LOOP;
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql VOLATILE;
|
||||
|
||||
-- Trigger function to set unique short ID
|
||||
CREATE OR REPLACE FUNCTION public.set_calendar_short_id()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
new_id text;
|
||||
attempts integer := 0;
|
||||
BEGIN
|
||||
LOOP
|
||||
new_id := generate_short_id();
|
||||
BEGIN
|
||||
NEW.short_id = new_id;
|
||||
RETURN NEW;
|
||||
EXCEPTION WHEN unique_violation THEN
|
||||
attempts := attempts + 1;
|
||||
IF attempts > 5 THEN
|
||||
RAISE EXCEPTION 'Could not generate unique ID after 5 attempts';
|
||||
END IF;
|
||||
CONTINUE;
|
||||
END;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger
|
||||
CREATE TRIGGER set_calendar_short_id
|
||||
BEFORE INSERT ON calendars
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_calendar_short_id();
|
||||
|
||||
@@ -36,10 +36,10 @@ insert into public.character_categories (name, color) values ('Activiste', 'sky'
|
||||
insert into public.character_categories (name, color) values ('Commerçant', 'amber');
|
||||
|
||||
-- Worlds
|
||||
insert into public.worlds (name, description, color) values ('Léïm', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis aliquet congue aliquet. Curabitur eu iaculis diam. Nunc iaculis nibh orci, eu semper nunc congue congue. Praesent euismod tortor eget metus tristique lobortis vel in risus. In volutpat ligula orci, id pharetra lectus egestas at.', 'black');
|
||||
insert into public.worlds (name, description, color, state) values ('Léïm', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis aliquet congue aliquet. Curabitur eu iaculis diam. Nunc iaculis nibh orci, eu semper nunc congue congue. Praesent euismod tortor eget metus tristique lobortis vel in risus. In volutpat ligula orci, id pharetra lectus egestas at.', 'black', 'published');
|
||||
|
||||
-- Worlds' calendars
|
||||
insert into public.calendars (world_id, name, today) values (1, 'Calendrier solaire', '{ "day": 23, "month": 8, "year": 3209 }');
|
||||
insert into public.calendars (world_id, name, today, state) values (1, 'Calendrier solaire', '{ "day": 23, "month": 8, "year": 3209 }', 'published');
|
||||
|
||||
-- Calendar's months
|
||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Jalen', 32, 1);
|
||||
|
||||
Reference in New Issue
Block a user