Merge pull request #69 from AlexisNP/features/draft-published-state
Features/draft published state
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<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 type { Calendar } from "~/models/CalendarConfig";
|
||||
|
||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
showActions?: boolean,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["on-delete"])
|
||||
const emit = defineEmits(["on-edit", "on-delete"])
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
@@ -23,38 +23,76 @@ const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calen
|
||||
|
||||
<template>
|
||||
<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"
|
||||
: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>
|
||||
|
||||
<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>
|
||||
|
||||
<UiCardContent>
|
||||
<UiCardContent class="grow">
|
||||
<p class="flex items-center gap-1">
|
||||
<PhCalendarDots size="24" weight="fill" />
|
||||
<span>{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span>
|
||||
</p>
|
||||
|
||||
<UiButton
|
||||
<div
|
||||
v-if="isOwner && showActions"
|
||||
size="icon"
|
||||
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')"
|
||||
class="flex gap-1 absolute top-4 right-4 z-20"
|
||||
>
|
||||
<PhTrash size="16" />
|
||||
</UiButton>
|
||||
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="emit('on-edit')">
|
||||
<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>
|
||||
|
||||
<UiCardFooter>
|
||||
<ul class="grid gap-2">
|
||||
<ul class="grid gap-1 text-sm">
|
||||
<li class="flex gap-1 items-center">
|
||||
<PhFilePlus size="20" />
|
||||
<PhFilePlus size="18" />
|
||||
<span>{{ $t('common.createdAt', { createdAt }) }}</span>
|
||||
</li>
|
||||
<li v-if="updatedAt" class="flex gap-1 items-center">
|
||||
<PhPencilSimpleLine size="20" />
|
||||
<PhPencilSimpleLine size="18" />
|
||||
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -17,6 +17,7 @@ const emit = defineEmits(["on-close"])
|
||||
|
||||
function handleClose() {
|
||||
emit("on-close")
|
||||
setTimeout(() => calendarSkeletonName.value = "", 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
49
components/calendar/dialog/Update.vue
Normal file
49
components/calendar/dialog/Update.vue
Normal 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>
|
||||
@@ -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: [], state: "draft" }
|
||||
const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: [], state: "draft", color: "white" }
|
||||
const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton })
|
||||
|
||||
onMounted(() => {
|
||||
@@ -79,25 +79,25 @@ function handleFormCancel() {
|
||||
<div class="flex items-center gap-1">
|
||||
<PhWrench size="18" weight="fill" />
|
||||
|
||||
{{ $t('entity.calendar.createDialog.tabs.general.title') }}
|
||||
{{ $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.createDialog.tabs.months.title') }}
|
||||
{{ $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.createDialog.tabs.today.title') }}
|
||||
{{ $t('entity.calendar.actionDialog.tabs.today.title') }}
|
||||
</div>
|
||||
</UiTabsTrigger>
|
||||
</UiTabsList>
|
||||
<UiTabsContent value="global">
|
||||
<UiTabsContent value="global" class="grid gap-4">
|
||||
<input
|
||||
id="new-calendar-name"
|
||||
v-model="calendarSkeleton.name"
|
||||
@@ -105,9 +105,25 @@ function handleFormCancel() {
|
||||
name="new-calendar-name"
|
||||
required
|
||||
: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"
|
||||
>
|
||||
|
||||
<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" />
|
||||
|
||||
147
components/calendar/form/Update.vue
Normal file
147
components/calendar/form/Update.vue
Normal 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>
|
||||
@@ -6,7 +6,7 @@ const emit = defineEmits(["on-click"])
|
||||
|
||||
<template>
|
||||
<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
|
||||
@on-click="emit('on-click')"
|
||||
>
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type RPGColor, rpgColors } from "~/models/Color";
|
||||
|
||||
defineProps<{
|
||||
id: string
|
||||
}>();
|
||||
|
||||
const model = defineModel<RPGColor>({ default: "white" });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSelect v-model="model">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectTrigger :id>
|
||||
<UiSelectValue
|
||||
:placeholder="$t('ui.colors.selectOne')"
|
||||
class="input-color"
|
||||
|
||||
30
components/global/input/ContentState.vue
Normal file
30
components/global/input/ContentState.vue
Normal 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>
|
||||
@@ -17,7 +17,7 @@ const delegatedProps = computed(() => {
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
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,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 { DateTime } from "luxon";
|
||||
|
||||
@@ -16,7 +16,7 @@ const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromIS
|
||||
|
||||
<template>
|
||||
<UiCard
|
||||
class="w-full h-full transition-all"
|
||||
class="w-full h-full flex flex-col transition-all"
|
||||
:link="`/my/worlds/${world.id}`"
|
||||
:class="{
|
||||
'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-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',
|
||||
' 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>
|
||||
<UiCardTitle>{{ world.name }}</UiCardTitle>
|
||||
<UiCardHeader class="gap-4">
|
||||
<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>
|
||||
<UiCardContent>
|
||||
|
||||
<UiCardContent class="grow">
|
||||
<p class="italic">{{ world.description }}</p>
|
||||
|
||||
<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>
|
||||
|
||||
<UiCardFooter>
|
||||
<ul class="grid gap-2">
|
||||
<ul class="grid gap-1 text-sm">
|
||||
<li class="flex gap-1 items-center">
|
||||
<PhFilePlus size="20" />
|
||||
<PhFilePlus size="18" />
|
||||
<span>{{ $t('common.createdAt', { createdAt }) }}</span>
|
||||
</li>
|
||||
<li v-if="updatedAt" class="flex gap-1 items-center">
|
||||
<PhPencilSimpleLine size="20" />
|
||||
<PhPencilSimpleLine size="18" />
|
||||
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -48,7 +48,7 @@ function handleClose() {
|
||||
<PhX size="20" />
|
||||
</UiButton>
|
||||
|
||||
<WorldFormEdit :world @on-changed-name="onChangedName" @on-close="handleClose" />
|
||||
<WorldFormUpdate :world @on-changed-name="onChangedName" @on-close="handleClose" />
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
|
||||
@@ -57,7 +57,7 @@ function handleFormCancel() {
|
||||
<template>
|
||||
<template v-if="worldSkeleton">
|
||||
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<div class="grid gap-4">
|
||||
<input
|
||||
id="new-world-name"
|
||||
v-model="worldSkeleton.name"
|
||||
@@ -69,10 +69,6 @@ function handleFormCancel() {
|
||||
@input="handleNameChange"
|
||||
>
|
||||
|
||||
<div class="-mx-1 mb-4">
|
||||
<InputColor v-model="worldSkeleton.color" />
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id="new-world-description"
|
||||
v-model="worldSkeleton.description"
|
||||
@@ -80,6 +76,22 @@ function handleFormCancel() {
|
||||
: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"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<footer class="flex justify-end gap-2 mt-6">
|
||||
|
||||
@@ -71,7 +71,7 @@ function handleFormCancel() {
|
||||
<template>
|
||||
<template v-if="worldSkeleton">
|
||||
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<div class="grid gap-4">
|
||||
<input
|
||||
id="new-world-name"
|
||||
v-model="worldSkeleton.name"
|
||||
@@ -79,14 +79,10 @@ function handleFormCancel() {
|
||||
name="new-world-name"
|
||||
required
|
||||
: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"
|
||||
>
|
||||
|
||||
<div class="-mx-1 mb-4">
|
||||
<InputColor v-model="worldSkeleton.color" />
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id="new-world-description"
|
||||
v-model="worldSkeleton.description"
|
||||
@@ -94,6 +90,22 @@ function handleFormCancel() {
|
||||
: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"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<footer class="flex justify-end gap-2 mt-6">
|
||||
@@ -17,6 +17,7 @@ export default defineI18nConfig(() => ({
|
||||
edit: "Edit",
|
||||
},
|
||||
colors: {
|
||||
label: "Color",
|
||||
selectOne: "Select a color",
|
||||
red: "Red",
|
||||
orange: "Orange",
|
||||
@@ -38,6 +39,13 @@ export default defineI18nConfig(() => ({
|
||||
black: "Black",
|
||||
white: "White",
|
||||
},
|
||||
contentState: {
|
||||
label: "Status",
|
||||
selectOne: "Select a publication state",
|
||||
published: "Published",
|
||||
draft: "Draft",
|
||||
archived: "Archived",
|
||||
},
|
||||
greeting: "Connected as {user}",
|
||||
anonymousGreeting: "Preferences",
|
||||
backToProfile: "Back to profile",
|
||||
@@ -189,8 +197,7 @@ export default defineI18nConfig(() => ({
|
||||
title: "The event \"{event}\" has been successfuly deleted.",
|
||||
},
|
||||
},
|
||||
createDialog: {
|
||||
title: "Create a calendar",
|
||||
actionDialog: {
|
||||
tabs: {
|
||||
general: {
|
||||
title: "General",
|
||||
@@ -203,6 +210,9 @@ export default defineI18nConfig(() => ({
|
||||
},
|
||||
}
|
||||
},
|
||||
createDialog: {
|
||||
title: "Create a calendar",
|
||||
},
|
||||
deleteDialog: {
|
||||
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 !",
|
||||
@@ -272,6 +282,7 @@ export default defineI18nConfig(() => ({
|
||||
edit: "Modifier",
|
||||
},
|
||||
colors: {
|
||||
label: "Couleur",
|
||||
selectOne: "Sélectionner une couleur",
|
||||
red: "Rouge",
|
||||
orange: "Orange",
|
||||
@@ -293,6 +304,13 @@ export default defineI18nConfig(() => ({
|
||||
black: "Noir",
|
||||
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}",
|
||||
anonymousGreeting: "Préférences",
|
||||
backToProfile: "Retour au profil",
|
||||
@@ -444,8 +462,7 @@ export default defineI18nConfig(() => ({
|
||||
title: "L'évènement \"{event}\" a été supprimé avec succès.",
|
||||
},
|
||||
},
|
||||
createDialog: {
|
||||
title: "Créer un calendrier",
|
||||
actionDialog: {
|
||||
tabs: {
|
||||
general: {
|
||||
title: "Général",
|
||||
@@ -458,6 +475,12 @@ export default defineI18nConfig(() => ({
|
||||
},
|
||||
}
|
||||
},
|
||||
createDialog: {
|
||||
title: "Créer un calendrier",
|
||||
},
|
||||
updateDialog: {
|
||||
title: "Modifier le calendrier",
|
||||
},
|
||||
deleteDialog: {
|
||||
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 !",
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { CalendarEvent } from "./CalendarEvent"
|
||||
import { calendarMonthSchema, type CalendarMonth } from "./CalendarMonth"
|
||||
import { dateSchema, type RPGDate } from "./Date"
|
||||
import type { World } from "./World"
|
||||
import type { ContentState } from "./Entity"
|
||||
import type { RPGColor } from "./Color"
|
||||
|
||||
export type CalendarState = "published" | "draft" | "archived"
|
||||
|
||||
export interface CalendarConfig {
|
||||
months: CalendarMonth[]
|
||||
@@ -17,17 +18,21 @@ export interface Calendar extends CalendarConfig {
|
||||
name: string
|
||||
events: CalendarEvent[]
|
||||
eventNb?: Array<{ count: number }>
|
||||
state: CalendarState
|
||||
color?: string
|
||||
state: ContentState
|
||||
color?: RPGColor
|
||||
world?: World
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type CalendarChannelPayload = Calendar & Record<string, any>
|
||||
|
||||
export const postCalendarSchema = z.object({
|
||||
name: z.string(),
|
||||
today: dateSchema.optional().nullable(),
|
||||
color: z.string().optional().nullable(),
|
||||
months: z.array(calendarMonthSchema).min(1),
|
||||
worldId: z.number().int(),
|
||||
state: z.string().optional().nullable().default("draft"),
|
||||
})
|
||||
|
||||
2
models/Entity.ts
Normal file
2
models/Entity.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const contentStates = ["published", "draft", "archived"] as const
|
||||
export type ContentState = typeof contentStates[number]
|
||||
@@ -1,8 +1,7 @@
|
||||
import { z } from "zod"
|
||||
import type { Calendar } from "./CalendarConfig"
|
||||
import { rpgColorSchema, type RPGColor } from "./Color"
|
||||
|
||||
export type WorldState = "published" | "draft" | "archived"
|
||||
import type { ContentState } from "./Entity"
|
||||
|
||||
export interface World {
|
||||
id?: number
|
||||
@@ -11,11 +10,14 @@ export interface World {
|
||||
color?: RPGColor
|
||||
calendars?: Calendar[]
|
||||
gmId?: string
|
||||
state?: WorldState
|
||||
state?: ContentState
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WorldChannelPayload = World & Record<string, any>
|
||||
|
||||
export const postWorldSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional().nullable(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RealtimeChannel } from "@supabase/supabase-js"
|
||||
import type { World } from "~/models/World";
|
||||
import type { World, WorldChannelPayload } from "~/models/World";
|
||||
|
||||
const supabase = useSupabaseClient()
|
||||
const user = useSupabaseUser()
|
||||
@@ -31,10 +31,14 @@ function hideCreateDialog() {
|
||||
/** Active world channel */
|
||||
let worldChannel: RealtimeChannel
|
||||
|
||||
|
||||
/** Handles world insertion realtime events */
|
||||
function handleInsertedWorld(newWorld: World) {
|
||||
function handleInsertedWorld(newWorld: WorldChannelPayload) {
|
||||
if (!worlds.value?.data) return
|
||||
|
||||
newWorld.createdAt = newWorld.created_at;
|
||||
newWorld.gmId = newWorld.gm_id;
|
||||
|
||||
try {
|
||||
worlds.value?.data.push(newWorld)
|
||||
} catch (err) {
|
||||
@@ -71,7 +75,7 @@ onMounted(() => {
|
||||
case "UPDATE":
|
||||
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
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RealtimeChannel } from "@supabase/supabase-js"
|
||||
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";
|
||||
|
||||
const supabase = useSupabaseClient()
|
||||
@@ -24,12 +24,6 @@ watch(user, (n) => {
|
||||
}
|
||||
})
|
||||
|
||||
const isCreateCalendarModalOpen = ref<boolean>(false)
|
||||
|
||||
function hideCreateDialog() {
|
||||
isCreateCalendarModalOpen.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* === Subscriptions ===
|
||||
*/
|
||||
@@ -40,9 +34,12 @@ let calendarChannel: RealtimeChannel
|
||||
let worldChannel: RealtimeChannel
|
||||
|
||||
/** Handles calendar insertion realtime events */
|
||||
function handleInsertedCalendar(newCalendar: Calendar) {
|
||||
function handleInsertedCalendar(newCalendar: CalendarChannelPayload) {
|
||||
if (!world.value) return
|
||||
|
||||
newCalendar.createdAt = newCalendar.created_at;
|
||||
newCalendar.eventNb = [{ count: 0 }];
|
||||
|
||||
try {
|
||||
world.value.data.calendars?.push(newCalendar)
|
||||
} catch (err) {
|
||||
@@ -66,7 +63,7 @@ onMounted(() => {
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{ event: "*", schema: "public", table: "calendars" },
|
||||
(payload) => {
|
||||
async (payload) => {
|
||||
switch (payload.eventType) {
|
||||
case "INSERT":
|
||||
handleInsertedCalendar(payload.new as Calendar)
|
||||
@@ -76,6 +73,13 @@ onMounted(() => {
|
||||
handleDeletedCalendar(payload.old.id)
|
||||
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:
|
||||
console.log("Unknown event has been triggered. This should not happen unless Supabase added one somehow.")
|
||||
break
|
||||
@@ -117,14 +121,28 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
const markedCalendar = ref<Calendar | null>(null)
|
||||
const isDeleteCalendarModalOpen = 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) {
|
||||
isDeleteCalendarModalOpen.value = true
|
||||
markedCalendar.value = calendar
|
||||
}
|
||||
|
||||
function hideDeleteCalendarModal() {
|
||||
isDeleteCalendarModalOpen.value = false
|
||||
markedCalendar.value = null
|
||||
@@ -133,7 +151,6 @@ function hideDeleteCalendarModal() {
|
||||
function deployEditModal() {
|
||||
isEditWorldModalOpen.value = true
|
||||
}
|
||||
|
||||
function hideEditModal() {
|
||||
isEditWorldModalOpen.value = false
|
||||
}
|
||||
@@ -190,7 +207,12 @@ function hideEditModal() {
|
||||
|
||||
<ul class="grid md:grid-cols-3 gap-2">
|
||||
<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 class="md:w-fit">
|
||||
@@ -209,6 +231,7 @@ function hideEditModal() {
|
||||
|
||||
<WorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" />
|
||||
<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"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
81
server/api/calendars/[id].patch.ts
Normal file
81
server/api/calendars/[id].patch.ts
Normal 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."
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -29,6 +29,7 @@ export default defineEventHandler(async (event) => {
|
||||
name: bodyData.name,
|
||||
today: bodyData.today,
|
||||
color: bodyData.color,
|
||||
state: bodyData.state,
|
||||
world_id: bodyData.worldId
|
||||
} as never
|
||||
)
|
||||
|
||||
@@ -19,9 +19,15 @@ export default defineEventHandler(async (event) => {
|
||||
today,
|
||||
months:calendar_months (*),
|
||||
state,
|
||||
color,
|
||||
createdAt:created_at,
|
||||
updatedAt:updated_at,
|
||||
eventNb:calendar_events(count)
|
||||
eventNb:calendar_events(count),
|
||||
world:worlds (
|
||||
id,
|
||||
name,
|
||||
gmId:gm_id
|
||||
)
|
||||
`
|
||||
|
||||
const fullFields = `
|
||||
@@ -31,6 +37,7 @@ export default defineEventHandler(async (event) => {
|
||||
today,
|
||||
months:calendar_months (*),
|
||||
state,
|
||||
color,
|
||||
createdAt:created_at,
|
||||
updatedAt:updated_at,
|
||||
events:calendar_events (
|
||||
@@ -48,6 +55,7 @@ export default defineEventHandler(async (event) => {
|
||||
eventNb:calendar_events(count),
|
||||
world:worlds (
|
||||
id,
|
||||
name,
|
||||
gmId:gm_id
|
||||
)
|
||||
`
|
||||
|
||||
@@ -58,7 +58,9 @@ export default defineEventHandler(async (event) => {
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
gm_id,
|
||||
createdAt:created_at,
|
||||
updatedAt:updated_at,
|
||||
gmId:gm_id,
|
||||
state
|
||||
`)
|
||||
.single<World>()
|
||||
|
||||
@@ -33,6 +33,8 @@ export default defineEventHandler(async (event) => {
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
createdAt:created_at,
|
||||
updatedAt:updated_at,
|
||||
gmId:gm_id
|
||||
`)
|
||||
.single<World>()
|
||||
|
||||
@@ -17,6 +17,7 @@ export default defineEventHandler(async (event) => {
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
state,
|
||||
createdAt:created_at,
|
||||
updatedAt:updated_at,
|
||||
gmId:gm_id,
|
||||
@@ -25,6 +26,8 @@ export default defineEventHandler(async (event) => {
|
||||
name,
|
||||
color,
|
||||
today,
|
||||
state,
|
||||
months:calendar_months(*),
|
||||
createdAt:created_at,
|
||||
updatedAt:updated_at,
|
||||
eventNb:calendar_events(count)
|
||||
@@ -35,11 +38,13 @@ export default defineEventHandler(async (event) => {
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
state,
|
||||
createdAt:created_at,
|
||||
updatedAt:updated_at,
|
||||
calendars (
|
||||
createdAt:created_at,
|
||||
updatedAt:updated_at,
|
||||
months:calendar_months(*),
|
||||
eventNb:calendar_events(count)
|
||||
)
|
||||
`
|
||||
|
||||
@@ -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 );
|
||||
|
||||
-- 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
|
||||
to authenticated, anon
|
||||
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 );
|
||||
@@ -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 );
|
||||
|
||||
-- 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
|
||||
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 (
|
||||
exists (
|
||||
select 1 from worlds
|
||||
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 (
|
||||
select 1 from worlds
|
||||
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 (
|
||||
select 1 from worlds
|
||||
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 (
|
||||
exists (
|
||||
select 1 from worlds
|
||||
where worlds.id = calendars.world_id
|
||||
and worlds.gm_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- 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
|
||||
to authenticated, anon
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.calendars
|
||||
where calendars.id = calendar_months.calendar_id
|
||||
and calendars.state = 'published'
|
||||
from public.calendars c
|
||||
join public.worlds w on w.id = c.world_id
|
||||
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
|
||||
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
|
||||
to authenticated, anon
|
||||
using (
|
||||
not hidden and exists (
|
||||
select 1
|
||||
|
||||
Reference in New Issue
Block a user