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>
|
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
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 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" />
|
||||||
|
|||||||
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>
|
<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')"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
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"
|
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,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -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 !",
|
||||||
|
|||||||
@@ -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
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 { 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(),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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,
|
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user