Merge pull request #69 from AlexisNP/features/draft-published-state

Features/draft published state
This commit is contained in:
AlexisNP
2025-03-10 21:58:21 +01:00
committed by GitHub
26 changed files with 583 additions and 83 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhCalendarDots, PhFilePlus, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue"; import { PhArchive, PhCalendarDots, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "~/models/CalendarConfig";
@@ -9,7 +9,7 @@ const props = defineProps<{
showActions?: boolean, showActions?: boolean,
}>() }>()
const emit = defineEmits(["on-delete"]) const emit = defineEmits(["on-edit", "on-delete"])
const { locale } = useI18n(); const { locale } = useI18n();
@@ -23,38 +23,76 @@ const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calen
<template> <template>
<UiCard <UiCard
class="w-full h-full transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900" class="w-full h-full flex flex-col transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900"
:link="calendarLink" :link="calendarLink"
:class="{
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !calendar.color,
'bg-red-100 dark:bg-red-950 border-red-200 hover:bg-red-50 dark:hover:bg-red-900 dark:border-red-900 dark:focus-within:outline-red-900': calendar.color === 'red',
'bg-orange-100 dark:bg-orange-950 border-orange-200 hover:bg-orange-50 dark:hover:bg-orange-900 dark:border-orange-900 dark:focus-within:outline-orange-900': calendar.color === 'orange',
'bg-amber-100 dark:bg-amber-950 border-amber-200 hover:bg-amber-50 dark:hover:bg-amber-900 dark:border-amber-900 dark:focus-within:outline-amber-900': calendar.color === 'amber',
'bg-yellow-100 dark:bg-yellow-950 border-yellow-200 hover:bg-yellow-50 dark:hover:bg-yellow-900 dark:border-yellow-900 dark:focus-within:outline-yellow-900': calendar.color === 'yellow',
'bg-lime-100 dark:bg-lime-950 border-lime-200 hover:bg-lime-50 dark:hover:bg-lime-900 dark:border-lime-900 dark:focus-within:outline-lime-900': calendar.color === 'lime',
' bg-green-100 dark:bg-green-950 border-green-200 hover:bg-green-50 dark:hover:bg-green-900 dark:border-green-900 dark:focus-within:outline-green-900': calendar.color === 'green',
'bg-emerald-100 dark:bg-emerald-950 border-emerald-200 hover:bg-emerald-50 dark:hover:bg-emerald-900 dark:border-emerald-900 dark:focus-within:outline-emerald-900': calendar.color === 'emerald',
'bg-teal-100 dark:bg-teal-950 border-teal-200 hover:bg-teal-50 dark:hover:bg-teal-900 dark:border-teal-900 dark:focus-within:outline-teal-900': calendar.color === 'teal',
'bg-cyan-100 dark:bg-cyan-950 border-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-900 dark:border-cyan-900 dark:focus-within:outline-cyan-900': calendar.color === 'cyan',
'bg-sky-100 dark:bg-sky-950 border-sky-200 hover:bg-sky-50 dark:hover:bg-sky-900 dark:border-sky-900 dark:focus-within:outline-sky-900': calendar.color === 'sky',
'bg-blue-100 dark:bg-blue-950 border-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900 dark:border-blue-900 dark:focus-within:outline-blue-900': calendar.color === 'blue',
'bg-indigo-100 dark:bg-indigo-950 border-indigo-200 hover:bg-indigo-50 dark:hover:bg-indigo-900 dark:border-indigo-900 dark:focus-within:outline-indigo-900': calendar.color === 'indigo',
'bg-violet-100 dark:bg-violet-950 border-violet-200 hover:bg-violet-50 dark:hover:bg-violet-900 dark:border-violet-900 dark:focus-within:outline-violet-900': calendar.color === 'violet',
'bg-purple-100 dark:bg-purple-950 border-purple-200 hover:bg-purple-50 dark:hover:bg-purple-900 dark:border-purple-900 dark:focus-within:outline-purple-900': calendar.color === 'purple',
'bg-fuchsia-100 dark:bg-fuchsia-950 border-fuchsia-200 hover:bg-fuchsia-50 dark:hover:bg-fuchsia-900 dark:border-fuchsia-900 dark:focus-within:outline-fuchsia-900': calendar.color === 'fuchsia',
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': calendar.color === 'pink',
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': calendar.color === 'rose',
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': calendar.color === 'black',
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-200 dark:border-slate-700 dark:focus-within:outline-slate-100': calendar.color === 'white',
}"
> >
<UiCardHeader> <UiCardHeader class="gap-4">
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle> <UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
<div v-if="calendar.state === 'published'" class="flex items-center gap-1 text-sm">
<PhFile size="20" weight="fill" />
<span>{{ $t('ui.contentState.published') }}</span>
</div>
<div v-if="calendar.state === 'draft'" class="flex items-center gap-1 text-sm">
<PhFileDashed size="20" weight="fill" />
<span>{{ $t('ui.contentState.draft') }}</span>
</div>
<div v-if="calendar.state === 'archived'" class="flex items-center gap-1 text-sm">
<PhArchive size="20" weight="fill" />
<span>{{ $t('ui.contentState.archived') }}</span>
</div>
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent class="grow">
<p class="flex items-center gap-1"> <p class="flex items-center gap-1">
<PhCalendarDots size="24" weight="fill" /> <PhCalendarDots size="24" weight="fill" />
<span>{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span> <span>{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span>
</p> </p>
<UiButton <div
v-if="isOwner && showActions" v-if="isOwner && showActions"
size="icon" class="flex gap-1 absolute top-4 right-4 z-20"
variant="ghost"
class="absolute top-2 right-2 z-20 hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700"
@click="emit('on-delete')"
> >
<PhTrash size="16" /> <UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="emit('on-edit')">
</UiButton> <PhPencil size="16" />
</UiButton>
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="emit('on-delete')">
<PhTrash size="16" />
</UiButton>
</div>
</UiCardContent> </UiCardContent>
<UiCardFooter> <UiCardFooter>
<ul class="grid gap-2"> <ul class="grid gap-1 text-sm">
<li class="flex gap-1 items-center"> <li class="flex gap-1 items-center">
<PhFilePlus size="20" /> <PhFilePlus size="18" />
<span>{{ $t('common.createdAt', { createdAt }) }}</span> <span>{{ $t('common.createdAt', { createdAt }) }}</span>
</li> </li>
<li v-if="updatedAt" class="flex gap-1 items-center"> <li v-if="updatedAt" class="flex gap-1 items-center">
<PhPencilSimpleLine size="20" /> <PhPencilSimpleLine size="18" />
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span> <span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
</li> </li>
</ul> </ul>

View File

@@ -17,6 +17,7 @@ const emit = defineEmits(["on-close"])
function handleClose() { function handleClose() {
emit("on-close") emit("on-close")
setTimeout(() => calendarSkeletonName.value = "", 100)
} }
</script> </script>

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue";
import type { Calendar } from "~/models/CalendarConfig";
import type { World } from "~/models/World";
const props = defineProps<{
calendar: Calendar | null,
world: World | null,
modalState?: boolean
}>()
const calendarSkeletonName = ref<string>(props.calendar?.name ?? "")
function onChangedName(newName: string) {
calendarSkeletonName.value = newName
}
const emit = defineEmits(["on-close"])
function handleClose() {
emit("on-close")
setTimeout(() => calendarSkeletonName.value = "", 100)
}
</script>
<template>
<UiAlertDialog :open="modalState">
<UiAlertDialogContent class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl gap-6" @close-auto-focus="(e) => e.preventDefault()">
<UiAlertDialogTitle>
<span class="text-2xl">
<strong class="font-bold">{{ world?.name }}</strong>
<span class="opacity-30"> — </span>
<span v-if="calendarSkeletonName">
{{ calendarSkeletonName }}
</span>
<span v-else>
{{ $t('entity.calendar.updateDialog.title') }}
</span>
</span>
</UiAlertDialogTitle>
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClose">
<PhX size="20" />
</UiButton>
<CalendarFormUpdate v-if="calendar" :world="world" :calendar="calendar" @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -2,7 +2,7 @@
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "~/models/CalendarConfig";
import { PhAlarm, PhCalendarDots, PhCircleNotch, PhWrench } from "@phosphor-icons/vue"; import { PhAlarm, PhCalendarDots, PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: [], state: "draft" } const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: [], state: "draft", color: "white" }
const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton }) const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton })
onMounted(() => { onMounted(() => {
@@ -79,25 +79,25 @@ function handleFormCancel() {
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<PhWrench size="18" weight="fill" /> <PhWrench size="18" weight="fill" />
{{ $t('entity.calendar.createDialog.tabs.general.title') }} {{ $t('entity.calendar.actionDialog.tabs.general.title') }}
</div> </div>
</UiTabsTrigger> </UiTabsTrigger>
<UiTabsTrigger value="months" class="font-bold"> <UiTabsTrigger value="months" class="font-bold">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<PhCalendarDots size="18" weight="fill" /> <PhCalendarDots size="18" weight="fill" />
{{ $t('entity.calendar.createDialog.tabs.months.title') }} {{ $t('entity.calendar.actionDialog.tabs.months.title') }}
</div> </div>
</UiTabsTrigger> </UiTabsTrigger>
<UiTabsTrigger value="today" class="font-bold"> <UiTabsTrigger value="today" class="font-bold">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<PhAlarm size="18" weight="fill" /> <PhAlarm size="18" weight="fill" />
{{ $t('entity.calendar.createDialog.tabs.today.title') }} {{ $t('entity.calendar.actionDialog.tabs.today.title') }}
</div> </div>
</UiTabsTrigger> </UiTabsTrigger>
</UiTabsList> </UiTabsList>
<UiTabsContent value="global"> <UiTabsContent value="global" class="grid gap-4">
<input <input
id="new-calendar-name" id="new-calendar-name"
v-model="calendarSkeleton.name" v-model="calendarSkeleton.name"
@@ -105,9 +105,25 @@ function handleFormCancel() {
name="new-calendar-name" name="new-calendar-name"
required required
:placeholder="$t('common.title')" :placeholder="$t('common.title')"
class="w-full -my-1 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
@input="handleNameChange" @input="handleNameChange"
> >
<div class="-mx-1 px-1 grid gap-3">
<UiLabel for="new-calendar-state">
{{ $t('ui.contentState.label') }}
</UiLabel>
<InputContentState id="new-calendar-state" v-model="calendarSkeleton.state" />
</div>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-calendar-color">
{{ $t('ui.colors.label') }}
</UiLabel>
<InputColor id="new-calendar-color" v-model="calendarSkeleton.color" />
</div>
</UiTabsContent> </UiTabsContent>
<UiTabsContent value="months"> <UiTabsContent value="months">
<CalendarInputMonthList v-model:model-value="calendarSkeleton.months" /> <CalendarInputMonthList v-model:model-value="calendarSkeleton.months" />

View File

@@ -0,0 +1,147 @@
<script lang="ts" setup>
import type { Calendar } from "~/models/CalendarConfig";
import { PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
import type { World } from "~/models/World";
const props = defineProps<{
calendar: Calendar | null,
world: World | null,
}>()
const calendarSkeleton = ref<Calendar>({ ...props.calendar } as Calendar)
onMounted(() => {
calendarSkeleton.value = { ...props.calendar } as Calendar
})
type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global")
/**
* === Form Validation ===
*/
/** Whether the skeleton has valid month data */
const validSkeletonMonths = computed(() => calendarSkeleton.value.months.length > 0)
/** Whether the skeleton has a valid name */
const validSkeletonGeneral = computed(() => calendarSkeleton.value.name)
/** Whether all the data checks above are a-ok */
const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeletonMonths.value)
/** Send the data to the store for validation */
const isUpdatingCalendar = ref<boolean>(false)
async function handleSubmit() {
try {
isUpdatingCalendar.value = true
await $fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: { ...calendarSkeleton.value, worldId: props.world?.id } })
emit("on-close")
} catch (err) {
console.log(err)
} finally {
isUpdatingCalendar.value = false
}
}
/**
* === Watch for name changes to display above ===
*/
const emit = defineEmits<{
// eslint-disable-next-line no-unused-vars
(e: "on-changed-name", calendarName: string): void
// eslint-disable-next-line no-unused-vars
(e: "on-close"): void
}>()
/** Hook to emit a debounced event for the changed skeleton name */
const handleNameChange = useDebounceFn(() => {
emit("on-changed-name", calendarSkeleton.value.name)
}, 400)
function handleFormCancel() {
emit("on-close")
}
</script>
<template>
<template v-if="calendarSkeleton">
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<UiTabs v-model:model-value="activeTab">
<UiTabsList class="grid w-full grid-cols-1 mb-4">
<UiTabsTrigger value="global" class="font-bold">
<div class="flex items-center gap-1">
<PhWrench size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.general.title') }}
</div>
</UiTabsTrigger>
<!-- <UiTabsTrigger value="months" class="font-bold">
<div class="flex items-center gap-1">
<PhCalendarDots size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.months.title') }}
</div>
</UiTabsTrigger>
<UiTabsTrigger value="today" class="font-bold">
<div class="flex items-center gap-1">
<PhAlarm size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.today.title') }}
</div>
</UiTabsTrigger> -->
</UiTabsList>
<UiTabsContent value="global" class="grid gap-4">
<input
id="new-calendar-name"
v-model="calendarSkeleton.name"
type="text"
name="new-calendar-name"
required
:placeholder="$t('common.title')"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
@input="handleNameChange"
>
<div class="-mx-1 px-1 grid gap-3">
<UiLabel for="new-calendar-state">
{{ $t('ui.contentState.label') }}
</UiLabel>
<InputContentState id="new-calendar-state" v-model="calendarSkeleton.state" />
</div>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-calendar-color">
{{ $t('ui.colors.label') }}
</UiLabel>
<InputColor id="new-calendar-color" v-model="calendarSkeleton.color" />
</div>
</UiTabsContent>
<!-- <UiTabsContent value="months">
<CalendarInputMonthList v-model:model-value="calendarSkeleton.months" />
</UiTabsContent>
<UiTabsContent value="today">
<CalendarInputTodaySelect v-model:model-value="calendarSkeleton.today" :available-months="calendarSkeleton.months"/>
</UiTabsContent> -->
</UiTabs>
<footer class="flex justify-end gap-2 mt-6">
<UiButton type="button" variant="destructive" @click="handleFormCancel">
{{ $t('ui.action.cancel') }}
</UiButton>
<UiButton type="submit" :disabled="!validSkeleton || isUpdatingCalendar">
<Transition name="fade">
<PhCircleNotch v-if="isUpdatingCalendar" size="20" class="opacity-50 animate-spin"/>
</Transition>
{{ $t('ui.action.save') }}
</UiButton>
</footer>
</form>
</template>
</template>

View File

@@ -6,7 +6,7 @@ const emit = defineEmits(["on-click"])
<template> <template>
<UiCard <UiCard
class="h-full md:w-fit transition-all bg-transparent dark:hover:bg-gray-950 dark:focus-within:outline-gray-900 hover:-translate-y-0" class="h-full md:w-fit transition-all bg-transparent hover:text-slate-900 text-slate-500 dark:hover:text-slate-100 dark:text-slate-500 dark:focus-within:outline-gray-900 hover:-translate-y-0"
has-click has-click
@on-click="emit('on-click')" @on-click="emit('on-click')"
> >

View File

@@ -2,12 +2,16 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { type RPGColor, rpgColors } from "~/models/Color"; import { type RPGColor, rpgColors } from "~/models/Color";
defineProps<{
id: string
}>();
const model = defineModel<RPGColor>({ default: "white" }); const model = defineModel<RPGColor>({ default: "white" });
</script> </script>
<template> <template>
<UiSelect v-model="model"> <UiSelect v-model="model">
<UiSelectTrigger> <UiSelectTrigger :id>
<UiSelectValue <UiSelectValue
:placeholder="$t('ui.colors.selectOne')" :placeholder="$t('ui.colors.selectOne')"
class="input-color" class="input-color"

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { contentStates, type ContentState } from "~/models/Entity";
defineProps<{
id: string
}>();
const model = defineModel<ContentState>({ default: "draft" });
</script>
<template>
<UiSelect v-model="model">
<UiSelectTrigger :id>
<UiSelectValue
:placeholder="$t('ui.contentState.selectOne')"
/>
</UiSelectTrigger>
<UiSelectContent position="popper">
<UiSelectGroup>
<UiSelectItem
v-for="state in contentStates"
:key="state"
:value="state"
>
{{ $t(`ui.contentState.${state}`) }}
</UiSelectItem>
</UiSelectGroup>
</UiSelectContent>
</UiSelect>
</template>

View File

@@ -17,7 +17,7 @@ const delegatedProps = computed(() => {
v-bind="delegatedProps" v-bind="delegatedProps"
:class=" :class="
cn( cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer',
props.class, props.class,
) )
" "

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue"; import { PhArchive, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
import type { World } from "~/models/World"; import type { World } from "~/models/World";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
@@ -16,7 +16,7 @@ const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromIS
<template> <template>
<UiCard <UiCard
class="w-full h-full transition-all" class="w-full h-full flex flex-col transition-all"
:link="`/my/worlds/${world.id}`" :link="`/my/worlds/${world.id}`"
:class="{ :class="{
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !world.color, 'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !world.color,
@@ -38,13 +38,28 @@ const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromIS
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': world.color === 'pink', 'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': world.color === 'pink',
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': world.color === 'rose', 'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': world.color === 'rose',
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': world.color === 'black', 'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': world.color === 'black',
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-300 dark:border-slate-500 dark:focus-within:outline-slate-100': world.color === 'white', ' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-200 dark:border-slate-700 dark:focus-within:outline-slate-100': world.color === 'white',
}" }"
> >
<UiCardHeader> <UiCardHeader class="gap-4">
<UiCardTitle>{{ world.name }}</UiCardTitle> <UiCardTitle>
{{ world.name }}
</UiCardTitle>
<div v-if="world.state === 'published'" class="flex items-center gap-1 text-sm">
<PhFile size="20" weight="fill" />
<span>{{ $t('ui.contentState.published') }}</span>
</div>
<div v-if="world.state === 'draft'" class="flex items-center gap-1 text-sm">
<PhFileDashed size="20" weight="fill" />
<span>{{ $t('ui.contentState.draft') }}</span>
</div>
<div v-if="world.state === 'archived'" class="flex items-center gap-1 text-sm">
<PhArchive size="20" weight="fill" />
<span>{{ $t('ui.contentState.archived') }}</span>
</div>
</UiCardHeader> </UiCardHeader>
<UiCardContent>
<UiCardContent class="grow">
<p class="italic">{{ world.description }}</p> <p class="italic">{{ world.description }}</p>
<div class="flex gap-1 absolute top-4 right-4 z-20"> <div class="flex gap-1 absolute top-4 right-4 z-20">
@@ -59,13 +74,13 @@ const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromIS
</UiCardContent> </UiCardContent>
<UiCardFooter> <UiCardFooter>
<ul class="grid gap-2"> <ul class="grid gap-1 text-sm">
<li class="flex gap-1 items-center"> <li class="flex gap-1 items-center">
<PhFilePlus size="20" /> <PhFilePlus size="18" />
<span>{{ $t('common.createdAt', { createdAt }) }}</span> <span>{{ $t('common.createdAt', { createdAt }) }}</span>
</li> </li>
<li v-if="updatedAt" class="flex gap-1 items-center"> <li v-if="updatedAt" class="flex gap-1 items-center">
<PhPencilSimpleLine size="20" /> <PhPencilSimpleLine size="18" />
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span> <span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
</li> </li>
</ul> </ul>

View File

@@ -48,7 +48,7 @@ function handleClose() {
<PhX size="20" /> <PhX size="20" />
</UiButton> </UiButton>
<WorldFormEdit :world @on-changed-name="onChangedName" @on-close="handleClose" /> <WorldFormUpdate :world @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent> </UiAlertDialogContent>
</UiAlertDialog> </UiAlertDialog>
</template> </template>

View File

@@ -57,7 +57,7 @@ function handleFormCancel() {
<template> <template>
<template v-if="worldSkeleton"> <template v-if="worldSkeleton">
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit"> <form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<div> <div class="grid gap-4">
<input <input
id="new-world-name" id="new-world-name"
v-model="worldSkeleton.name" v-model="worldSkeleton.name"
@@ -69,10 +69,6 @@ function handleFormCancel() {
@input="handleNameChange" @input="handleNameChange"
> >
<div class="-mx-1 mb-4">
<InputColor v-model="worldSkeleton.color" />
</div>
<textarea <textarea
id="new-world-description" id="new-world-description"
v-model="worldSkeleton.description" v-model="worldSkeleton.description"
@@ -80,6 +76,22 @@ function handleFormCancel() {
:placeholder="$t('entity.addDescription')" :placeholder="$t('entity.addDescription')"
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" 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 class="-mx-1 grid gap-3">
<UiLabel for="new-world-state">
{{ $t('ui.contentState.label') }}
</UiLabel>
<InputContentState id="new-world-state" v-model="worldSkeleton.state" />
</div>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-world-color">
{{ $t('ui.colors.label') }}
</UiLabel>
<InputColor id="new-world-color" v-model="worldSkeleton.color" />
</div>
</div> </div>
<footer class="flex justify-end gap-2 mt-6"> <footer class="flex justify-end gap-2 mt-6">

View File

@@ -71,7 +71,7 @@ function handleFormCancel() {
<template> <template>
<template v-if="worldSkeleton"> <template v-if="worldSkeleton">
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit"> <form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<div> <div class="grid gap-4">
<input <input
id="new-world-name" id="new-world-name"
v-model="worldSkeleton.name" v-model="worldSkeleton.name"
@@ -79,14 +79,10 @@ function handleFormCancel() {
name="new-world-name" name="new-world-name"
required required
:placeholder="$t('common.title')" :placeholder="$t('common.title')"
class="w-full -my-1 mb-4 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" class="w-full -my-1 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
@input="handleNameChange" @input="handleNameChange"
> >
<div class="-mx-1 mb-4">
<InputColor v-model="worldSkeleton.color" />
</div>
<textarea <textarea
id="new-world-description" id="new-world-description"
v-model="worldSkeleton.description" v-model="worldSkeleton.description"
@@ -94,6 +90,22 @@ function handleFormCancel() {
:placeholder="$t('entity.addDescription')" :placeholder="$t('entity.addDescription')"
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" 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 class="-mx-1 grid gap-3">
<UiLabel for="new-world-state">
{{ $t('ui.contentState.label') }}
</UiLabel>
<InputContentState id="new-world-state" v-model="worldSkeleton.state" />
</div>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-world-color">
{{ $t('ui.colors.label') }}
</UiLabel>
<InputColor id="new-world-color" v-model="worldSkeleton.color" />
</div>
</div> </div>
<footer class="flex justify-end gap-2 mt-6"> <footer class="flex justify-end gap-2 mt-6">

View File

@@ -17,6 +17,7 @@ export default defineI18nConfig(() => ({
edit: "Edit", edit: "Edit",
}, },
colors: { colors: {
label: "Color",
selectOne: "Select a color", selectOne: "Select a color",
red: "Red", red: "Red",
orange: "Orange", orange: "Orange",
@@ -38,6 +39,13 @@ export default defineI18nConfig(() => ({
black: "Black", black: "Black",
white: "White", white: "White",
}, },
contentState: {
label: "Status",
selectOne: "Select a publication state",
published: "Published",
draft: "Draft",
archived: "Archived",
},
greeting: "Connected as {user}", greeting: "Connected as {user}",
anonymousGreeting: "Preferences", anonymousGreeting: "Preferences",
backToProfile: "Back to profile", backToProfile: "Back to profile",
@@ -189,8 +197,7 @@ export default defineI18nConfig(() => ({
title: "The event \"{event}\" has been successfuly deleted.", title: "The event \"{event}\" has been successfuly deleted.",
}, },
}, },
createDialog: { actionDialog: {
title: "Create a calendar",
tabs: { tabs: {
general: { general: {
title: "General", title: "General",
@@ -203,6 +210,9 @@ export default defineI18nConfig(() => ({
}, },
} }
}, },
createDialog: {
title: "Create a calendar",
},
deleteDialog: { deleteDialog: {
title: "Are you sure you want to delete this calendar ?", title: "Are you sure you want to delete this calendar ?",
subtitle: "Its events won't be accessible anymore and you won't be able to retrieve the deleted data !", subtitle: "Its events won't be accessible anymore and you won't be able to retrieve the deleted data !",
@@ -272,6 +282,7 @@ export default defineI18nConfig(() => ({
edit: "Modifier", edit: "Modifier",
}, },
colors: { colors: {
label: "Couleur",
selectOne: "Sélectionner une couleur", selectOne: "Sélectionner une couleur",
red: "Rouge", red: "Rouge",
orange: "Orange", orange: "Orange",
@@ -293,6 +304,13 @@ export default defineI18nConfig(() => ({
black: "Noir", black: "Noir",
white: "Blanc", white: "Blanc",
}, },
contentState: {
label: "État de publication",
selectOne: "Modifier l'état de publication",
published: "Publié",
draft: "Brouillon",
archived: "Archivé",
},
greeting: "Connecté en tant que {user}", greeting: "Connecté en tant que {user}",
anonymousGreeting: "Préférences", anonymousGreeting: "Préférences",
backToProfile: "Retour au profil", backToProfile: "Retour au profil",
@@ -444,8 +462,7 @@ export default defineI18nConfig(() => ({
title: "L'évènement \"{event}\" a été supprimé avec succès.", title: "L'évènement \"{event}\" a été supprimé avec succès.",
}, },
}, },
createDialog: { actionDialog: {
title: "Créer un calendrier",
tabs: { tabs: {
general: { general: {
title: "Général", title: "Général",
@@ -458,6 +475,12 @@ export default defineI18nConfig(() => ({
}, },
} }
}, },
createDialog: {
title: "Créer un calendrier",
},
updateDialog: {
title: "Modifier le calendrier",
},
deleteDialog: { deleteDialog: {
title: "Êtes-vous sûr de supprimer ce calendrier ?", title: "Êtes-vous sûr de supprimer ce calendrier ?",
subtitle: "Les évènements ne seront plus accessibles et vous ne pourrez plus récupérer les données !", subtitle: "Les évènements ne seront plus accessibles et vous ne pourrez plus récupérer les données !",

View File

@@ -3,8 +3,9 @@ import type { CalendarEvent } from "./CalendarEvent"
import { calendarMonthSchema, type CalendarMonth } from "./CalendarMonth" import { calendarMonthSchema, type CalendarMonth } from "./CalendarMonth"
import { dateSchema, type RPGDate } from "./Date" import { dateSchema, type RPGDate } from "./Date"
import type { World } from "./World" import type { World } from "./World"
import type { ContentState } from "./Entity"
import type { RPGColor } from "./Color"
export type CalendarState = "published" | "draft" | "archived"
export interface CalendarConfig { export interface CalendarConfig {
months: CalendarMonth[] months: CalendarMonth[]
@@ -17,17 +18,21 @@ export interface Calendar extends CalendarConfig {
name: string name: string
events: CalendarEvent[] events: CalendarEvent[]
eventNb?: Array<{ count: number }> eventNb?: Array<{ count: number }>
state: CalendarState state: ContentState
color?: string color?: RPGColor
world?: World world?: World
createdAt?: string createdAt?: string
updatedAt?: string updatedAt?: string
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CalendarChannelPayload = Calendar & Record<string, any>
export const postCalendarSchema = z.object({ export const postCalendarSchema = z.object({
name: z.string(), name: z.string(),
today: dateSchema.optional().nullable(), today: dateSchema.optional().nullable(),
color: z.string().optional().nullable(), color: z.string().optional().nullable(),
months: z.array(calendarMonthSchema).min(1), months: z.array(calendarMonthSchema).min(1),
worldId: z.number().int(), worldId: z.number().int(),
state: z.string().optional().nullable().default("draft"),
}) })

2
models/Entity.ts Normal file
View File

@@ -0,0 +1,2 @@
export const contentStates = ["published", "draft", "archived"] as const
export type ContentState = typeof contentStates[number]

View File

@@ -1,8 +1,7 @@
import { z } from "zod" import { z } from "zod"
import type { Calendar } from "./CalendarConfig" import type { Calendar } from "./CalendarConfig"
import { rpgColorSchema, type RPGColor } from "./Color" import { rpgColorSchema, type RPGColor } from "./Color"
import type { ContentState } from "./Entity"
export type WorldState = "published" | "draft" | "archived"
export interface World { export interface World {
id?: number id?: number
@@ -11,11 +10,14 @@ export interface World {
color?: RPGColor color?: RPGColor
calendars?: Calendar[] calendars?: Calendar[]
gmId?: string gmId?: string
state?: WorldState state?: ContentState
createdAt?: string createdAt?: string
updatedAt?: string updatedAt?: string
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WorldChannelPayload = World & Record<string, any>
export const postWorldSchema = z.object({ export const postWorldSchema = z.object({
name: z.string(), name: z.string(),
description: z.string().optional().nullable(), description: z.string().optional().nullable(),

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RealtimeChannel } from "@supabase/supabase-js" import type { RealtimeChannel } from "@supabase/supabase-js"
import type { World } from "~/models/World"; import type { World, WorldChannelPayload } from "~/models/World";
const supabase = useSupabaseClient() const supabase = useSupabaseClient()
const user = useSupabaseUser() const user = useSupabaseUser()
@@ -31,10 +31,14 @@ function hideCreateDialog() {
/** Active world channel */ /** Active world channel */
let worldChannel: RealtimeChannel let worldChannel: RealtimeChannel
/** Handles world insertion realtime events */ /** Handles world insertion realtime events */
function handleInsertedWorld(newWorld: World) { function handleInsertedWorld(newWorld: WorldChannelPayload) {
if (!worlds.value?.data) return if (!worlds.value?.data) return
newWorld.createdAt = newWorld.created_at;
newWorld.gmId = newWorld.gm_id;
try { try {
worlds.value?.data.push(newWorld) worlds.value?.data.push(newWorld)
} catch (err) { } catch (err) {
@@ -71,7 +75,7 @@ onMounted(() => {
case "UPDATE": case "UPDATE":
if (!worlds.value?.data) return if (!worlds.value?.data) return
worlds.value.data = (await $fetch("/api/worlds/query", { query: { gmId: user?.value!.id } })).data as World[] worlds.value.data = (await $fetch("/api/worlds/query", { query: { gmId: user?.value!.id } })).data as World[]
break break
default: default:

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RealtimeChannel } from "@supabase/supabase-js" import type { RealtimeChannel } from "@supabase/supabase-js"
import type { World } from "~/models/World"; import type { World } from "~/models/World";
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar, CalendarChannelPayload } from "~/models/CalendarConfig";
import { PhArrowBendDoubleUpLeft, PhGlobeHemisphereWest, PhPencil } from "@phosphor-icons/vue"; import { PhArrowBendDoubleUpLeft, PhGlobeHemisphereWest, PhPencil } from "@phosphor-icons/vue";
const supabase = useSupabaseClient() const supabase = useSupabaseClient()
@@ -24,12 +24,6 @@ watch(user, (n) => {
} }
}) })
const isCreateCalendarModalOpen = ref<boolean>(false)
function hideCreateDialog() {
isCreateCalendarModalOpen.value = false
}
/** /**
* === Subscriptions === * === Subscriptions ===
*/ */
@@ -40,9 +34,12 @@ let calendarChannel: RealtimeChannel
let worldChannel: RealtimeChannel let worldChannel: RealtimeChannel
/** Handles calendar insertion realtime events */ /** Handles calendar insertion realtime events */
function handleInsertedCalendar(newCalendar: Calendar) { function handleInsertedCalendar(newCalendar: CalendarChannelPayload) {
if (!world.value) return if (!world.value) return
newCalendar.createdAt = newCalendar.created_at;
newCalendar.eventNb = [{ count: 0 }];
try { try {
world.value.data.calendars?.push(newCalendar) world.value.data.calendars?.push(newCalendar)
} catch (err) { } catch (err) {
@@ -66,7 +63,7 @@ onMounted(() => {
.on( .on(
"postgres_changes", "postgres_changes",
{ event: "*", schema: "public", table: "calendars" }, { event: "*", schema: "public", table: "calendars" },
(payload) => { async (payload) => {
switch (payload.eventType) { switch (payload.eventType) {
case "INSERT": case "INSERT":
handleInsertedCalendar(payload.new as Calendar) handleInsertedCalendar(payload.new as Calendar)
@@ -76,6 +73,13 @@ onMounted(() => {
handleDeletedCalendar(payload.old.id) handleDeletedCalendar(payload.old.id)
break break
// Maybe this case could be handled better than doing a separate API call
case "UPDATE":
if (!world.value?.data) return
world.value.data = (await $fetch<{ data: World }>("/api/worlds/query", { query: { id, full: true } })).data
break
default: default:
console.log("Unknown event has been triggered. This should not happen unless Supabase added one somehow.") console.log("Unknown event has been triggered. This should not happen unless Supabase added one somehow.")
break break
@@ -117,14 +121,28 @@ onUnmounted(() => {
}) })
const markedCalendar = ref<Calendar | null>(null) const markedCalendar = ref<Calendar | null>(null)
const isDeleteCalendarModalOpen = ref<boolean>(false)
const isEditWorldModalOpen = ref<boolean>(false) const isEditWorldModalOpen = ref<boolean>(false)
const isCreateCalendarModalOpen = ref<boolean>(false)
const isUpdateCalendarModalOpen = ref<boolean>(false)
const isDeleteCalendarModalOpen = ref<boolean>(false)
function hideCreateDialog() {
isCreateCalendarModalOpen.value = false
}
function deployUpdateDialog(calendar: Calendar) {
markedCalendar.value = calendar
isUpdateCalendarModalOpen.value = true
}
function hideUpdateDialog() {
isUpdateCalendarModalOpen.value = false
}
function deployDeleteCalendarModal(calendar: Calendar) { function deployDeleteCalendarModal(calendar: Calendar) {
isDeleteCalendarModalOpen.value = true isDeleteCalendarModalOpen.value = true
markedCalendar.value = calendar markedCalendar.value = calendar
} }
function hideDeleteCalendarModal() { function hideDeleteCalendarModal() {
isDeleteCalendarModalOpen.value = false isDeleteCalendarModalOpen.value = false
markedCalendar.value = null markedCalendar.value = null
@@ -133,7 +151,6 @@ function hideDeleteCalendarModal() {
function deployEditModal() { function deployEditModal() {
isEditWorldModalOpen.value = true isEditWorldModalOpen.value = true
} }
function hideEditModal() { function hideEditModal() {
isEditWorldModalOpen.value = false isEditWorldModalOpen.value = false
} }
@@ -190,7 +207,12 @@ function hideEditModal() {
<ul class="grid md:grid-cols-3 gap-2"> <ul class="grid md:grid-cols-3 gap-2">
<li v-for="calendar in sortedCalendars" :key="calendar.id"> <li v-for="calendar in sortedCalendars" :key="calendar.id">
<CalendarPreviewCard :calendar="calendar" :gm-id="world.data.gmId" show-actions @on-delete="() => deployDeleteCalendarModal(calendar)" /> <CalendarPreviewCard
:calendar="calendar"
:gm-id="world.data.gmId"
show-actions
@on-edit="() => deployUpdateDialog(calendar)"
@on-delete="() => deployDeleteCalendarModal(calendar)" />
</li> </li>
<li class="md:w-fit"> <li class="md:w-fit">
@@ -209,6 +231,7 @@ function hideEditModal() {
<WorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" /> <WorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" />
<CalendarDialogCreate :world="world.data" :modal-state="isCreateCalendarModalOpen" @on-close="hideCreateDialog" /> <CalendarDialogCreate :world="world.data" :modal-state="isCreateCalendarModalOpen" @on-close="hideCreateDialog" />
<CalendarDialogUpdate v-if="markedCalendar?.id" :world="world.data" :calendar="markedCalendar" :modal-state="isUpdateCalendarModalOpen" @on-close="hideUpdateDialog" />
<CalendarDialogDelete :calendar="markedCalendar" :modal-state="isDeleteCalendarModalOpen" @on-close="hideDeleteCalendarModal"/> <CalendarDialogDelete :calendar="markedCalendar" :modal-state="isDeleteCalendarModalOpen" @on-close="hideDeleteCalendarModal"/>
</template> </template>
<template v-else> <template v-else>

View File

@@ -0,0 +1,81 @@
import { z } from "zod"
import { serverSupabaseClient } from "#supabase/server"
import type { Calendar} from "~/models/CalendarConfig";
import { postCalendarSchema } from "~/models/CalendarConfig"
const paramsSchema = z.object({
id: z.number({ coerce: true }).positive().int()
})
export default defineEventHandler(async (event) => {
const client = await serverSupabaseClient(event)
const { data: params, error: paramsError} = await getValidatedRouterParams(event, paramsSchema.safeParse)
const { data: bodyData, error: bodyError } = await readValidatedBody(event, body => postCalendarSchema.safeParse(body))
if (paramsError) {
throw createError({
cause: "Utilisateur",
fatal: false,
message: "L'identifiant du calendrier est manquant ou mal renseigné.",
status: 401,
})
}
if (bodyError) {
console.log(bodyData)
console.log(bodyError)
const error = createError({
cause: "Utilisateur",
fatal: false,
statusCode: 401,
statusMessage: "Validation Error",
message: "Erreur de validation du schéma, probablement dûe à une erreur utilisateur.",
data: {
errors: bodyError.issues.map(issue => ({
path: issue.path,
message: issue.message,
code: issue.code
}))
}
})
throw error
}
try {
const { data, error } = await client
.from("calendars")
.update(
{
name: bodyData.name,
world_id: bodyData.worldId,
today: bodyData.today,
state: bodyData.state,
color: bodyData.color,
} as never
)
.eq("id", params.id)
.select(`
id,
name,
color,
state,
createdAt:created_at,
updatedAt:updated_at
`)
.single<Calendar>()
if (error) throw error
return data
} catch (err) {
console.log(err)
throw createError({
cause: "Serveur",
status: 500,
fatal: false,
message: "Une erreur inconnue est survenue."
})
}
})

View File

@@ -29,6 +29,7 @@ export default defineEventHandler(async (event) => {
name: bodyData.name, name: bodyData.name,
today: bodyData.today, today: bodyData.today,
color: bodyData.color, color: bodyData.color,
state: bodyData.state,
world_id: bodyData.worldId world_id: bodyData.worldId
} as never } as never
) )

View File

@@ -19,9 +19,15 @@ export default defineEventHandler(async (event) => {
today, today,
months:calendar_months (*), months:calendar_months (*),
state, state,
color,
createdAt:created_at, createdAt:created_at,
updatedAt:updated_at, updatedAt:updated_at,
eventNb:calendar_events(count) eventNb:calendar_events(count),
world:worlds (
id,
name,
gmId:gm_id
)
` `
const fullFields = ` const fullFields = `
@@ -31,6 +37,7 @@ export default defineEventHandler(async (event) => {
today, today,
months:calendar_months (*), months:calendar_months (*),
state, state,
color,
createdAt:created_at, createdAt:created_at,
updatedAt:updated_at, updatedAt:updated_at,
events:calendar_events ( events:calendar_events (
@@ -48,6 +55,7 @@ export default defineEventHandler(async (event) => {
eventNb:calendar_events(count), eventNb:calendar_events(count),
world:worlds ( world:worlds (
id, id,
name,
gmId:gm_id gmId:gm_id
) )
` `

View File

@@ -58,7 +58,9 @@ export default defineEventHandler(async (event) => {
name, name,
description, description,
color, color,
gm_id, createdAt:created_at,
updatedAt:updated_at,
gmId:gm_id,
state state
`) `)
.single<World>() .single<World>()

View File

@@ -33,6 +33,8 @@ export default defineEventHandler(async (event) => {
name, name,
description, description,
color, color,
createdAt:created_at,
updatedAt:updated_at,
gmId:gm_id gmId:gm_id
`) `)
.single<World>() .single<World>()

View File

@@ -17,6 +17,7 @@ export default defineEventHandler(async (event) => {
name, name,
description, description,
color, color,
state,
createdAt:created_at, createdAt:created_at,
updatedAt:updated_at, updatedAt:updated_at,
gmId:gm_id, gmId:gm_id,
@@ -25,6 +26,8 @@ export default defineEventHandler(async (event) => {
name, name,
color, color,
today, today,
state,
months:calendar_months(*),
createdAt:created_at, createdAt:created_at,
updatedAt:updated_at, updatedAt:updated_at,
eventNb:calendar_events(count) eventNb:calendar_events(count)
@@ -35,11 +38,13 @@ export default defineEventHandler(async (event) => {
name, name,
description, description,
color, color,
state,
createdAt:created_at, createdAt:created_at,
updatedAt:updated_at, updatedAt:updated_at,
calendars ( calendars (
createdAt:created_at, createdAt:created_at,
updatedAt:updated_at, updatedAt:updated_at,
months:calendar_months(*),
eventNb:calendar_events(count) eventNb:calendar_events(count)
) )
` `

View File

@@ -214,8 +214,9 @@ create policy "Allow individual update access" on public.users for update using
create policy "Allow individual read access" on public.user_roles for select using ( auth.uid() = user_id ); create policy "Allow individual read access" on public.user_roles for select using ( auth.uid() = user_id );
-- World policies -- World policies
create policy "Allow anonymous access to published worlds" on public.worlds create policy "Allow public access to published worlds" on public.worlds
for select for select
to authenticated, anon
using (state = 'published'); 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 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 create worlds" on public.worlds for insert with check ( auth.uid() = gm_id );
@@ -223,44 +224,60 @@ create policy "Allow GMs to edit their worlds" on public.worlds for update using
create policy "Allow GMs to delete their worlds" on public.worlds for delete 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 -- Calendar policies
create policy "Allow anonymous access to published calendars" on public.calendars create policy "Allow public access to published calendars" on public.calendars
for select for select
using (state = 'published'); to authenticated, anon
using (
exists (
select 1
from public.worlds
where worlds.id = calendars.world_id
and worlds.state = 'published'
and calendars.state = 'published'
)
);
create policy "Allow GMs to see their calendars" on public.calendars for select using ( create policy "Allow GMs to see their calendars" on public.calendars for select using (
exists ( exists (
select 1 from worlds select 1 from worlds
where worlds.id = calendars.world_id where worlds.id = calendars.world_id
and worlds.gm_id = auth.uid()
) )
); );
create policy "Allow GMs to add calendars to their worldd" on public.calendars for insert with check ( create policy "Allow GMs to add calendars to their world" on public.calendars for insert with check (
exists ( exists (
select 1 from worlds select 1 from worlds
where worlds.id = calendars.world_id where worlds.id = calendars.world_id
and worlds.gm_id = auth.uid()
) )
); );
create policy "Allow GMs to edit their calendars" on public.calendars for update with check ( create policy "Allow GMs to edit their calendars" on public.calendars for update using (
exists ( exists (
select 1 from worlds select 1 from worlds
where worlds.id = calendars.world_id where worlds.id = calendars.world_id
and worlds.gm_id = auth.uid()
) )
); );
create policy "Allow GMs to delete their calendars" on public.calendars for delete using ( create policy "Allow GMs to delete their calendars" on public.calendars for delete using (
exists ( exists (
select 1 from worlds select 1 from worlds
where worlds.id = calendars.world_id where worlds.id = calendars.world_id
and worlds.gm_id = auth.uid()
) )
); );
-- Month policies -- Month policies
create policy "Allow anonymous access to months in published calendars" ON public.calendar_months create policy "Allow anonymous access to months in published calendars" on public.calendar_months
for select for select
to authenticated, anon
using ( using (
exists ( exists (
select 1 select 1
from public.calendars from public.calendars c
where calendars.id = calendar_months.calendar_id join public.worlds w on w.id = c.world_id
and calendars.state = 'published' where c.id = calendar_months.calendar_id
and c.state = 'published'
and w.state = 'published'
) )
); );
@@ -318,8 +335,9 @@ create policy "Allow GMs to delete their calendar's months"
); );
-- Event policies -- Event policies
create policy "Allow anonymous access to non-hidden events in published calendars" ON public.calendar_events create policy "Allow anonymous access to non-hidden events in published calendars" on public.calendar_events
for select for select
to authenticated, anon
using ( using (
not hidden and exists ( not hidden and exists (
select 1 select 1