Merge pull request #74 from AlexisNP/features/category-management
Features/category management
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
--primary: 245 58% 51%;
|
--primary: 245 58% 51%;
|
||||||
--primary-foreground: 0 0% 100%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 50% 95%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 210 40% 96.1%;
|
||||||
@@ -338,6 +338,81 @@
|
|||||||
--border-color: color-mix(in srgb, var(--base-color), var(--color-slate-800) 50%);
|
--border-color: color-mix(in srgb, var(--base-color), var(--color-slate-800) 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bgc {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgc::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: .75rem;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
border-radius: .25rem;
|
||||||
|
background-color: red;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgc-red::before {
|
||||||
|
@apply bg-red-500;
|
||||||
|
}
|
||||||
|
.bgc-orange::before {
|
||||||
|
@apply bg-orange-500;
|
||||||
|
}
|
||||||
|
.bgc-amber::before {
|
||||||
|
@apply bg-amber-500;
|
||||||
|
}
|
||||||
|
.bgc-yellow::before {
|
||||||
|
@apply bg-yellow-500;
|
||||||
|
}
|
||||||
|
.bgc-lime::before {
|
||||||
|
@apply bg-lime-500;
|
||||||
|
}
|
||||||
|
.bgc-green::before {
|
||||||
|
@apply bg-green-500;
|
||||||
|
}
|
||||||
|
.bgc-emerald::before {
|
||||||
|
@apply bg-emerald-600;
|
||||||
|
}
|
||||||
|
.bgc-teal::before {
|
||||||
|
@apply bg-teal-600;
|
||||||
|
}
|
||||||
|
.bgc-cyan::before {
|
||||||
|
@apply bg-cyan-600;
|
||||||
|
}
|
||||||
|
.bgc-sky::before {
|
||||||
|
@apply bg-sky-600;
|
||||||
|
}
|
||||||
|
.bgc-blue::before {
|
||||||
|
@apply bg-blue-600;
|
||||||
|
}
|
||||||
|
.bgc-indigo::before {
|
||||||
|
@apply bg-indigo-600;
|
||||||
|
}
|
||||||
|
.bgc-violet::before {
|
||||||
|
@apply bg-violet-600;
|
||||||
|
}
|
||||||
|
.bgc-purple::before {
|
||||||
|
@apply bg-purple-600;
|
||||||
|
}
|
||||||
|
.bgc-fuchsia::before {
|
||||||
|
@apply bg-fuchsia-600;
|
||||||
|
}
|
||||||
|
.bgc-pink::before {
|
||||||
|
@apply bg-pink-600;
|
||||||
|
}
|
||||||
|
.bgc-rose::before {
|
||||||
|
@apply bg-rose-600;
|
||||||
|
}
|
||||||
|
.bgc-black::before {
|
||||||
|
@apply bg-black dark:border-[1px] dark:border-slate-300;
|
||||||
|
}
|
||||||
|
.bgc-white::before {
|
||||||
|
@apply bg-white border-[1px] border-slate-700;
|
||||||
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: all .5s ease;
|
transition: all .5s ease;
|
||||||
|
|||||||
@@ -8,17 +8,6 @@ import CenturyLayout from "./state/centennially/Layout.vue"
|
|||||||
import DecadeLayout from "./state/decennially/Layout.vue"
|
import DecadeLayout from "./state/decennially/Layout.vue"
|
||||||
import YearLayout from "./state/yearly/Layout.vue"
|
import YearLayout from "./state/yearly/Layout.vue"
|
||||||
|
|
||||||
import type { Calendar } from "~/models/CalendarConfig"
|
|
||||||
import type { Category } from "~/models/Category"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
calendarData: Calendar,
|
|
||||||
categories: Category[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { setActiveCalendar } = useCalendar()
|
|
||||||
setActiveCalendar(props.calendarData, props.categories)
|
|
||||||
|
|
||||||
const { currentConfig, jumpToDate, selectedDate } = useCalendar()
|
const { currentConfig, jumpToDate, selectedDate } = useCalendar()
|
||||||
const { isReadOnly } = storeToRefs(useCalendar())
|
const { isReadOnly } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
@@ -52,6 +41,7 @@ onMounted(() => {
|
|||||||
<component :is="currentViewComponent" />
|
<component :is="currentViewComponent" />
|
||||||
|
|
||||||
<LazyCalendarSearch />
|
<LazyCalendarSearch />
|
||||||
|
<LazyCalendarDialogCategories v-if="!isReadOnly" />
|
||||||
<LazyCalendarDialogUpdateEvent v-if="!isReadOnly" />
|
<LazyCalendarDialogUpdateEvent v-if="!isReadOnly" />
|
||||||
<LazyCalendarDialogDeleteEvent v-if="!isReadOnly" />
|
<LazyCalendarDialogDeleteEvent v-if="!isReadOnly" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { computed, type ComputedRef } from "vue"
|
|
||||||
import { useCalendar } from "@/stores/CalendarStore"
|
|
||||||
|
|
||||||
import {
|
|
||||||
PhCaretDoubleLeft,
|
|
||||||
PhCaretDoubleRight,
|
|
||||||
PhCaretLeft,
|
|
||||||
PhCaretRight
|
|
||||||
} from "@phosphor-icons/vue"
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
interface DirectionLabels {
|
|
||||||
pastFar: string
|
|
||||||
pastNear: string
|
|
||||||
futureNear: string
|
|
||||||
futureFar: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentConfig, decrementViewMonth, incrementViewMonth, decrementViewYear, incrementViewYear } =
|
|
||||||
useCalendar()
|
|
||||||
|
|
||||||
const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
|
|
||||||
switch (currentConfig.viewType) {
|
|
||||||
case "month":
|
|
||||||
return {
|
|
||||||
pastFar: t("entity.calendar.years.prevSingular"),
|
|
||||||
pastNear: t("entity.calendar.months.prevSingular"),
|
|
||||||
futureNear: t("entity.calendar.months.nextSingular"),
|
|
||||||
futureFar: t("entity.calendar.years.nextSingular")
|
|
||||||
}
|
|
||||||
|
|
||||||
case "year":
|
|
||||||
return {
|
|
||||||
pastFar: t("entity.calendar.decades.prevSingular"),
|
|
||||||
pastNear: t("entity.calendar.years.prevSingular"),
|
|
||||||
futureNear: t("entity.calendar.years.nextSingular"),
|
|
||||||
futureFar: t("entity.calendar.decades.nextSingular")
|
|
||||||
}
|
|
||||||
|
|
||||||
case "decade":
|
|
||||||
return {
|
|
||||||
pastFar: t("entity.calendar.centuries.prevSingular"),
|
|
||||||
pastNear: t("entity.calendar.decades.prevSingular"),
|
|
||||||
futureNear: t("entity.calendar.decades.nextSingular"),
|
|
||||||
futureFar: t("entity.calendar.centuries.nextSingular")
|
|
||||||
}
|
|
||||||
|
|
||||||
case "century":
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
pastFar: t("entity.calendar.millenias.prevSingular"),
|
|
||||||
pastNear: t("entity.calendar.centuries.prevSingular"),
|
|
||||||
futureNear: t("entity.calendar.centuries.nextSingular"),
|
|
||||||
futureFar: t("entity.calendar.millenias.nextSingular")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function toPastFar(): void {
|
|
||||||
switch (currentConfig.viewType) {
|
|
||||||
case "month":
|
|
||||||
decrementViewYear()
|
|
||||||
break
|
|
||||||
|
|
||||||
case "year":
|
|
||||||
decrementViewYear(10)
|
|
||||||
break
|
|
||||||
|
|
||||||
case "decade":
|
|
||||||
decrementViewYear(100)
|
|
||||||
break
|
|
||||||
|
|
||||||
case "century":
|
|
||||||
default:
|
|
||||||
decrementViewYear(1000)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toPastNear(): void {
|
|
||||||
switch (currentConfig.viewType) {
|
|
||||||
case "month":
|
|
||||||
decrementViewMonth()
|
|
||||||
break
|
|
||||||
|
|
||||||
case "year":
|
|
||||||
decrementViewYear()
|
|
||||||
break
|
|
||||||
|
|
||||||
case "decade":
|
|
||||||
decrementViewYear(10)
|
|
||||||
break
|
|
||||||
|
|
||||||
case "century":
|
|
||||||
default:
|
|
||||||
decrementViewYear(100)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toFutureNear(): void {
|
|
||||||
switch (currentConfig.viewType) {
|
|
||||||
case "month":
|
|
||||||
incrementViewMonth()
|
|
||||||
break
|
|
||||||
|
|
||||||
case "year":
|
|
||||||
incrementViewYear()
|
|
||||||
break
|
|
||||||
|
|
||||||
case "decade":
|
|
||||||
incrementViewYear(10)
|
|
||||||
break
|
|
||||||
|
|
||||||
case "century":
|
|
||||||
default:
|
|
||||||
incrementViewYear(100)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toFutureFar(): void {
|
|
||||||
switch (currentConfig.viewType) {
|
|
||||||
case "month":
|
|
||||||
incrementViewYear()
|
|
||||||
break
|
|
||||||
|
|
||||||
case "year":
|
|
||||||
incrementViewYear(10)
|
|
||||||
break
|
|
||||||
|
|
||||||
case "decade":
|
|
||||||
incrementViewYear(100)
|
|
||||||
break
|
|
||||||
|
|
||||||
case "century":
|
|
||||||
default:
|
|
||||||
incrementViewYear(1000)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UiTooltipProvider :delay-duration="250">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton variant="outline" size="icon" @click="toPastFar()">
|
|
||||||
<PhCaretDoubleLeft size="18" />
|
|
||||||
</UiButton>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent>
|
|
||||||
<p>{{ activeDirectionLabels.pastFar }}</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
|
|
||||||
<UiTooltipProvider :delay-duration="250">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton variant="outline" size="icon" @click="toPastNear()">
|
|
||||||
<PhCaretLeft size="18" />
|
|
||||||
</UiButton>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent>
|
|
||||||
<p>{{ activeDirectionLabels.pastNear }}</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
|
|
||||||
<UiTooltipProvider :delay-duration="250">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton variant="outline" size="icon" @click="toFutureNear()">
|
|
||||||
<PhCaretRight size="18" />
|
|
||||||
</UiButton>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent>
|
|
||||||
<p>{{ activeDirectionLabels.futureNear }}</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
|
|
||||||
<UiTooltipProvider :delay-duration="250">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton variant="outline" size="icon" @click="toFutureFar()">
|
|
||||||
<PhCaretDoubleRight size="18" />
|
|
||||||
</UiButton>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent>
|
|
||||||
<p>{{ activeDirectionLabels.futureFar }}</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
const { currentDate } = useCalendar()
|
|
||||||
|
|
||||||
// const { toast } = useToast()
|
|
||||||
// const { t } = useI18n()
|
|
||||||
|
|
||||||
// function handleGotoPreviousEventPage(position: "next" | "prev" = "next") {
|
|
||||||
// let fromDate: RPGDate
|
|
||||||
|
|
||||||
// // To modify, obviously
|
|
||||||
// const daysPerMonth = 32
|
|
||||||
|
|
||||||
// const toDay = position === "next" ? daysPerMonth : 1
|
|
||||||
// const toMonth = position === "next" ? monthsPerYear : 0
|
|
||||||
|
|
||||||
// switch (currentConfig.viewType) {
|
|
||||||
// case "month":
|
|
||||||
// fromDate = {
|
|
||||||
// day: toDay,
|
|
||||||
// month: currentDate.currentMonth,
|
|
||||||
// year: currentDate.currentYear
|
|
||||||
// }
|
|
||||||
// break
|
|
||||||
|
|
||||||
// case "year":
|
|
||||||
// fromDate = {
|
|
||||||
// day: toDay,
|
|
||||||
// month: toMonth,
|
|
||||||
// year: currentDate.currentYear
|
|
||||||
// }
|
|
||||||
// break
|
|
||||||
|
|
||||||
// case "decade":
|
|
||||||
// fromDate = {
|
|
||||||
// day: toDay,
|
|
||||||
// month: currentDate.currentMonth,
|
|
||||||
// year: currentDate.currentYear
|
|
||||||
// }
|
|
||||||
// break
|
|
||||||
|
|
||||||
// case "century":
|
|
||||||
// default:
|
|
||||||
// fromDate = {
|
|
||||||
// day: toDay,
|
|
||||||
// month: currentDate.currentMonth,
|
|
||||||
// year: currentDate.currentYear
|
|
||||||
// }
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const { targetDate } = getRelativeEventFromDate(fromDate, position)
|
|
||||||
|
|
||||||
// jumpToDate(targetDate)
|
|
||||||
// } catch (err) {
|
|
||||||
// toast({
|
|
||||||
// title: t("entity.calendar.event.outOfBoundsTitle"),
|
|
||||||
// variant: "default",
|
|
||||||
// description: t("entity.calendar.event.outOfBoundsMessage"),
|
|
||||||
// duration: 4000,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<div class="grid items-end w-40 px-4 py-2 border-slate-200 bg-white dark:bg-black dark:border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm text-sm transition-colors">
|
|
||||||
<ClientOnly>
|
|
||||||
<span>{{ currentDate.currentDateTitle }}</span>
|
|
||||||
|
|
||||||
<template #fallback>
|
|
||||||
<span class="inline-block">
|
|
||||||
<UiSkeleton class="h-[19px] w-full rounded-sm" />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
<!-- <div>
|
|
||||||
<UiTooltipProvider :delay-duration="250">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
class="rounded-t-sm rounded-b-none border-b-0"
|
|
||||||
@click="handleGotoPreviousEventPage('prev')"
|
|
||||||
>
|
|
||||||
<PhArrowLineLeft size="22" />
|
|
||||||
</UiButton>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent>
|
|
||||||
<p>
|
|
||||||
{{ $t('entity.calendar.event.prevPage') }}
|
|
||||||
</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<UiTooltipProvider :delay-duration="250">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
class="rounded-t-sm rounded-b-none border-b-0"
|
|
||||||
@click="handleGotoPreviousEventPage('next')"
|
|
||||||
>
|
|
||||||
<PhArrowLineRight size="22" />
|
|
||||||
</UiButton>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent>
|
|
||||||
<p>
|
|
||||||
{{ $t('entity.calendar.event.nextPage') }}
|
|
||||||
</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useCalendar } from "@/stores/CalendarStore"
|
|
||||||
import { PhCalendarBlank } from "@phosphor-icons/vue"
|
|
||||||
import { computed } from "vue"
|
|
||||||
|
|
||||||
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
|
|
||||||
|
|
||||||
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<UiDropdownMenu>
|
|
||||||
<UiDropdownMenuTrigger as-child>
|
|
||||||
<UiButton size="sm" variant="secondary">
|
|
||||||
<PhCalendarBlank size="18" weight="fill" />
|
|
||||||
|
|
||||||
{{ viewTypeTitle }}
|
|
||||||
</UiButton>
|
|
||||||
</UiDropdownMenuTrigger>
|
|
||||||
<UiDropdownMenuContent :side="'bottom'" :collision-padding="30">
|
|
||||||
<UiDropdownMenuLabel>
|
|
||||||
{{ $t('ui.displayMode') }}
|
|
||||||
</UiDropdownMenuLabel>
|
|
||||||
<UiDropdownMenuSeparator />
|
|
||||||
<UiDropdownMenuItem
|
|
||||||
v-for="option in viewTypeOptions"
|
|
||||||
:key="option"
|
|
||||||
@click="setViewType(option)"
|
|
||||||
>
|
|
||||||
{{ getViewTypeTitle(option) }}
|
|
||||||
</UiDropdownMenuItem>
|
|
||||||
</UiDropdownMenuContent>
|
|
||||||
</UiDropdownMenu>
|
|
||||||
</template>
|
|
||||||
23
components/calendar/CategoriesCTA.vue
Normal file
23
components/calendar/CategoriesCTA.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhTag } from "@phosphor-icons/vue"
|
||||||
|
|
||||||
|
const { toggleCategoriesModal } = useCalendar()
|
||||||
|
const { isReadOnly } = storeToRefs(useCalendar())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton v-if="!isReadOnly" variant="secondary" size="icon" @click="toggleCategoriesModal(true)">
|
||||||
|
<PhTag size="20" weight="light" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('entity.calendar.seeCategories') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
</template>
|
||||||
@@ -4,26 +4,29 @@ import { computed } from "vue"
|
|||||||
|
|
||||||
import { PhMapPin } from "@phosphor-icons/vue"
|
import { PhMapPin } from "@phosphor-icons/vue"
|
||||||
|
|
||||||
const { defaultDate, getFormattedDateTitle, getRelativeString } = useCalendar()
|
const { defaultDate, getFormattedDateTitle, getRelativeString, getDifferenceInDays } = useCalendar()
|
||||||
const { selectedDate } = storeToRefs(useCalendar())
|
const { selectedDate } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
|
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
|
||||||
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
|
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
|
||||||
|
|
||||||
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
|
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
|
||||||
|
const isToday = computed(() => getDifferenceInDays(defaultDate, selectedDate.value) === 0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<h1 class="text-2xl font-bold flex items-center gap-1">
|
<div class="flex gap-2 items-center">
|
||||||
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
|
<h1 class="text-2xl font-bold flex items-center gap-1">
|
||||||
</h1>
|
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
|
||||||
<h2 class="text-lg italic opacity-75">
|
</h1>
|
||||||
{{ dateDifference }}
|
<h2 v-if="!isToday" class="text-xl italic opacity-75">
|
||||||
</h2>
|
– {{ dateDifference }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="grid gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<UiSkeleton class="h-8 w-64" />
|
<UiSkeleton class="h-8 w-64" />
|
||||||
<UiSkeleton class="h-6 w-28" />
|
<UiSkeleton class="h-6 w-28" />
|
||||||
</div>
|
</div>
|
||||||
50
components/calendar/OptionsCTA.vue
Normal file
50
components/calendar/OptionsCTA.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useCalendar } from "@/stores/CalendarStore"
|
||||||
|
import { PhCalendarBlank, PhCheckCircle, PhGear } from "@phosphor-icons/vue"
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
|
||||||
|
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiDropdownMenu>
|
||||||
|
<UiDropdownMenuTrigger as-child>
|
||||||
|
<UiButton variant="secondary" size="icon">
|
||||||
|
<PhGear size="20" weight="fill" />
|
||||||
|
</UiButton>
|
||||||
|
</UiDropdownMenuTrigger>
|
||||||
|
|
||||||
|
<UiDropdownMenuContent :side="'bottom'" :align="'start'" :side-offset="10" :align-offset="25" :collision-padding="40" class="text-right">
|
||||||
|
<UiDropdownMenuArrow />
|
||||||
|
<UiDropdownMenuLabel>
|
||||||
|
{{ $t('entity.calendar.seeOptions') }}
|
||||||
|
</UiDropdownMenuLabel>
|
||||||
|
<UiDropdownMenuSub>
|
||||||
|
<UiDropdownMenuSubTrigger arrow-direction="left" class="p-0 rounded-none">
|
||||||
|
<UiDropdownMenuItem class="flex gap-[1ch] justify-end items-center pointer-events-none">
|
||||||
|
{{ $t('ui.displayMode') }}
|
||||||
|
|
||||||
|
<PhCalendarBlank size="18" weight="fill" />
|
||||||
|
</UiDropdownMenuItem>
|
||||||
|
</UiDropdownMenuSubTrigger>
|
||||||
|
<UiDropdownMenuPortal>
|
||||||
|
<UiDropdownMenuSubContent>
|
||||||
|
<UiDropdownMenuItem
|
||||||
|
v-for="option in viewTypeOptions"
|
||||||
|
:key="option"
|
||||||
|
class="flex gap-[.5ch] items-center rounded-none transition-colors"
|
||||||
|
:class="cn({ 'text-emerald-600': viewTypeTitle === getViewTypeTitle(option) })"
|
||||||
|
@click="setViewType(option)"
|
||||||
|
>
|
||||||
|
<PhCheckCircle v-if="viewTypeTitle === getViewTypeTitle(option)" size="20" weight="fill" />
|
||||||
|
|
||||||
|
{{ getViewTypeTitle(option) }}
|
||||||
|
</UiDropdownMenuItem>
|
||||||
|
</UiDropdownMenuSubContent>
|
||||||
|
</UiDropdownMenuPortal>
|
||||||
|
</UiDropdownMenuSub>
|
||||||
|
</UiDropdownMenuContent>
|
||||||
|
</UiDropdownMenu>
|
||||||
|
</template>
|
||||||
@@ -68,7 +68,8 @@ const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calen
|
|||||||
<UiCardContent class="grow">
|
<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 v-if="calendar.eventNb?.[0].count! > 1">{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span>
|
||||||
|
<span v-else>{{ $t("entity.calendar.hasXEvent", { count: calendar.eventNb?.[0].count }) }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
15
components/calendar/category/List.vue
Normal file
15
components/calendar/category/List.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Category } from "~/models/Category";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
categories: Category[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li v-for="category in categories" :key="category.id">
|
||||||
|
<CalendarCategoryListItem :category="category" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
15
components/calendar/category/ListItem.vue
Normal file
15
components/calendar/category/ListItem.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Category } from "~/models/Category";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
category: Category
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ category.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
72
components/calendar/category/Table.vue
Normal file
72
components/calendar/category/Table.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Category } from "~/models/Category";
|
||||||
|
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue"
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { categories } = defineProps<{
|
||||||
|
categories: Category[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const sortedCategories = computed(() => categories.toSorted((a, b) => a.name.localeCompare(b.name)))
|
||||||
|
|
||||||
|
const deleteDialogOpened = ref<boolean>(false)
|
||||||
|
|
||||||
|
function openDeleteDialog() {
|
||||||
|
deleteDialogOpened.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteDialog() {
|
||||||
|
deleteDialogOpened.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<CalendarCategoryTableHeader />
|
||||||
|
|
||||||
|
<ScrollAreaRoot class="h-36 grow overflow-hidden ">
|
||||||
|
<ScrollAreaViewport class="w-full h-full pr-4" as-child>
|
||||||
|
<div class="[&:last-child]:border-0">
|
||||||
|
<CalendarCategoryTableRow
|
||||||
|
v-for="item in sortedCategories"
|
||||||
|
:key="item.id"
|
||||||
|
:category="item"
|
||||||
|
@on-delete-category="openDeleteDialog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ScrollAreaViewport>
|
||||||
|
<ScrollAreaScrollbar
|
||||||
|
class="flex select-none touch-none p-0.5 bg-background transition-colors duration-100 ease-out hover:bg-foreground/5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:h-2.5"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollAreaThumb
|
||||||
|
class="flex-1 bg-foreground/20 rounded-[10px] relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaScrollbar>
|
||||||
|
</ScrollAreaRoot>
|
||||||
|
|
||||||
|
<CalendarCategoryTableFooter />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiDialog v-model:open="deleteDialogOpened">
|
||||||
|
<UiDialogContent
|
||||||
|
:disable-outside-pointer-events="true"
|
||||||
|
:trap-focus="true"
|
||||||
|
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||||
|
@escape-key-down="closeDeleteDialog"
|
||||||
|
@focus-outside="closeDeleteDialog"
|
||||||
|
@interact-outside="closeDeleteDialog"
|
||||||
|
@pointer-down-outside="closeDeleteDialog"
|
||||||
|
>
|
||||||
|
<UiDialogTitle>
|
||||||
|
{{ $t('entity.category.deleteDialog.title') }}
|
||||||
|
</UiDialogTitle>
|
||||||
|
<UiDialogDescription>
|
||||||
|
{{ $t('entity.category.deleteDialog.subtitle') }}
|
||||||
|
</UiDialogDescription>
|
||||||
|
|
||||||
|
<CalendarFormDeleteCategory />
|
||||||
|
</UiDialogContent>
|
||||||
|
</UiDialog>
|
||||||
|
</template>
|
||||||
130
components/calendar/category/TableFooter.vue
Normal file
130
components/calendar/category/TableFooter.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhPlus } from "@phosphor-icons/vue"
|
||||||
|
import { useToast } from "~/components/ui/toast"
|
||||||
|
import { ToastLifetime } from "~/components/ui/toast/use-toast"
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const { toast } = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
type FooterMode = "add" | "view"
|
||||||
|
const currentMode = ref<FooterMode>("view")
|
||||||
|
|
||||||
|
const rowRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const { focused: inputFocused } = useFocus(inputRef)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle view mode options
|
||||||
|
*/
|
||||||
|
type ToggleViewOptions = {
|
||||||
|
execution?: "now" | "nextTick"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle view mode
|
||||||
|
* @param options.execution - When to execute the toggle. "now" or "nextTick"
|
||||||
|
*/
|
||||||
|
function toggleView(options: ToggleViewOptions = { execution: "now" }) {
|
||||||
|
currentMode.value = "view"
|
||||||
|
|
||||||
|
if (options.execution === "now") {
|
||||||
|
inputFocused.value = false
|
||||||
|
} else {
|
||||||
|
nextTick(() => {
|
||||||
|
inputFocused.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle add mode
|
||||||
|
*/
|
||||||
|
function toggleAdd() {
|
||||||
|
currentMode.value = "add"
|
||||||
|
categorySkeleton.value = { name: "", color: "black" }
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
inputFocused.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickOutside(rowRef, () => toggleView({ execution: "nextTick" }))
|
||||||
|
onKeyStroke("Escape", () => toggleView({ execution: "now" }))
|
||||||
|
|
||||||
|
const { addCategoryFromSkeleton } = useCategoryStore()
|
||||||
|
const { categorySkeleton } = storeToRefs(useCategoryStore())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the update
|
||||||
|
*/
|
||||||
|
async function submitNew() {
|
||||||
|
if (!categorySkeleton.value) return
|
||||||
|
const newCategoryName = toRaw(categorySkeleton.value).name
|
||||||
|
|
||||||
|
const { error } = await tryCatch(addCategoryFromSkeleton())
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast({
|
||||||
|
title: t("entity.category.addedToast.titleError", { category: newCategoryName }),
|
||||||
|
variant: "destructive",
|
||||||
|
duration: ToastLifetime.LONG
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleView({ execution: "now" })
|
||||||
|
toast({
|
||||||
|
title: t("entity.category.addedToast.title", { category: newCategoryName }),
|
||||||
|
variant: "success",
|
||||||
|
duration: ToastLifetime.SHORT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="border-y-[1px] border-b-foreground/10 mr-4">
|
||||||
|
<form
|
||||||
|
ref="rowRef"
|
||||||
|
class="grid grid-cols-12 items-center gap-4 p-1 bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900"
|
||||||
|
@submit.prevent="submitNew"
|
||||||
|
>
|
||||||
|
<div v-if="currentMode === 'add'" class="col-span-1" />
|
||||||
|
<div
|
||||||
|
:class="cn({
|
||||||
|
'col-span-6': currentMode === 'view',
|
||||||
|
'col-span-5': currentMode === 'add'
|
||||||
|
})"
|
||||||
|
>
|
||||||
|
<template v-if="currentMode === 'view'">
|
||||||
|
<button
|
||||||
|
class="p-2 h-full w-full text-left underline-offset-4 hover:underline cursor-pointer"
|
||||||
|
@click="toggleAdd"
|
||||||
|
>
|
||||||
|
<PhPlus size="18" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-if="currentMode === 'add' && categorySkeleton">
|
||||||
|
<input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="categorySkeleton.name"
|
||||||
|
type="text"
|
||||||
|
class="p-1 h-full w-full bg-transparent focus-visible:outline-none italic"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-4">
|
||||||
|
<template v-if="currentMode === 'add' && categorySkeleton">
|
||||||
|
<div class="-mx-2">
|
||||||
|
<InputColor
|
||||||
|
id="category-color"
|
||||||
|
v-model="categorySkeleton.color"
|
||||||
|
position="item-aligned"
|
||||||
|
theme="subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
19
components/calendar/category/TableHeader.vue
Normal file
19
components/calendar/category/TableHeader.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { PhListBullets } from "@phosphor-icons/vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="grid grid-cols-12 gap-4 px-2 py-4 border-b-[1px] border-b-foreground/10 font-bold mr-4">
|
||||||
|
<div class="col-span-1 flex items-end">
|
||||||
|
<PhListBullets size="20" />
|
||||||
|
</div>
|
||||||
|
<div class="col-span-5">
|
||||||
|
Nom
|
||||||
|
</div>
|
||||||
|
<div class="col-span-4">
|
||||||
|
Couleur
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2" />
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
187
components/calendar/category/TableRow.vue
Normal file
187
components/calendar/category/TableRow.vue
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { PhCheck, PhTrash } from "@phosphor-icons/vue"
|
||||||
|
import { useToast } from "~/components/ui/toast"
|
||||||
|
import { ToastLifetime } from "~/components/ui/toast/use-toast"
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
import type { Category } from "~/models/Category"
|
||||||
|
|
||||||
|
const { toast } = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { category } = defineProps<{
|
||||||
|
category: Category
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "on-delete-category", payload: Category): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
type RowMode = "edit" | "view"
|
||||||
|
const currentMode = ref<RowMode>("view")
|
||||||
|
|
||||||
|
const rowRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const { focused: inputFocused } = useFocus(inputRef)
|
||||||
|
const rowHovered = useElementHover(rowRef)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle view mode options
|
||||||
|
*/
|
||||||
|
type ToggleViewOptions = {
|
||||||
|
execution?: "now" | "nextTick"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle view mode
|
||||||
|
* @param options.execution - When to execute the toggle. "now" or "nextTick"
|
||||||
|
*/
|
||||||
|
function toggleView(options: ToggleViewOptions = { execution: "now" }) {
|
||||||
|
currentMode.value = "view"
|
||||||
|
|
||||||
|
if (options.execution === "now") {
|
||||||
|
inputFocused.value = false
|
||||||
|
} else {
|
||||||
|
nextTick(() => {
|
||||||
|
inputFocused.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle edit mode
|
||||||
|
*/
|
||||||
|
function toggleEdit() {
|
||||||
|
currentMode.value = "edit"
|
||||||
|
categorySkeleton.value = structuredClone(toRaw(category))
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
inputFocused.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickOutside(rowRef, () => toggleView({ execution: "nextTick" }))
|
||||||
|
onKeyStroke("Escape", () => toggleView({ execution: "now" }))
|
||||||
|
|
||||||
|
const { resetSkeleton, updateCategoryFromSkeleton } = useCategoryStore()
|
||||||
|
const { categorySkeleton } = storeToRefs(useCategoryStore())
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
resetSkeleton()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the update
|
||||||
|
*/
|
||||||
|
async function submitUpdate() {
|
||||||
|
const { error } = await tryCatch(updateCategoryFromSkeleton())
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast({
|
||||||
|
title: t("entity.category.updatedToast.titleError", { category: category.name }),
|
||||||
|
variant: "destructive",
|
||||||
|
duration: ToastLifetime.LONG
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleView({ execution: "now" })
|
||||||
|
toast({
|
||||||
|
title: t("entity.category.updatedToast.title", { category: category.name }),
|
||||||
|
variant: "success",
|
||||||
|
duration: ToastLifetime.SHORT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQueryDelete() {
|
||||||
|
categorySkeleton.value = structuredClone(toRaw(category))
|
||||||
|
emit("on-delete-category", category)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="rowRef" class="relative">
|
||||||
|
<form
|
||||||
|
class="grid grid-cols-12 items-center gap-4 p-1 border-b-[1px] border-b-foreground/10"
|
||||||
|
:class="cn(
|
||||||
|
{ 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-900 dark:hover:bg-slate-800': currentMode === 'edit' },
|
||||||
|
{ 'bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900': currentMode !== 'edit' }
|
||||||
|
)"
|
||||||
|
@submit.prevent="submitUpdate"
|
||||||
|
>
|
||||||
|
<div class="col-span-1 pointer-events-none">
|
||||||
|
<span class="opacity-50 p-2">{{ category.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-span-5"
|
||||||
|
>
|
||||||
|
<template v-if="currentMode === 'view'">
|
||||||
|
<button
|
||||||
|
class="py-2 px-1 h-full w-full text-left underline-offset-4 hover:underline cursor-pointer"
|
||||||
|
@click="toggleEdit"
|
||||||
|
>
|
||||||
|
{{ category.name }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="currentMode === 'edit' && categorySkeleton">
|
||||||
|
<input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="categorySkeleton.name"
|
||||||
|
type="text"
|
||||||
|
class="p-1 h-full w-full bg-transparent focus-visible:outline-none italic"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-4">
|
||||||
|
<template v-if="currentMode === 'view'">
|
||||||
|
<button
|
||||||
|
class="p-1 h-full w-full text-left text-sm cursor-pointer"
|
||||||
|
@click="toggleEdit"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="bgc"
|
||||||
|
:class="cn(`bgc-${category.color}`)"
|
||||||
|
>
|
||||||
|
{{ $t(`ui.colors.${category.color}`) }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="currentMode === 'edit' && categorySkeleton">
|
||||||
|
<div class="-mx-2">
|
||||||
|
<InputColor
|
||||||
|
id="category-color"
|
||||||
|
v-model="categorySkeleton.color"
|
||||||
|
position="item-aligned"
|
||||||
|
theme="subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<menu class="w-fit absolute top-1/2 -translate-y-1/2 right-2 flex items-center gap-2">
|
||||||
|
<li
|
||||||
|
v-if="currentMode === 'edit' && categorySkeleton"
|
||||||
|
>
|
||||||
|
<UiButton
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
class="w-6 h-6 rounded-full bg-emerald-500 hover:bg-emerald-600 text-white"
|
||||||
|
:title="$t('ui.actions.edit')"
|
||||||
|
@click="submitUpdate"
|
||||||
|
>
|
||||||
|
<PhCheck size="14" weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</li>
|
||||||
|
<li v-else-if="rowHovered">
|
||||||
|
<UiButton
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
class="w-6 h-6 rounded-full hover:bg-red-600 hover:text-white"
|
||||||
|
:title="$t('ui.actions.delete')"
|
||||||
|
@click="handleQueryDelete"
|
||||||
|
>
|
||||||
|
<PhTrash size="14" weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
41
components/calendar/dialog/Categories.vue
Normal file
41
components/calendar/dialog/Categories.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhX } from "@phosphor-icons/vue";
|
||||||
|
|
||||||
|
const { toggleCategoriesModal } = useCalendar()
|
||||||
|
const { categories, isCategoriesModalOpen } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
|
function handleClosing() {
|
||||||
|
toggleCategoriesModal(false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiAlertDialog :open="isCategoriesModalOpen">
|
||||||
|
<UiAlertDialogContent
|
||||||
|
class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl"
|
||||||
|
:disable-outside-pointer-events="true"
|
||||||
|
:trap-focus="true"
|
||||||
|
@focus-outside="handleClosing"
|
||||||
|
@interact-outside="handleClosing"
|
||||||
|
@close-auto-focus="(e) => e.preventDefault()"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<UiAlertDialogTitle>
|
||||||
|
<span class="text-2xl">
|
||||||
|
{{ $t('entity.category.manageDialog.title') }}
|
||||||
|
</span>
|
||||||
|
</UiAlertDialogTitle>
|
||||||
|
|
||||||
|
<UiAlertDialogDescription>
|
||||||
|
{{ $t('entity.category.manageDialog.subtitle') }}
|
||||||
|
</UiAlertDialogDescription>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClosing">
|
||||||
|
<PhX size="20" />
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<CalendarCategoryTable :categories />
|
||||||
|
</UiAlertDialogContent>
|
||||||
|
</UiAlertDialog>
|
||||||
|
</template>
|
||||||
@@ -35,8 +35,7 @@ function openEventCreatePopover() {
|
|||||||
*
|
*
|
||||||
* @param e The closing event (can be keydown or click)
|
* @param e The closing event (can be keydown or click)
|
||||||
*/
|
*/
|
||||||
function handleClosing(e: Event) {
|
function handleClosing() {
|
||||||
e.preventDefault()
|
|
||||||
popoverOpen.value = false
|
popoverOpen.value = false
|
||||||
resetSkeleton()
|
resetSkeleton()
|
||||||
}
|
}
|
||||||
@@ -54,8 +53,8 @@ function handleClosing(e: Event) {
|
|||||||
:disable-outside-pointer-events="true"
|
:disable-outside-pointer-events="true"
|
||||||
:trap-focus="true"
|
:trap-focus="true"
|
||||||
class="pl-3 w-[30rem] max-w-full border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
class="pl-3 w-[30rem] max-w-full border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||||
@escape-key-down="handleClosing"
|
@escape-key-down.prevent="handleClosing"
|
||||||
@pointer-down-outside="handleClosing"
|
@pointer-down-outside.prevent="handleClosing"
|
||||||
>
|
>
|
||||||
<CalendarFormCreateEvent @event-created="handleClosing" />
|
<CalendarFormCreateEvent @event-created="handleClosing" />
|
||||||
</UiPopoverContent>
|
</UiPopoverContent>
|
||||||
|
|||||||
@@ -11,11 +11,8 @@ function toggleDialog() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevents the modal from closing if's still loading
|
* Prevents the modal from closing if's still loading
|
||||||
*
|
|
||||||
* @param e The closing event (can be keydown or click)
|
|
||||||
*/
|
*/
|
||||||
function handleClosing(e: Event) {
|
function handleClosing() {
|
||||||
e.preventDefault()
|
|
||||||
setTimeout(() => resetSkeleton(), 100)
|
setTimeout(() => resetSkeleton(), 100)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -32,8 +29,8 @@ function handleClosing(e: Event) {
|
|||||||
<UiDialog v-model:open="isDialogOpen">
|
<UiDialog v-model:open="isDialogOpen">
|
||||||
<UiDialogContent
|
<UiDialogContent
|
||||||
class="border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
class="border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||||
@escape-key-down="handleClosing"
|
@escape-key-down.prevent="handleClosing"
|
||||||
@pointer-down-outside="handleClosing"
|
@pointer-down-outside.prevent="handleClosing"
|
||||||
>
|
>
|
||||||
<UiDialogTitle>
|
<UiDialogTitle>
|
||||||
{{ $t("entity.calendar.event.addSingle") }}
|
{{ $t("entity.calendar.event.addSingle") }}
|
||||||
|
|||||||
85
components/calendar/form/DeleteCategory.vue
Normal file
85
components/calendar/form/DeleteCategory.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
|
import { useToast } from "~/components/ui/toast";
|
||||||
|
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||||
|
|
||||||
|
const { deleteCategoryFromSkeleton, cancelLatestRequest } = useCategoryStore()
|
||||||
|
const { isDeletingCategory, categorySkeleton } = storeToRefs(useCategoryStore())
|
||||||
|
|
||||||
|
const { toast } = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const isLoading = ref<boolean>(false)
|
||||||
|
|
||||||
|
const formErrors = reactive<{ message: string | null }>({
|
||||||
|
message: null
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleAction(): Promise<void> {
|
||||||
|
if (isLoading.value && !categorySkeleton.value) return
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
const categoryName = categorySkeleton.value?.name
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteCategoryFromSkeleton()
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("entity.category.deletedToast.title", { category: categoryName }),
|
||||||
|
variant: "success",
|
||||||
|
duration: ToastLifetime.MEDIUM
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
formErrors.message = err.message
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click on the cancel button
|
||||||
|
*
|
||||||
|
* Must cancel the abortController in the store, and stop the loading
|
||||||
|
*/
|
||||||
|
function handleCancel(): void {
|
||||||
|
cancelLatestRequest()
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleAction">
|
||||||
|
<div class="grid grid-cols-2 gap-y-4">
|
||||||
|
<div class="text-red-500 ml-8">
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ formErrors.message }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="flex gap-2 justify-between">
|
||||||
|
<UiButton type="button" size="sm" variant="outline" @click="() => isDeletingCategory = false">
|
||||||
|
{{ $t('ui.action.back') }}
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<Transition name="fade-delay">
|
||||||
|
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||||
|
{{ $t('ui.action.cancel') }}
|
||||||
|
</UiButton>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<UiButton size="sm" variant="destructive" :disabled="isLoading">
|
||||||
|
<Transition name="fade">
|
||||||
|
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
{{ $t('ui.action.delete') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -8,18 +8,15 @@ const { isReadOnly } = storeToRefs(useCalendar())
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="border-slate-200 contrast-more:border-slate-500 dark:border-slate-700 border-b-[1px]">
|
<header class="mt-2 grid gap-4 border-slate-200 contrast-more:border-slate-500 dark:border-slate-700 border-b-[1px]">
|
||||||
<div class="px-8 flex justify-between">
|
<div class="px-8 flex items-center justify-between gap-2">
|
||||||
<menu class="flex items-center gap-2">
|
<menu class="flex items-center gap-2">
|
||||||
<li v-if="!isReadOnly">
|
<li v-if="!isReadOnly">
|
||||||
<CalendarDialogQuickCreateEvent />
|
<LazyCalendarDialogQuickCreateEvent />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<CalendarMenuToday />
|
<CalendarMenuToday />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<CalendarMenuNav />
|
|
||||||
</li>
|
|
||||||
<li class="ml-4">
|
<li class="ml-4">
|
||||||
<CalendarCurrentDate />
|
<CalendarCurrentDate />
|
||||||
</li>
|
</li>
|
||||||
@@ -29,13 +26,16 @@ const { isReadOnly } = storeToRefs(useCalendar())
|
|||||||
<li>
|
<li>
|
||||||
<UiButton search-slash @click="revealAdvancedSearch()">
|
<UiButton search-slash @click="revealAdvancedSearch()">
|
||||||
<PhMagnifyingGlass size="20" weight="light" />
|
<PhMagnifyingGlass size="20" weight="light" />
|
||||||
{{ $t('entity.advancedSearch.title') }}
|
<span>
|
||||||
|
{{ $t('entity.advancedSearch.title') }}
|
||||||
|
</span>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<ClientOnly>
|
<CalendarCategoriesCTA />
|
||||||
<CalendarSwitch />
|
</li>
|
||||||
</ClientOnly>
|
<li>
|
||||||
|
<CalendarOptionsCTA />
|
||||||
</li>
|
</li>
|
||||||
</menu>
|
</menu>
|
||||||
</div>
|
</div>
|
||||||
230
components/calendar/menu/MenuSubnav.vue
Normal file
230
components/calendar/menu/MenuSubnav.vue
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhCaretDoubleLeft, PhCaretDoubleRight, PhCaretLeft, PhCaretRight } from "@phosphor-icons/vue"
|
||||||
|
|
||||||
|
const { currentDate } = useCalendar()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
interface DirectionLabels {
|
||||||
|
pastFar: string
|
||||||
|
pastNear: string
|
||||||
|
futureNear: string
|
||||||
|
futureFar: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentConfig, decrementViewMonth, incrementViewMonth, decrementViewYear, incrementViewYear } =
|
||||||
|
useCalendar()
|
||||||
|
|
||||||
|
const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
|
||||||
|
switch (currentConfig.viewType) {
|
||||||
|
case "month":
|
||||||
|
return {
|
||||||
|
pastFar: t("entity.calendar.years.prevSingular"),
|
||||||
|
pastNear: t("entity.calendar.months.prevSingular"),
|
||||||
|
futureNear: t("entity.calendar.months.nextSingular"),
|
||||||
|
futureFar: t("entity.calendar.years.nextSingular")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "year":
|
||||||
|
return {
|
||||||
|
pastFar: t("entity.calendar.decades.prevSingular"),
|
||||||
|
pastNear: t("entity.calendar.years.prevSingular"),
|
||||||
|
futureNear: t("entity.calendar.years.nextSingular"),
|
||||||
|
futureFar: t("entity.calendar.decades.nextSingular")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "decade":
|
||||||
|
return {
|
||||||
|
pastFar: t("entity.calendar.centuries.prevSingular"),
|
||||||
|
pastNear: t("entity.calendar.decades.prevSingular"),
|
||||||
|
futureNear: t("entity.calendar.decades.nextSingular"),
|
||||||
|
futureFar: t("entity.calendar.centuries.nextSingular")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "century":
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
pastFar: t("entity.calendar.millenias.prevSingular"),
|
||||||
|
pastNear: t("entity.calendar.centuries.prevSingular"),
|
||||||
|
futureNear: t("entity.calendar.centuries.nextSingular"),
|
||||||
|
futureFar: t("entity.calendar.millenias.nextSingular")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function toPastFar(): void {
|
||||||
|
switch (currentConfig.viewType) {
|
||||||
|
case "month":
|
||||||
|
decrementViewYear()
|
||||||
|
break
|
||||||
|
|
||||||
|
case "year":
|
||||||
|
decrementViewYear(10)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "decade":
|
||||||
|
decrementViewYear(100)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "century":
|
||||||
|
default:
|
||||||
|
decrementViewYear(1000)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPastNear(): void {
|
||||||
|
switch (currentConfig.viewType) {
|
||||||
|
case "month":
|
||||||
|
decrementViewMonth()
|
||||||
|
break
|
||||||
|
|
||||||
|
case "year":
|
||||||
|
decrementViewYear()
|
||||||
|
break
|
||||||
|
|
||||||
|
case "decade":
|
||||||
|
decrementViewYear(10)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "century":
|
||||||
|
default:
|
||||||
|
decrementViewYear(100)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFutureNear(): void {
|
||||||
|
switch (currentConfig.viewType) {
|
||||||
|
case "month":
|
||||||
|
incrementViewMonth()
|
||||||
|
break
|
||||||
|
|
||||||
|
case "year":
|
||||||
|
incrementViewYear()
|
||||||
|
break
|
||||||
|
|
||||||
|
case "decade":
|
||||||
|
incrementViewYear(10)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "century":
|
||||||
|
default:
|
||||||
|
incrementViewYear(100)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFutureFar(): void {
|
||||||
|
switch (currentConfig.viewType) {
|
||||||
|
case "month":
|
||||||
|
incrementViewYear()
|
||||||
|
break
|
||||||
|
|
||||||
|
case "year":
|
||||||
|
incrementViewYear(10)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "decade":
|
||||||
|
incrementViewYear(100)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "century":
|
||||||
|
default:
|
||||||
|
incrementViewYear(1000)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="grid items-end w-40 px-4 py-2 border-slate-200 bg-white dark:bg-black dark:border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm text-sm transition-colors">
|
||||||
|
<ClientOnly>
|
||||||
|
<span>{{ currentDate.currentDateTitle }}</span>
|
||||||
|
|
||||||
|
<template #fallback>
|
||||||
|
<span class="inline-block">
|
||||||
|
<UiSkeleton class="h-[19px] w-full rounded-sm" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="rounded-t-sm rounded-b-none border-b-0"
|
||||||
|
@click="toPastFar()"
|
||||||
|
>
|
||||||
|
<PhCaretDoubleLeft size="18" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>{{ activeDirectionLabels.pastFar }}</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="rounded-t-sm rounded-b-none border-b-0"
|
||||||
|
@click="toPastNear()"
|
||||||
|
>
|
||||||
|
<PhCaretLeft size="18" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>{{ activeDirectionLabels.pastNear }}</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="rounded-t-sm rounded-b-none border-b-0"
|
||||||
|
@click="toFutureNear()"
|
||||||
|
>
|
||||||
|
<PhCaretRight size="18" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>{{ activeDirectionLabels.futureNear }}</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="rounded-t-sm rounded-b-none border-b-0"
|
||||||
|
@click="toFutureFar()"
|
||||||
|
>
|
||||||
|
<PhCaretDoubleRight size="18" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>{{ activeDirectionLabels.futureFar }}</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
import SearchList from "./lists/SearchList.vue"
|
import SearchList from "./lists/SearchList.vue"
|
||||||
import type { Category } from "~/models/Category"
|
import type { Category } from "~/models/Category"
|
||||||
|
|
||||||
const { isAdvancedSearchOpen, allEvents } = storeToRefs(useCalendar())
|
const { isAdvancedSearchOpen, allEvents, categories } = storeToRefs(useCalendar())
|
||||||
const { characters } = storeToRefs(useCharacters())
|
const { characters } = storeToRefs(useCharacters())
|
||||||
|
|
||||||
const searchInput = shallowRef<HTMLInputElement>()
|
const searchInput = shallowRef<HTMLInputElement>()
|
||||||
@@ -207,8 +207,7 @@ watch([currentPage, selectedEntity], () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Compute categories based on current selectedEntity
|
// Compute categories based on current selectedEntity
|
||||||
const { data: resCategories } = await useFetch("/api/calendars/categories/query")
|
const currentCategories = computed(() => categories.value)
|
||||||
const currentCategories = ref<Category[]>(resCategories.value?.data as Category[])
|
|
||||||
|
|
||||||
const selectedCategories = ref<(Category)[]>([])
|
const selectedCategories = ref<(Category)[]>([])
|
||||||
const categoryFilterOpened = ref<boolean>(false)
|
const categoryFilterOpened = ref<boolean>(false)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useElementBounding } from "@vueuse/core"
|
|||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import { computed, ref, type ComputedRef } from "vue"
|
import { computed, ref, type ComputedRef } from "vue"
|
||||||
|
|
||||||
import CalendarEventButton from "../../CalendarEvent.vue"
|
import CalendarEventButton from "../../event/Event.vue"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
date: RPGDate
|
date: RPGDate
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
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<{
|
const { id, theme = "normal", position = "popper" } = defineProps<{
|
||||||
id: string
|
id: string
|
||||||
|
theme?: "normal" | "subtle"
|
||||||
|
position?: "item-aligned" | "popper" | undefined
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const model = defineModel<RPGColor>({ default: "white" });
|
const model = defineModel<RPGColor>({ default: "white" });
|
||||||
@@ -11,27 +13,23 @@ const model = defineModel<RPGColor>({ default: "white" });
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiSelect v-model="model">
|
<UiSelect v-model="model">
|
||||||
<UiSelectTrigger :id>
|
<UiSelectTrigger :id :class="cn({ 'h-9': theme === 'subtle' })">
|
||||||
<UiSelectValue
|
<UiSelectValue
|
||||||
:placeholder="$t('ui.colors.selectOne')"
|
:placeholder="$t('ui.colors.selectOne')"
|
||||||
class="input-color"
|
class="bgc"
|
||||||
:class="
|
:class="cn(`bgc-${model}`)"
|
||||||
cn(
|
|
||||||
`input-color-${model}`,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</UiSelectTrigger>
|
</UiSelectTrigger>
|
||||||
<UiSelectContent position="popper">
|
<UiSelectContent :position>
|
||||||
<UiSelectGroup>
|
<UiSelectGroup>
|
||||||
<UiSelectItem
|
<UiSelectItem
|
||||||
v-for="color in rpgColors"
|
v-for="color in rpgColors"
|
||||||
:key="color"
|
:key="color"
|
||||||
:value="color"
|
:value="color"
|
||||||
class="input-color"
|
class="bgc"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
`input-color-${color}`,
|
`bgc-${color}`,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -41,76 +39,3 @@ const model = defineModel<RPGColor>({ default: "white" });
|
|||||||
</UiSelectContent>
|
</UiSelectContent>
|
||||||
</UiSelect>
|
</UiSelect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.input-color {
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
display: inline-block;
|
|
||||||
width: .75rem;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
border-radius: .25rem;
|
|
||||||
background-color: red;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.input-color-red::before {
|
|
||||||
@apply bg-red-500;
|
|
||||||
}
|
|
||||||
&.input-color-orange::before {
|
|
||||||
@apply bg-orange-500;
|
|
||||||
}
|
|
||||||
&.input-color-amber::before {
|
|
||||||
@apply bg-amber-500;
|
|
||||||
}
|
|
||||||
&.input-color-yellow::before {
|
|
||||||
@apply bg-yellow-500;
|
|
||||||
}
|
|
||||||
&.input-color-lime::before {
|
|
||||||
@apply bg-lime-500;
|
|
||||||
}
|
|
||||||
&.input-color-green::before {
|
|
||||||
@apply bg-green-500;
|
|
||||||
}
|
|
||||||
&.input-color-emerald::before {
|
|
||||||
@apply bg-emerald-600;
|
|
||||||
}
|
|
||||||
&.input-color-teal::before {
|
|
||||||
@apply bg-teal-600;
|
|
||||||
}
|
|
||||||
&.input-color-cyan::before {
|
|
||||||
@apply bg-cyan-600;
|
|
||||||
}
|
|
||||||
&.input-color-sky::before {
|
|
||||||
@apply bg-sky-600;
|
|
||||||
}
|
|
||||||
&.input-color-blue::before {
|
|
||||||
@apply bg-blue-600;
|
|
||||||
}
|
|
||||||
&.input-color-indigo::before {
|
|
||||||
@apply bg-indigo-600;
|
|
||||||
}
|
|
||||||
&.input-color-violet::before {
|
|
||||||
@apply bg-violet-600;
|
|
||||||
}
|
|
||||||
&.input-color-purple::before {
|
|
||||||
@apply bg-purple-600;
|
|
||||||
}
|
|
||||||
&.input-color-fuchsia::before {
|
|
||||||
@apply bg-fuchsia-600;
|
|
||||||
}
|
|
||||||
&.input-color-pink::before {
|
|
||||||
@apply bg-pink-600;
|
|
||||||
}
|
|
||||||
&.input-color-rose::before {
|
|
||||||
@apply bg-rose-600;
|
|
||||||
}
|
|
||||||
&.input-color-black::before {
|
|
||||||
@apply bg-black dark:border-[1px] dark:border-slate-300;
|
|
||||||
}
|
|
||||||
&.input-color-white::before {
|
|
||||||
@apply bg-white border-[1px] border-slate-700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
<template>
|
<template>
|
||||||
<AlertDialogPortal>
|
<AlertDialogPortal>
|
||||||
<AlertDialogOverlay
|
<AlertDialogOverlay
|
||||||
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
class="fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||||
/>
|
/>
|
||||||
<AlertDialogContent
|
<AlertDialogContent
|
||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
v-if="props.searchSlash"
|
v-if="props.searchSlash"
|
||||||
class="h-4 p-1 ml-1 grid place-items-center text-[0.7em] leading-none font-semibold text-slate-500 bg-slate-100 border-slate-300 border-[1px] rounded-[3px] shadow-sm group-hover:text-slate-600 group-hover:bg-slate-200 group-hover:border-slate-400 transition-colors"
|
class="h-4 p-1 ml-1 grid place-items-center text-[0.7em] leading-none font-semibold text-slate-500 bg-slate-100 border-slate-300 border-[1px] rounded-[3px] shadow-sm group-hover:text-slate-600 group-hover:bg-slate-200 group-hover:border-slate-400 transition-colors"
|
||||||
>
|
>
|
||||||
<span class="-mt-[2px]">CTRL + :</span>
|
<kbd class="-mt-[2px]">CTRL + :</kbd>
|
||||||
</span>
|
</span>
|
||||||
</Primitive>
|
</Primitive>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const buttonVariants = cva(
|
|||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
|
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
secondary: "bg-secondary text-secondary-foreground hover:bg-primary/20",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline"
|
link: "text-primary underline-offset-4 hover:underline"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
<template>
|
<template>
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay
|
<DialogOverlay
|
||||||
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
class="fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||||
/>
|
/>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
<template>
|
<template>
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay
|
<DialogOverlay
|
||||||
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
:class="
|
:class="
|
||||||
|
|||||||
13
components/ui/dropdown-menu/DropdownMenuArrow.vue
Normal file
13
components/ui/dropdown-menu/DropdownMenuArrow.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DropdownMenuArrow, type DropdownMenuArrowProps, useForwardProps } from "radix-vue"
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuArrowProps>()
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(props)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuArrow class="fill-border" v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuArrow>
|
||||||
|
</template>
|
||||||
@@ -5,10 +5,10 @@ import {
|
|||||||
type DropdownMenuSubTriggerProps,
|
type DropdownMenuSubTriggerProps,
|
||||||
useForwardProps
|
useForwardProps
|
||||||
} from "radix-vue"
|
} from "radix-vue"
|
||||||
import { ChevronRight } from "lucide-vue-next"
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { PhCaretLeft, PhCaretRight } from "@phosphor-icons/vue";
|
||||||
|
|
||||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"] }>()
|
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], arrowDirection?: "left" | "right" }>()
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
const delegatedProps = computed(() => {
|
||||||
const { class: _, ...delegated } = props
|
const { class: _, ...delegated } = props
|
||||||
@@ -29,7 +29,17 @@ const forwardedProps = useForwardProps(delegatedProps)
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<slot />
|
<template v-if="props.arrowDirection === 'left'">
|
||||||
<ChevronRight class="ml-auto h-4 w-4" />
|
<PhCaretLeft class="mr-auto h-4 w-4" />
|
||||||
|
|
||||||
|
<span class="inline-block">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<PhCaretRight class="ml-auto h-4 w-4" />
|
||||||
|
</template>
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export { DropdownMenuPortal } from "radix-vue"
|
export { DropdownMenuPortal } from "radix-vue"
|
||||||
|
|
||||||
export { default as DropdownMenu } from "./DropdownMenu.vue"
|
export { default as DropdownMenu } from "./DropdownMenu.vue"
|
||||||
|
export { default as DropdownMenuArrow } from "./DropdownMenuArrow.vue"
|
||||||
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
|
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
|
||||||
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
|
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
|
||||||
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
|
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
|||||||
v-bind="forwardedProps"
|
v-bind="forwardedProps"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background dark:bg-slate-950 contrast-more:dark:bg-slate-900 px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background dark:bg-slate-950 contrast-more:dark:bg-slate-900 px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
16
components/ui/table/Table.vue
Normal file
16
components/ui/table/Table.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full overflow-auto">
|
||||||
|
<table :class="cn('w-full caption-bottom text-sm', props.class)">
|
||||||
|
<slot />
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
14
components/ui/table/TableBody.vue
Normal file
14
components/ui/table/TableBody.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tbody :class="cn('[&_tr:last-child]:border-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
14
components/ui/table/TableCaption.vue
Normal file
14
components/ui/table/TableCaption.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<caption :class="cn('mt-4 text-sm text-muted-foreground', props.class)">
|
||||||
|
<slot />
|
||||||
|
</caption>
|
||||||
|
</template>
|
||||||
21
components/ui/table/TableCell.vue
Normal file
21
components/ui/table/TableCell.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<td
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-4 align-middle [&:has([role=checkbox])]:pr-0',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
38
components/ui/table/TableEmpty.vue
Normal file
38
components/ui/table/TableEmpty.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import TableCell from "./TableCell.vue"
|
||||||
|
import TableRow from "./TableRow.vue"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
colspan?: number
|
||||||
|
}>(), {
|
||||||
|
colspan: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-4 whitespace-nowrap align-middle text-sm text-foreground',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center py-10">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
14
components/ui/table/TableFooter.vue
Normal file
14
components/ui/table/TableFooter.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tfoot :class="cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</tfoot>
|
||||||
|
</template>
|
||||||
14
components/ui/table/TableHead.vue
Normal file
14
components/ui/table/TableHead.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<th :class="cn('h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
14
components/ui/table/TableHeader.vue
Normal file
14
components/ui/table/TableHeader.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<thead :class="cn('[&_tr]:border-b', props.class)">
|
||||||
|
<slot />
|
||||||
|
</thead>
|
||||||
|
</template>
|
||||||
14
components/ui/table/TableRow.vue
Normal file
14
components/ui/table/TableRow.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr :class="cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', props.class)">
|
||||||
|
<slot />
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
9
components/ui/table/index.ts
Normal file
9
components/ui/table/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export { default as Table } from './Table.vue'
|
||||||
|
export { default as TableBody } from './TableBody.vue'
|
||||||
|
export { default as TableCaption } from './TableCaption.vue'
|
||||||
|
export { default as TableCell } from './TableCell.vue'
|
||||||
|
export { default as TableEmpty } from './TableEmpty.vue'
|
||||||
|
export { default as TableFooter } from './TableFooter.vue'
|
||||||
|
export { default as TableHead } from './TableHead.vue'
|
||||||
|
export { default as TableHeader } from './TableHeader.vue'
|
||||||
|
export { default as TableRow } from './TableRow.vue'
|
||||||
@@ -79,7 +79,7 @@ export default defineI18nConfig(() => ({
|
|||||||
dark: "Dark",
|
dark: "Dark",
|
||||||
light: "Light",
|
light: "Light",
|
||||||
system: "System",
|
system: "System",
|
||||||
displayMode: "Display mode"
|
displayMode: "Display"
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
title: "Title",
|
title: "Title",
|
||||||
@@ -93,13 +93,32 @@ export default defineI18nConfig(() => ({
|
|||||||
search: "Search categories",
|
search: "Search categories",
|
||||||
notFoundAny: "No categories found.",
|
notFoundAny: "No categories found.",
|
||||||
addPrimary: "Add a primary category",
|
addPrimary: "Add a primary category",
|
||||||
addSecondaries: "Add secondary categories"
|
addSecondaries: "Add secondary categories",
|
||||||
|
manageDialog: {
|
||||||
|
title: "Manage categories",
|
||||||
|
subtitle: "Add and change the categories of your calendar",
|
||||||
|
},
|
||||||
|
deleteDialog: {
|
||||||
|
title: "Delete this category ?",
|
||||||
|
subtitle: "The events attached to this category won't be deleted, but you'll lose the category for this calendar.",
|
||||||
|
},
|
||||||
|
addedToast: {
|
||||||
|
title: "The category \"{category}\" has been added to the calendar.",
|
||||||
|
titleError: "An error has occured and the category \"{category}\" wasn't added to the calendar.",
|
||||||
|
},
|
||||||
|
updatedToast: {
|
||||||
|
title: "The category \"{category}\" has been successfuly updated.",
|
||||||
|
titleError: "An error has occured and the category \"{category}\" wasn't updated.",
|
||||||
|
},
|
||||||
|
deletedToast: {
|
||||||
|
title: "The category \"{category}\" has been successfuly deleted.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
isLoading: "Loading in progress…",
|
isLoading: "Loading in progress…",
|
||||||
addDescription: "Add a description",
|
addDescription: "Add a description",
|
||||||
deleteOne: "Delete \"{entity}\"",
|
deleteOne: "Delete \"{entity}\"",
|
||||||
advancedSearch: {
|
advancedSearch: {
|
||||||
title: "Advanced search",
|
title: "Search",
|
||||||
subtitle: "Search through calendar and world data",
|
subtitle: "Search through calendar and world data",
|
||||||
older: "Older",
|
older: "Older",
|
||||||
newer: "Newer",
|
newer: "Newer",
|
||||||
@@ -147,7 +166,10 @@ export default defineI18nConfig(() => ({
|
|||||||
notFoundForWorld: "No calendar for this world… yet !",
|
notFoundForWorld: "No calendar for this world… yet !",
|
||||||
backToList: "Go back to calendars",
|
backToList: "Go back to calendars",
|
||||||
isLoading: "Calendar is loading…",
|
isLoading: "Calendar is loading…",
|
||||||
|
hasXEvent: "{count} available event",
|
||||||
hasXEvents: "{count} available events",
|
hasXEvents: "{count} available events",
|
||||||
|
seeCategories: "Modify categories",
|
||||||
|
seeOptions: "Calendar options",
|
||||||
date: {
|
date: {
|
||||||
start: "Start date",
|
start: "Start date",
|
||||||
end: "End date",
|
end: "End date",
|
||||||
@@ -183,8 +205,8 @@ export default defineI18nConfig(() => ({
|
|||||||
title: "Event title",
|
title: "Event title",
|
||||||
isStart: "Start",
|
isStart: "Start",
|
||||||
isEnd: "End",
|
isEnd: "End",
|
||||||
isHidden: "Hidden event",
|
isHidden: "Hidden",
|
||||||
isPublic: "Public event",
|
isPublic: "Public",
|
||||||
hiddenTooltip: "This event is visible only to game masters.",
|
hiddenTooltip: "This event is visible only to game masters.",
|
||||||
addLocation: "Add a place",
|
addLocation: "Add a place",
|
||||||
prevPage: "Previous page with events",
|
prevPage: "Previous page with events",
|
||||||
@@ -242,26 +264,31 @@ export default defineI18nConfig(() => ({
|
|||||||
},
|
},
|
||||||
millennia: {
|
millennia: {
|
||||||
nameSingular: "Millennia",
|
nameSingular: "Millennia",
|
||||||
|
displayMode: "Millennial",
|
||||||
nextSingular: "Next millennia",
|
nextSingular: "Next millennia",
|
||||||
prevSingular: "Last millennia",
|
prevSingular: "Last millennia",
|
||||||
},
|
},
|
||||||
centuries: {
|
centuries: {
|
||||||
nameSingular: "Century",
|
nameSingular: "Century",
|
||||||
|
displayMode: "Centuries",
|
||||||
nextSingular: "Next century",
|
nextSingular: "Next century",
|
||||||
prevSingular: "Last century",
|
prevSingular: "Last century",
|
||||||
},
|
},
|
||||||
decades: {
|
decades: {
|
||||||
nameSingular: "Decade",
|
nameSingular: "Decade",
|
||||||
|
displayMode: "Decadal",
|
||||||
nextSingular: "Next decade",
|
nextSingular: "Next decade",
|
||||||
prevSingular: "Last decade",
|
prevSingular: "Last decade",
|
||||||
},
|
},
|
||||||
years: {
|
years: {
|
||||||
nameSingular: "Year",
|
nameSingular: "Year",
|
||||||
|
displayMode: "Yearly",
|
||||||
nextSingular: "Next year",
|
nextSingular: "Next year",
|
||||||
prevSingular: "Last year",
|
prevSingular: "Last year",
|
||||||
},
|
},
|
||||||
months: {
|
months: {
|
||||||
nameSingular: "Month",
|
nameSingular: "Month",
|
||||||
|
displayMode: "Monthly",
|
||||||
nextSingular: "Next month",
|
nextSingular: "Next month",
|
||||||
prevSingular: "Last month",
|
prevSingular: "Last month",
|
||||||
inputName: "Month's name",
|
inputName: "Month's name",
|
||||||
@@ -375,7 +402,7 @@ export default defineI18nConfig(() => ({
|
|||||||
dark: "Sombre",
|
dark: "Sombre",
|
||||||
light: "Clair",
|
light: "Clair",
|
||||||
system: "Système",
|
system: "Système",
|
||||||
displayMode: "Mode d'affichage",
|
displayMode: "Affichage",
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
title: "Titre",
|
title: "Titre",
|
||||||
@@ -389,13 +416,32 @@ export default defineI18nConfig(() => ({
|
|||||||
search: "Rechercher les catégories",
|
search: "Rechercher les catégories",
|
||||||
notFoundAny: "Aucune catégorie trouvée.",
|
notFoundAny: "Aucune catégorie trouvée.",
|
||||||
addPrimary: "Ajouter une catégorie principale",
|
addPrimary: "Ajouter une catégorie principale",
|
||||||
addSecondaries: "Ajouter des catégories secondaires"
|
addSecondaries: "Ajouter des catégories secondaires",
|
||||||
|
manageDialog: {
|
||||||
|
title: "Gestion des catégories",
|
||||||
|
subtitle: "Créer et modifier les catégories de votre calendrier",
|
||||||
|
},
|
||||||
|
deleteDialog: {
|
||||||
|
title: "Supprimer cette catégorie ?",
|
||||||
|
subtitle: "Les évènements l'utilisant ne seront pas supprimés.",
|
||||||
|
},
|
||||||
|
addedToast: {
|
||||||
|
title: "La catégorie \"{category}\" a été ajoutée au calendrier.",
|
||||||
|
titleError: "Une erreur s'est produite et la catégorie \"{category}\" n'a pas pu être ajoutée.",
|
||||||
|
},
|
||||||
|
updatedToast: {
|
||||||
|
title: "La catégorie \"{category}\" a été modifiée avec succès.",
|
||||||
|
titleError: "Une erreur s'est produite et la catégorie \"{category}\" n'a pas pu être modifiée.",
|
||||||
|
},
|
||||||
|
deletedToast: {
|
||||||
|
title: "La catégorie \"{category}\" a été supprimée avec succès.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
isLoading: "Chargement en cours…",
|
isLoading: "Chargement en cours…",
|
||||||
addDescription: "Ajouter une description",
|
addDescription: "Ajouter une description",
|
||||||
deleteOne: "Supprimer \"{entity}\"",
|
deleteOne: "Supprimer \"{entity}\"",
|
||||||
advancedSearch: {
|
advancedSearch: {
|
||||||
title: "Recherche avancée",
|
title: "Rechercher",
|
||||||
subtitle: "Rechercher les données disponibles sur le calendrier",
|
subtitle: "Rechercher les données disponibles sur le calendrier",
|
||||||
older: "Plus ancien",
|
older: "Plus ancien",
|
||||||
newer: "Plus récent",
|
newer: "Plus récent",
|
||||||
@@ -443,7 +489,10 @@ export default defineI18nConfig(() => ({
|
|||||||
notFoundForWorld: "Aucun calendrier pour ce monde… pour l'instant !",
|
notFoundForWorld: "Aucun calendrier pour ce monde… pour l'instant !",
|
||||||
backToList: "Retourner aux calendriers",
|
backToList: "Retourner aux calendriers",
|
||||||
isLoading: "Chargement du calendrier…",
|
isLoading: "Chargement du calendrier…",
|
||||||
|
hasXEvent: "{count} évènement disponible",
|
||||||
hasXEvents: "{count} évènements disponibles",
|
hasXEvents: "{count} évènements disponibles",
|
||||||
|
seeCategories: "Gestion des catégories",
|
||||||
|
seeOptions: "Options du calendrier",
|
||||||
date: {
|
date: {
|
||||||
start: "Date de début",
|
start: "Date de début",
|
||||||
end: "Date de fin",
|
end: "Date de fin",
|
||||||
@@ -479,8 +528,8 @@ export default defineI18nConfig(() => ({
|
|||||||
title: "Titre de l'évènement",
|
title: "Titre de l'évènement",
|
||||||
isStart: "Début",
|
isStart: "Début",
|
||||||
isEnd: "Fin",
|
isEnd: "Fin",
|
||||||
isHidden: "Évènement privé",
|
isHidden: "Privé",
|
||||||
isPublic: "Évènement public",
|
isPublic: "Public",
|
||||||
hiddenTooltip: "Cet évènement est uniquement visible pour les maîtres du jeu.",
|
hiddenTooltip: "Cet évènement est uniquement visible pour les maîtres du jeu.",
|
||||||
addLocation: "Ajouter un endroit",
|
addLocation: "Ajouter un endroit",
|
||||||
prevPage: "Précédente page à évènements",
|
prevPage: "Précédente page à évènements",
|
||||||
@@ -541,26 +590,31 @@ export default defineI18nConfig(() => ({
|
|||||||
},
|
},
|
||||||
millennia: {
|
millennia: {
|
||||||
nameSingular: "Millénaire",
|
nameSingular: "Millénaire",
|
||||||
|
displayMode: "Par millénaire",
|
||||||
nextSingular: "Millénaire suivant",
|
nextSingular: "Millénaire suivant",
|
||||||
prevSingular: "Millénaire précédent",
|
prevSingular: "Millénaire précédent",
|
||||||
},
|
},
|
||||||
centuries: {
|
centuries: {
|
||||||
nameSingular: "Siècle",
|
nameSingular: "Siècle",
|
||||||
|
displayMode: "Par siècle",
|
||||||
nextSingular: "Siècle suivant",
|
nextSingular: "Siècle suivant",
|
||||||
prevSingular: "Siècle précédent",
|
prevSingular: "Siècle précédent",
|
||||||
},
|
},
|
||||||
decades: {
|
decades: {
|
||||||
nameSingular: "Décennie",
|
nameSingular: "Décennie",
|
||||||
|
displayMode: "Par décennie",
|
||||||
nextSingular: "Décennie suivante",
|
nextSingular: "Décennie suivante",
|
||||||
prevSingular: "Décennie précédente",
|
prevSingular: "Décennie précédente",
|
||||||
},
|
},
|
||||||
years: {
|
years: {
|
||||||
nameSingular: "Année",
|
nameSingular: "Année",
|
||||||
|
displayMode: "Annuel",
|
||||||
nextSingular: "Année suivante",
|
nextSingular: "Année suivante",
|
||||||
prevSingular: "Année précédente",
|
prevSingular: "Année précédente",
|
||||||
},
|
},
|
||||||
months: {
|
months: {
|
||||||
nameSingular: "Mois",
|
nameSingular: "Mois",
|
||||||
|
displayMode: "Mensuel",
|
||||||
nextSingular: "Mois suivant",
|
nextSingular: "Mois suivant",
|
||||||
prevSingular: "Mois précédent",
|
prevSingular: "Mois précédent",
|
||||||
inputName: "Nom du mois",
|
inputName: "Nom du mois",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { dateSchema, type RPGDate } from "./Date"
|
|||||||
import type { World } from "./World"
|
import type { World } from "./World"
|
||||||
import type { ContentState } from "./Entity"
|
import type { ContentState } from "./Entity"
|
||||||
import type { RPGColor } from "./Color"
|
import type { RPGColor } from "./Color"
|
||||||
|
import type { Category } from "./Category"
|
||||||
|
|
||||||
export interface CalendarConfig {
|
export interface CalendarConfig {
|
||||||
months: CalendarMonth[]
|
months: CalendarMonth[]
|
||||||
@@ -16,7 +16,8 @@ export interface Calendar extends CalendarConfig {
|
|||||||
id?: number
|
id?: number
|
||||||
shortId?: string
|
shortId?: string
|
||||||
name: string
|
name: string
|
||||||
events: CalendarEvent[]
|
events: CalendarEvent[],
|
||||||
|
categories: Category[]
|
||||||
eventNb?: Array<{ count: number }>
|
eventNb?: Array<{ count: number }>
|
||||||
state: ContentState
|
state: ContentState
|
||||||
color?: RPGColor
|
color?: RPGColor
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { categorySchema, type Category } from "./Category"
|
import type { Category } from "./Category"
|
||||||
import { dateSchema, type RPGDate } from "./Date"
|
import { dateSchema, type RPGDate } from "./Date"
|
||||||
|
|
||||||
export interface CalendarEvent {
|
export interface CalendarEvent {
|
||||||
@@ -26,7 +26,11 @@ export const postEventBodySchema = z.object({
|
|||||||
startDate: dateSchema.required(),
|
startDate: dateSchema.required(),
|
||||||
endDate: dateSchema.optional().nullable(),
|
endDate: dateSchema.optional().nullable(),
|
||||||
hidden: z.boolean().optional().nullable(),
|
hidden: z.boolean().optional().nullable(),
|
||||||
category: categorySchema.optional().nullable(),
|
category: z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
name: z.string(),
|
||||||
|
color: z.string().default("black")
|
||||||
|
}).optional().nullable(),
|
||||||
}),
|
}),
|
||||||
calendarId: z.number({ coerce: true }).int().positive()
|
calendarId: z.number({ coerce: true }).int().positive()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import { z } from "zod"
|
|||||||
import type { RPGColor } from "./Color"
|
import type { RPGColor } from "./Color"
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
color?: RPGColor
|
color?: RPGColor
|
||||||
}
|
}
|
||||||
|
|
||||||
export const categorySchema = z.object({
|
export const categorySchema = z.object({
|
||||||
id: z.number().int(),
|
category: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
color: z.string().default("black")
|
color: z.string().default("black"),
|
||||||
|
}),
|
||||||
|
calendarId: z.number({ coerce: true }).int().positive()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhArrowBendDoubleUpLeft, PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
import { PhArrowBendDoubleUpLeft, PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
import type { Category } from "~/models/Category";
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const shortId = route.params.id
|
const shortId = route.params.id
|
||||||
@@ -14,7 +13,7 @@ const {
|
|||||||
refresh: calRefresh
|
refresh: calRefresh
|
||||||
} = await useLazyFetch<{ data: Calendar }>("/api/calendars/query",
|
} = await useLazyFetch<{ data: Calendar }>("/api/calendars/query",
|
||||||
{
|
{
|
||||||
key: `calendar-${shortId}`,
|
key: "active-calendar",
|
||||||
query: {
|
query: {
|
||||||
shortId,
|
shortId,
|
||||||
full: true
|
full: true
|
||||||
@@ -22,20 +21,17 @@ const {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const isLoading = computed(() => calendarStatus.value === "pending")
|
||||||
data: categories,
|
|
||||||
status: categoriesStatus,
|
watch(user, () => calRefresh())
|
||||||
refresh: catRefresh
|
|
||||||
} = await useLazyFetch<{ data: Category[] }>("/api/calendars/categories/query",
|
const { setActiveCalendar } = useCalendar()
|
||||||
{ key: `categories-${shortId}` }
|
watch(isLoading, (n) => {
|
||||||
|
if (!n && calendar.value?.data) {
|
||||||
|
setActiveCalendar(calendar.value?.data)
|
||||||
|
}
|
||||||
|
}, { immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const isLoading = computed(() => calendarStatus.value === "pending" || categoriesStatus.value === "pending")
|
|
||||||
|
|
||||||
watch(user, () => {
|
|
||||||
calRefresh()
|
|
||||||
catRefresh()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -52,12 +48,12 @@ watch(user, () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="calendar?.data && categories?.data" class="h-full w-full pt-8">
|
<div v-else-if="calendar?.data" class="h-full w-full pt-8">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>{{ calendar.data.name }}</Title>
|
<Title>{{ calendar.data.name }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Calendar :calendar-data="calendar.data" :categories="categories.data" />
|
<Calendar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="h-full w-full grid place-items-center">
|
<div v-else class="h-full w-full grid place-items-center">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhArrowBendDoubleUpLeft, PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
import { PhArrowBendDoubleUpLeft, PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
import type { Category } from "~/models/Category";
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["auth-guard"]
|
middleware: ["auth-guard"]
|
||||||
@@ -24,7 +23,7 @@ const {
|
|||||||
status: calendarStatus
|
status: calendarStatus
|
||||||
} = await useLazyFetch<{ data: Calendar }>("/api/calendars/query",
|
} = await useLazyFetch<{ data: Calendar }>("/api/calendars/query",
|
||||||
{
|
{
|
||||||
key: `calendar-${id}`,
|
key: "active-calendar",
|
||||||
query: {
|
query: {
|
||||||
id,
|
id,
|
||||||
full: true
|
full: true
|
||||||
@@ -32,39 +31,14 @@ const {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const isLoading = computed(() => calendarStatus.value === "pending")
|
||||||
data: categories,
|
|
||||||
status: categoriesStatus
|
|
||||||
} = await useLazyFetch<{ data: Category[] }>("/api/calendars/categories/query",
|
|
||||||
{ key: `categories-${id}` }
|
|
||||||
)
|
|
||||||
|
|
||||||
const isLoading = computed(() => calendarStatus.value === "pending" || categoriesStatus.value === "pending")
|
const { setActiveCalendar } = useCalendar()
|
||||||
|
watch([calendar], () => {
|
||||||
// Set custom menu
|
if (calendar.value?.data) {
|
||||||
// This should be reserved for actions, not for breadcrumbs
|
setActiveCalendar(calendar.value?.data)
|
||||||
//
|
}
|
||||||
// const { t } = useI18n()
|
}, { immediate: true })
|
||||||
// const { setCurrentMenu } = useUiStore()
|
|
||||||
|
|
||||||
// // Set menu once we have the calendar data
|
|
||||||
// watch(calendar, (n) => {
|
|
||||||
// if (n?.data) {
|
|
||||||
// setCurrentMenu([
|
|
||||||
// {
|
|
||||||
// tooltip: t("entity.world.backToMy"),
|
|
||||||
// to: "/my",
|
|
||||||
// phIcon: "universe",
|
|
||||||
// phIconWeight: "regular"
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// tooltip: t("entity.world.backToSingle", { world: calendar.value?.data.world?.name }),
|
|
||||||
// to: `/my/worlds/${calendar.value?.data.world?.id}`,
|
|
||||||
// phIcon: "world"
|
|
||||||
// },
|
|
||||||
// ])
|
|
||||||
// }
|
|
||||||
// }, { immediate: true })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -81,7 +55,7 @@ const isLoading = computed(() => calendarStatus.value === "pending" || categorie
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="calendar?.data && categories?.data" class="h-full w-full">
|
<div v-else-if="calendar?.data" class="h-full w-full">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>{{ calendar.data.name }}</Title>
|
<Title>{{ calendar.data.name }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
@@ -97,7 +71,7 @@ const isLoading = computed(() => calendarStatus.value === "pending" || categorie
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Calendar :calendar-data="calendar.data" :categories="categories.data" />
|
<Calendar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bodyError) {
|
if (bodyError) {
|
||||||
console.log(bodyData)
|
|
||||||
console.log(bodyError)
|
|
||||||
const error = createError({
|
const error = createError({
|
||||||
cause: "Utilisateur",
|
cause: "Utilisateur",
|
||||||
fatal: false,
|
fatal: false,
|
||||||
|
|||||||
41
server/api/calendars/categories/[id].delete.ts
Normal file
41
server/api/calendars/categories/[id].delete.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { serverSupabaseClient } from "#supabase/server"
|
||||||
|
import type { Category } from "~/models/Category"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (paramsError) {
|
||||||
|
throw createError({
|
||||||
|
cause: "Utilisateur",
|
||||||
|
fatal: false,
|
||||||
|
message: "L'identifiant de l'évènement est manquant ou mal renseigné.",
|
||||||
|
status: 401,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await client
|
||||||
|
.from("calendar_event_categories")
|
||||||
|
.delete()
|
||||||
|
.eq("id", params.id)
|
||||||
|
.maybeSingle<Category>()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
throw createError({
|
||||||
|
cause: "Serveur",
|
||||||
|
status: 500,
|
||||||
|
fatal: false,
|
||||||
|
message: "Une erreur inconnue est survenue."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
73
server/api/calendars/categories/[id].patch.ts
Normal file
73
server/api/calendars/categories/[id].patch.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { serverSupabaseClient } from "#supabase/server"
|
||||||
|
import type { Category} from "~/models/Category";
|
||||||
|
import { categorySchema } from "~/models/Category"
|
||||||
|
|
||||||
|
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 => categorySchema.safeParse(body))
|
||||||
|
|
||||||
|
if (paramsError) {
|
||||||
|
throw createError({
|
||||||
|
cause: "Utilisateur",
|
||||||
|
fatal: false,
|
||||||
|
message: "L'identifiant de la catégorie est manquant ou mal renseigné.",
|
||||||
|
status: 401,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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("calendar_event_categories")
|
||||||
|
.update(
|
||||||
|
{
|
||||||
|
name: bodyData.category.name,
|
||||||
|
color: bodyData.category.color,
|
||||||
|
calendar_id: bodyData.calendarId
|
||||||
|
} as never
|
||||||
|
)
|
||||||
|
.eq("id", params.id)
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
color
|
||||||
|
`)
|
||||||
|
.single<Category>()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
throw createError({
|
||||||
|
cause: "Serveur",
|
||||||
|
status: 500,
|
||||||
|
fatal: false,
|
||||||
|
message: "Une erreur inconnue est survenue."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
56
server/api/calendars/categories/create.post.ts
Normal file
56
server/api/calendars/categories/create.post.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { serverSupabaseClient } from "#supabase/server";
|
||||||
|
import { type Category, categorySchema } from "@/models/Category";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const client = await serverSupabaseClient(event)
|
||||||
|
|
||||||
|
const { data: bodyData, error: bodyError } = await readValidatedBody(event, body => categorySchema.safeParse(body))
|
||||||
|
|
||||||
|
if (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("calendar_event_categories")
|
||||||
|
.insert(
|
||||||
|
{
|
||||||
|
name: bodyData.category.name,
|
||||||
|
color: bodyData.category.color,
|
||||||
|
calendar_id: bodyData.calendarId
|
||||||
|
} as never
|
||||||
|
)
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
color
|
||||||
|
`)
|
||||||
|
.single<Category>()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
throw createError({
|
||||||
|
cause: "Serveur",
|
||||||
|
status: 500,
|
||||||
|
fatal: false,
|
||||||
|
message: "Une erreur inconnue est survenue."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -4,6 +4,7 @@ import type { Category } from "~/models/Category";
|
|||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
id: z.number({ coerce: true }).positive().int().optional(),
|
id: z.number({ coerce: true }).positive().int().optional(),
|
||||||
|
calendarId: z.number({ coerce: true }).positive().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
@@ -16,12 +17,17 @@ export default defineEventHandler(async (event) => {
|
|||||||
.from("calendar_event_categories")
|
.from("calendar_event_categories")
|
||||||
.select(`
|
.select(`
|
||||||
id,
|
id,
|
||||||
name
|
name,
|
||||||
|
color
|
||||||
`)
|
`)
|
||||||
|
|
||||||
if (query.id) {
|
if (query.id) {
|
||||||
return output.eq("id", query.id).limit(1).single<Category>()
|
return output.eq("id", query.id).limit(1).single<Category>()
|
||||||
}
|
}
|
||||||
|
|
||||||
return output.returns<Category[]>()
|
if (query.calendarId) {
|
||||||
|
output.eq("calendar_id", query.calendarId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.overrideTypes<Category[]>()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => postCalendarSchema.safeParse(body))
|
const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => postCalendarSchema.safeParse(body))
|
||||||
|
|
||||||
if (schemaError) {
|
if (schemaError) {
|
||||||
console.log(schemaError)
|
|
||||||
throw createError({
|
throw createError({
|
||||||
cause: "Utilisateur",
|
cause: "Utilisateur",
|
||||||
fatal: false,
|
fatal: false,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
const { data: bodyData, error: bodyError } = await readValidatedBody(event, body => calendarMonthSchema.safeParse(body))
|
const { data: bodyData, error: bodyError } = await readValidatedBody(event, body => calendarMonthSchema.safeParse(body))
|
||||||
|
|
||||||
if (paramsError) {
|
if (paramsError) {
|
||||||
console.log(paramsError)
|
|
||||||
throw createError({
|
throw createError({
|
||||||
cause: "Utilisateur",
|
cause: "Utilisateur",
|
||||||
fatal: false,
|
fatal: false,
|
||||||
@@ -23,7 +22,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bodyError) {
|
if (bodyError) {
|
||||||
console.log(bodyData)
|
|
||||||
throw createError({
|
throw createError({
|
||||||
cause: "Utilisateur",
|
cause: "Utilisateur",
|
||||||
fatal: false,
|
fatal: false,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => calendarMonthSchema.safeParse(body))
|
const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => calendarMonthSchema.safeParse(body))
|
||||||
|
|
||||||
if (schemaError) {
|
if (schemaError) {
|
||||||
console.log(schemaError)
|
|
||||||
throw createError({
|
throw createError({
|
||||||
cause: "Utilisateur",
|
cause: "Utilisateur",
|
||||||
fatal: false,
|
fatal: false,
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
category:calendar_event_categories!calendar_events_category_fkey (*),
|
category:calendar_event_categories!calendar_events_category_fkey (*),
|
||||||
secondaryCategories:calendar_event_categories!calendar_event_categories_links (*)
|
secondaryCategories:calendar_event_categories!calendar_event_categories_links (*)
|
||||||
),
|
),
|
||||||
|
categories:calendar_event_categories (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
color
|
||||||
|
),
|
||||||
eventNb:calendar_events(count),
|
eventNb:calendar_events(count),
|
||||||
world:worlds (
|
world:worlds (
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => postWorldSchema.safeParse(body))
|
const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => postWorldSchema.safeParse(body))
|
||||||
|
|
||||||
if (schemaError) {
|
if (schemaError) {
|
||||||
console.log(schemaError)
|
|
||||||
throw createError({
|
throw createError({
|
||||||
cause: "Utilisateur",
|
cause: "Utilisateur",
|
||||||
fatal: false,
|
fatal: false,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
*/
|
*/
|
||||||
const months = ref<CalendarMonth[]>([])
|
const months = ref<CalendarMonth[]>([])
|
||||||
|
|
||||||
function setActiveCalendar(calendarData: Calendar, categoryData: Category[]) {
|
function setActiveCalendar(calendarData: Calendar) {
|
||||||
try {
|
try {
|
||||||
if (!calendarData.id) return
|
if (!calendarData.id) return
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
months.value = calendarData.months
|
months.value = calendarData.months
|
||||||
|
|
||||||
baseEvents.value = calendarData.events
|
baseEvents.value = calendarData.events
|
||||||
categories.value = categoryData
|
categories.value = calendarData.categories
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
@@ -345,17 +345,17 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
function getViewTypeTitle(viewType: CalendarViewType): string {
|
function getViewTypeTitle(viewType: CalendarViewType): string {
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
case "year":
|
case "year":
|
||||||
return t("entity.calendar.years.nameSingular")
|
return t("entity.calendar.years.displayMode")
|
||||||
|
|
||||||
case "decade":
|
case "decade":
|
||||||
return "Décennie"
|
return t("entity.calendar.decades.displayMode")
|
||||||
|
|
||||||
case "century":
|
case "century":
|
||||||
return "Siècle"
|
return t("entity.calendar.centuries.displayMode")
|
||||||
|
|
||||||
case "month":
|
case "month":
|
||||||
default:
|
default:
|
||||||
return t("entity.calendar.months.nameSingular")
|
return t("entity.calendar.months.displayMode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,7 +819,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State for event modal edition
|
* State for event modal deletion
|
||||||
*/
|
*/
|
||||||
const isDeleteEventModalOpen = ref<boolean>(false)
|
const isDeleteEventModalOpen = ref<boolean>(false)
|
||||||
|
|
||||||
@@ -910,6 +910,36 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates all events categories
|
||||||
|
* This is used when the user updates a category and we want to update all events in the client
|
||||||
|
* @param categories The new categories
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
function updateAllEventsCategories(categories: Category[]) {
|
||||||
|
baseEvents.value.forEach((event) => {
|
||||||
|
const category = categories.find((c) => c.id === event.category?.id)
|
||||||
|
|
||||||
|
if (category && event.category?.id) {
|
||||||
|
event.category = category
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for categories changes
|
||||||
|
watch(categories, (n) => {
|
||||||
|
updateAllEventsCategories(n)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State for categories modal
|
||||||
|
*/
|
||||||
|
const isCategoriesModalOpen = ref<boolean>(false)
|
||||||
|
|
||||||
|
function toggleCategoriesModal(state: boolean) {
|
||||||
|
isCategoriesModalOpen.value = state
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
setReadStatus,
|
setReadStatus,
|
||||||
@@ -969,6 +999,8 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
isEditEventModalOpen,
|
isEditEventModalOpen,
|
||||||
revealEditEventModal,
|
revealEditEventModal,
|
||||||
isDeleteEventModalOpen,
|
isDeleteEventModalOpen,
|
||||||
revealDeleteEventModal
|
revealDeleteEventModal,
|
||||||
|
isCategoriesModalOpen,
|
||||||
|
toggleCategoriesModal
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
117
stores/CategoryStore.ts
Normal file
117
stores/CategoryStore.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import type { Category } from "~/models/Category";
|
||||||
|
|
||||||
|
export const useCategoryStore = defineStore("calendar-category", () => {
|
||||||
|
const { activeCalendar, categories } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dummy event to hold creation data
|
||||||
|
*/
|
||||||
|
const isCreatingCategory = ref<boolean>(false)
|
||||||
|
const isUpdatingCategory = ref<boolean>(false)
|
||||||
|
const isDeletingCategory = ref<boolean>(false)
|
||||||
|
const categorySkeleton = ref<Category | null>(null);
|
||||||
|
|
||||||
|
const operationInProgress = computed<boolean>(() => isCreatingCategory.value || isUpdatingCategory.value || isDeletingCategory.value)
|
||||||
|
let abortController: AbortController | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the dummy category data
|
||||||
|
*/
|
||||||
|
function resetSkeleton() {
|
||||||
|
categorySkeleton.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addCategoryFromSkeleton() {
|
||||||
|
if (!categorySkeleton.value) return
|
||||||
|
|
||||||
|
abortController = new AbortController()
|
||||||
|
|
||||||
|
isCreatingCategory.value = true
|
||||||
|
|
||||||
|
const { data, error } = await tryCatch(
|
||||||
|
$fetch<Category>("/api/calendars/categories/create", { method: "POST", body: { category: categorySkeleton.value, calendarId: activeCalendar.value?.id }, signal: abortController.signal })
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
isCreatingCategory.value = false
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the category in the store
|
||||||
|
categories.value.push(data)
|
||||||
|
|
||||||
|
abortController = null
|
||||||
|
isCreatingCategory.value = false
|
||||||
|
resetSkeleton()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCategoryFromSkeleton() {
|
||||||
|
if (!categorySkeleton.value) return
|
||||||
|
|
||||||
|
abortController = new AbortController()
|
||||||
|
isUpdatingCategory.value = true
|
||||||
|
|
||||||
|
const { data, error } = await tryCatch(
|
||||||
|
$fetch<Category>(`/api/calendars/categories/${categorySkeleton.value.id}`, { method: "PATCH", body: { category: categorySkeleton.value, calendarId: activeCalendar.value?.id }, signal: abortController.signal })
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
isUpdatingCategory.value = false
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the category in the store
|
||||||
|
categories.value = categories.value.map((category) => {
|
||||||
|
if (category.id === data.id) {
|
||||||
|
return { ...category, ...data }
|
||||||
|
}
|
||||||
|
return category
|
||||||
|
})
|
||||||
|
|
||||||
|
abortController = null
|
||||||
|
isUpdatingCategory.value = false
|
||||||
|
resetSkeleton()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCategoryFromSkeleton() {
|
||||||
|
if (!categorySkeleton.value) return
|
||||||
|
|
||||||
|
abortController = new AbortController()
|
||||||
|
isDeletingCategory.value = true
|
||||||
|
|
||||||
|
const { error } = await tryCatch(
|
||||||
|
$fetch<Category>(`/api/calendars/categories/${categorySkeleton.value.id}`, { method: "DELETE", signal: abortController.signal })
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
isDeletingCategory.value = false
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryIndex = categories.value.findIndex(c => c.id === categorySkeleton.value!.id)
|
||||||
|
categories.value.splice(categoryIndex, 1)
|
||||||
|
|
||||||
|
abortController = null
|
||||||
|
isDeletingCategory.value = false
|
||||||
|
resetSkeleton()
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelLatestRequest() {
|
||||||
|
if (abortController) {
|
||||||
|
abortController.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCreatingCategory,
|
||||||
|
isUpdatingCategory,
|
||||||
|
isDeletingCategory,
|
||||||
|
operationInProgress,
|
||||||
|
categorySkeleton,
|
||||||
|
resetSkeleton,
|
||||||
|
addCategoryFromSkeleton,
|
||||||
|
updateCategoryFromSkeleton,
|
||||||
|
deleteCategoryFromSkeleton,
|
||||||
|
cancelLatestRequest
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
import type { CalendarEvent } from "@/models/CalendarEvent"
|
|
||||||
import type { RPGDate } from "@/models/Date"
|
|
||||||
import { defineStore } from "pinia"
|
|
||||||
import { ref, watch, type Ref } from "vue"
|
|
||||||
import type { Category } from "~/models/Category"
|
|
||||||
import { useCalendar } from "./CalendarStore"
|
|
||||||
|
|
||||||
export const useCalendarEvents = defineStore("calendar-events", () => {
|
|
||||||
const { activeCalendar, defaultDate, currentConfig, convertDateToDays, compareDates } = useCalendar()
|
|
||||||
const { currentRPGDate } = storeToRefs(useCalendar())
|
|
||||||
|
|
||||||
const baseEvents = ref<CalendarEvent[]>([])
|
|
||||||
|
|
||||||
async function fetchCalendarEvents(calendarId: number) {
|
|
||||||
try {
|
|
||||||
const res = await $fetch("/api/calendars/events/query", { query: { calendarId } })
|
|
||||||
|
|
||||||
const eventData = res.data as CalendarEvent[]
|
|
||||||
|
|
||||||
if (!eventData.length) return
|
|
||||||
|
|
||||||
baseEvents.value = eventData
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const categories = ref<Category[]>([])
|
|
||||||
|
|
||||||
function setCategories(data: Category[]) {
|
|
||||||
categories.value = data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order base events by dates
|
|
||||||
const allEvents = computed(() => baseEvents.value.sort((a, b) => {
|
|
||||||
return compareDates(a.startDate, b.startDate, "desc")
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Gets all current event in its default state
|
|
||||||
const currentEvents: Ref<CalendarEvent[]> = ref([])
|
|
||||||
|
|
||||||
// Watch for currentDate or events' list changes
|
|
||||||
// This is deep because we're watching an array, and changes need to trigger and mutations like .push and .splice
|
|
||||||
watch([currentRPGDate, allEvents], () => {
|
|
||||||
currentEvents.value = computeCurrentEvents()
|
|
||||||
}, { deep: true, immediate: true })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the event can appear in the front end
|
|
||||||
*
|
|
||||||
* This function takes into consideration the viewType of the calendar config
|
|
||||||
*
|
|
||||||
* @param event The event to analyze
|
|
||||||
* @returns Whether the event should appear in the current view
|
|
||||||
*/
|
|
||||||
function shouldEventBeDisplayed(event: CalendarEvent): boolean {
|
|
||||||
const isEventOnCurrentScreen =
|
|
||||||
(event.startDate.year === currentRPGDate.value.day &&
|
|
||||||
event.startDate.month === currentRPGDate.value.month) ||
|
|
||||||
(event.endDate &&
|
|
||||||
event.endDate.year === currentRPGDate.value.year &&
|
|
||||||
event.endDate.month === currentRPGDate.value.month)
|
|
||||||
|
|
||||||
switch (currentConfig.viewType) {
|
|
||||||
case "month":
|
|
||||||
return isEventOnCurrentScreen!
|
|
||||||
|
|
||||||
case "year":
|
|
||||||
return event.startDate.year === currentRPGDate.value.year
|
|
||||||
|
|
||||||
case "decade":
|
|
||||||
return (
|
|
||||||
event.startDate.year >= currentRPGDate.value.year &&
|
|
||||||
event.startDate.year <= currentRPGDate.value.year + 10
|
|
||||||
)
|
|
||||||
|
|
||||||
case "century":
|
|
||||||
return (
|
|
||||||
event.startDate.year >= currentRPGDate.value.year &&
|
|
||||||
event.startDate.year <= currentRPGDate.value.year + 100
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches all the current events for the current view
|
|
||||||
*
|
|
||||||
* @returns A list of events that can appear in the current view
|
|
||||||
*/
|
|
||||||
function computeCurrentEvents(): CalendarEvent[] {
|
|
||||||
return allEvents.value.filter((event) => shouldEventBeDisplayed(event))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* From a base event, gets the next or previous one in the timeline
|
|
||||||
*
|
|
||||||
* @param event The event at a given position in the data
|
|
||||||
* @param position Whether we should get the next or previous event
|
|
||||||
* @returns The next event in chronological order
|
|
||||||
*/
|
|
||||||
function getRelativeEventFromEvent(
|
|
||||||
event: CalendarEvent,
|
|
||||||
position: "next" | "prev" = "next",
|
|
||||||
initialIsEnd: boolean = false
|
|
||||||
): { event: CalendarEvent; targetDate: RPGDate } {
|
|
||||||
let dateToParse: RPGDate // Day value of the date that the user interacted with
|
|
||||||
|
|
||||||
if (initialIsEnd && event.endDate) {
|
|
||||||
dateToParse = event.endDate
|
|
||||||
} else {
|
|
||||||
dateToParse = event.startDate
|
|
||||||
}
|
|
||||||
|
|
||||||
return getRelativeEventFromDate(dateToParse, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* From a date, gets the next or previous event in the timeline
|
|
||||||
*
|
|
||||||
* @param date The starting date from which to get the next event
|
|
||||||
* @param position Whether we should get the next or previous event
|
|
||||||
* @returns The next event in chronological order
|
|
||||||
*/
|
|
||||||
function getRelativeEventFromDate(
|
|
||||||
date: RPGDate,
|
|
||||||
position: "next" | "prev" = "next"
|
|
||||||
): { event: CalendarEvent; targetDate: RPGDate } {
|
|
||||||
const pivotValue = convertDateToDays(date)
|
|
||||||
let t: { eventData: CalendarEvent; distance: number; targetKey: "startDate" | "endDate" }[] = []
|
|
||||||
|
|
||||||
// Loop over all event once to convert the structure to a usable one
|
|
||||||
for (let i = 0; i < allEvents.value.length; i++) {
|
|
||||||
const e: CalendarEvent = allEvents.value[i]
|
|
||||||
|
|
||||||
// Estimate distance from pivot
|
|
||||||
const startDateDays: number = convertDateToDays(e.startDate)
|
|
||||||
const startDistance: number = startDateDays - pivotValue
|
|
||||||
|
|
||||||
// Push startDate to comparator array
|
|
||||||
t.push({
|
|
||||||
eventData: e,
|
|
||||||
distance: startDistance,
|
|
||||||
targetKey: "startDate"
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check the same things for endDate
|
|
||||||
if (e.endDate) {
|
|
||||||
const endDateDays: number = convertDateToDays(e.endDate)
|
|
||||||
const endDistance: number = endDateDays - pivotValue
|
|
||||||
|
|
||||||
// Push optional endDate to comparator array
|
|
||||||
t.push({
|
|
||||||
eventData: e,
|
|
||||||
distance: endDistance,
|
|
||||||
targetKey: "endDate"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Based on the direction, either ignore negative distance (past) or positive distance (future)
|
|
||||||
t = t.filter((i) => {
|
|
||||||
return position === "next" ? i.distance > 0 : i.distance < 0
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!t.length) {
|
|
||||||
throw new Error(
|
|
||||||
"Aucun évènement suivant ou précédent trouvé ; Peut-être l'évènement se situe au début ou à la fin du calendrier ?"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get event with remaining minimum distance
|
|
||||||
const closestEvent = t.reduce((a, b) => {
|
|
||||||
return Math.abs(b.distance) < Math.abs(a.distance) ? b : a
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
event: closestEvent.eventData,
|
|
||||||
targetDate: closestEvent.eventData[closestEvent.targetKey]!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State for event modal edition
|
|
||||||
*/
|
|
||||||
const isEditEventModalOpen: Ref<boolean> = ref<boolean>(false)
|
|
||||||
|
|
||||||
function revealEditEventModal() {
|
|
||||||
isEditEventModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State for event modal edition
|
|
||||||
*/
|
|
||||||
const isDeleteEventModalOpen: Ref<boolean> = ref<boolean>(false)
|
|
||||||
|
|
||||||
function revealDeleteEventModal() {
|
|
||||||
isDeleteEventModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EVENT CREATION FUNCTIONS
|
|
||||||
*/
|
|
||||||
const lastActiveEvent = ref<CalendarEvent | null>()
|
|
||||||
const isCreatingEvent = ref<boolean>(false)
|
|
||||||
const isUpdatingEvent = ref<boolean>(false)
|
|
||||||
const isDeletingEvent = ref<boolean>(false)
|
|
||||||
const operationInProgress = computed(() => isCreatingEvent.value || isUpdatingEvent.value || isDeletingEvent.value)
|
|
||||||
let abortController: AbortController | null = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dummy event to hold creation data
|
|
||||||
*/
|
|
||||||
const eventSkeleton: Ref<CalendarEvent> = ref<CalendarEvent>({ title: "", startDate: defaultDate })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the dummy event data
|
|
||||||
*/
|
|
||||||
function resetSkeleton() {
|
|
||||||
eventSkeleton.value = { title: "", startDate: defaultDate }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submits the skeleton event and creates a real event from its data
|
|
||||||
*
|
|
||||||
* We assume it's been sanitized by the caller
|
|
||||||
*/
|
|
||||||
async function submitSkeleton() {
|
|
||||||
abortController = new AbortController()
|
|
||||||
isCreatingEvent.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await $fetch("/api/calendars/events/create", { method: "POST", body: { event : eventSkeleton.value, calendarId: activeCalendar?.id }, signal: abortController.signal })
|
|
||||||
|
|
||||||
baseEvents.value.push(res)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
} finally {
|
|
||||||
abortController = null
|
|
||||||
isCreatingEvent.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateEventFromSkeleton() {
|
|
||||||
abortController = new AbortController()
|
|
||||||
isUpdatingEvent.value = true
|
|
||||||
|
|
||||||
const res = await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: "PATCH", body: { event : eventSkeleton.value, calendarId: activeCalendar?.id }, signal: abortController.signal })
|
|
||||||
|
|
||||||
const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
|
|
||||||
baseEvents.value[eventIndex] = res
|
|
||||||
|
|
||||||
abortController = null
|
|
||||||
isUpdatingEvent.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteEventFromSkeleton() {
|
|
||||||
abortController = new AbortController()
|
|
||||||
isDeletingEvent.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: "DELETE", signal: abortController.signal })
|
|
||||||
|
|
||||||
const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
|
|
||||||
baseEvents.value.splice(eventIndex, 1)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
} finally {
|
|
||||||
abortController = null
|
|
||||||
isDeletingEvent.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelLatestRequest() {
|
|
||||||
if (abortController) {
|
|
||||||
abortController.abort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allEvents,
|
|
||||||
fetchCalendarEvents,
|
|
||||||
categories,
|
|
||||||
setCategories,
|
|
||||||
currentEvents,
|
|
||||||
getRelativeEventFromDate,
|
|
||||||
getRelativeEventFromEvent,
|
|
||||||
cancelLatestRequest,
|
|
||||||
isCreatingEvent,
|
|
||||||
isUpdatingEvent,
|
|
||||||
isDeletingEvent,
|
|
||||||
operationInProgress,
|
|
||||||
eventSkeleton,
|
|
||||||
resetSkeleton,
|
|
||||||
submitSkeleton,
|
|
||||||
lastActiveEvent,
|
|
||||||
updateEventFromSkeleton,
|
|
||||||
deleteEventFromSkeleton,
|
|
||||||
isEditEventModalOpen,
|
|
||||||
revealEditEventModal,
|
|
||||||
isDeleteEventModalOpen,
|
|
||||||
revealDeleteEventModal
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -14,40 +14,40 @@ create type public.calendar_state as enum ('published', 'draft', 'archived');
|
|||||||
-- DATA STRUCTURES
|
-- DATA STRUCTURES
|
||||||
-- Users
|
-- Users
|
||||||
create table public.users (
|
create table public.users (
|
||||||
id uuid references auth.users not null primary key, -- UUID from auth.users
|
id uuid references auth.users not null primary key, -- UUID from auth.users
|
||||||
username text
|
username text
|
||||||
);
|
);
|
||||||
comment on table public.users is 'Profile data for each user.';
|
comment on table public.users is 'Profile data for each user.';
|
||||||
comment on column public.users.id is 'References the internal Supabase Auth user.';
|
comment on column public.users.id is 'References the internal Supabase Auth user.';
|
||||||
|
|
||||||
-- Roles
|
-- Roles
|
||||||
create table public.user_roles (
|
create table public.user_roles (
|
||||||
id bigint generated by default as identity primary key,
|
id bigint generated by default as identity primary key,
|
||||||
role app_role not null,
|
role app_role not null,
|
||||||
user_id uuid references public.users on delete cascade not null,
|
user_id uuid references public.users on delete cascade not null,
|
||||||
unique (user_id, role)
|
unique (user_id, role)
|
||||||
);
|
);
|
||||||
comment on table public.user_roles is 'Application roles for each user.';
|
comment on table public.user_roles is 'Application roles for each user.';
|
||||||
|
|
||||||
-- Permissions
|
-- Permissions
|
||||||
create table public.role_permissions (
|
create table public.role_permissions (
|
||||||
id bigint generated by default as identity primary key,
|
id bigint generated by default as identity primary key,
|
||||||
role app_role not null,
|
role app_role not null,
|
||||||
permission app_permission not null,
|
permission app_permission not null,
|
||||||
unique (role, permission)
|
unique (role, permission)
|
||||||
);
|
);
|
||||||
comment on table public.role_permissions is 'Application permissions for each role.';
|
comment on table public.role_permissions is 'Application permissions for each role.';
|
||||||
|
|
||||||
-- Worlds
|
-- Worlds
|
||||||
create table public.worlds (
|
create table public.worlds (
|
||||||
id bigint generated by default as identity primary key,
|
id bigint generated by default as identity primary key,
|
||||||
name text not null,
|
name text not null,
|
||||||
description text,
|
description text,
|
||||||
color app_colors default 'black',
|
color app_colors default 'black',
|
||||||
state world_state default 'draft',
|
state world_state default 'draft',
|
||||||
created_at timestamptz default now(),
|
created_at timestamptz default now(),
|
||||||
updated_at timestamptz,
|
updated_at timestamptz,
|
||||||
gm_id uuid references public.users on delete cascade
|
gm_id uuid references public.users on delete cascade
|
||||||
);
|
);
|
||||||
comment on table public.worlds is 'Worlds belonging to a single user ; a game master.';
|
comment on table public.worlds is 'Worlds belonging to a single user ; a game master.';
|
||||||
create trigger handle_updated_at before update on public.worlds
|
create trigger handle_updated_at before update on public.worlds
|
||||||
@@ -55,9 +55,9 @@ create trigger handle_updated_at before update on public.worlds
|
|||||||
|
|
||||||
-- World Players (join table)
|
-- World Players (join table)
|
||||||
create table public.world_players (
|
create table public.world_players (
|
||||||
world_id bigint references public.worlds on delete cascade,
|
world_id bigint references public.worlds on delete cascade,
|
||||||
player_id uuid references public.users on delete cascade,
|
player_id uuid references public.users on delete cascade,
|
||||||
joined_at timestamptz default now(),
|
joined_at timestamptz default now(),
|
||||||
primary key (world_id, player_id)
|
primary key (world_id, player_id)
|
||||||
);
|
);
|
||||||
comment on table public.world_players is 'Players (users) that belong to specific worlds';
|
comment on table public.world_players is 'Players (users) that belong to specific worlds';
|
||||||
@@ -91,27 +91,27 @@ comment on table public.calendar_months is 'A calendar month.';
|
|||||||
|
|
||||||
-- Calendar Event categories
|
-- Calendar Event categories
|
||||||
create table public.calendar_event_categories (
|
create table public.calendar_event_categories (
|
||||||
id bigint generated by default as identity primary key,
|
id bigint generated by default as identity primary key,
|
||||||
name text not null,
|
name text not null,
|
||||||
color app_colors default 'black',
|
color app_colors default 'white',
|
||||||
unique (name)
|
calendar_id bigint references public.calendars on delete cascade not null
|
||||||
);
|
);
|
||||||
comment on table public.calendar_event_categories is 'Categories describing events.';
|
comment on table public.calendar_event_categories is 'Categories describing events.';
|
||||||
|
|
||||||
-- Calendar Events
|
-- Calendar Events
|
||||||
create table public.calendar_events (
|
create table public.calendar_events (
|
||||||
id bigint generated by default as identity primary key,
|
id bigint generated by default as identity primary key,
|
||||||
title text not null,
|
title text not null,
|
||||||
description text,
|
description text,
|
||||||
location text,
|
location text,
|
||||||
start_date json not null,
|
start_date json not null,
|
||||||
end_date json,
|
end_date json,
|
||||||
category bigint references public.calendar_event_categories on delete cascade,
|
category bigint references public.calendar_event_categories on delete set null,
|
||||||
hidden boolean default false,
|
hidden boolean default false,
|
||||||
wiki text,
|
wiki text,
|
||||||
created_at timestamptz default now(),
|
created_at timestamptz default now(),
|
||||||
updated_at timestamptz,
|
updated_at timestamptz,
|
||||||
calendar_id bigint references public.calendars on delete cascade not null
|
calendar_id bigint references public.calendars on delete cascade not null
|
||||||
);
|
);
|
||||||
comment on table public.calendar_events is 'Events linked to a world';
|
comment on table public.calendar_events is 'Events linked to a world';
|
||||||
|
|
||||||
@@ -134,12 +134,12 @@ comment on table public.calendar_event_categories_links is 'Link tables for mult
|
|||||||
|
|
||||||
-- Character categories
|
-- Character categories
|
||||||
create table public.character_categories (
|
create table public.character_categories (
|
||||||
id bigint generated by default as identity primary key,
|
id bigint generated by default as identity primary key,
|
||||||
name text not null,
|
name text not null,
|
||||||
color app_colors default 'black',
|
color app_colors default 'white',
|
||||||
created_at timestamptz default now(),
|
created_at timestamptz default now(),
|
||||||
updated_at timestamptz,
|
updated_at timestamptz,
|
||||||
unique (name)
|
world_id bigint references public.worlds on delete cascade not null
|
||||||
);
|
);
|
||||||
comment on table public.character_categories is 'Categories describing characters';
|
comment on table public.character_categories is 'Categories describing characters';
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ create table public.characters (
|
|||||||
description text,
|
description text,
|
||||||
birth json not null,
|
birth json not null,
|
||||||
death json,
|
death json,
|
||||||
category bigint references public.character_categories on delete cascade,
|
category bigint references public.character_categories on delete set null,
|
||||||
hidden_birth boolean,
|
hidden_birth boolean,
|
||||||
hidden_death boolean,
|
hidden_death boolean,
|
||||||
wiki text,
|
wiki text,
|
||||||
@@ -335,7 +335,7 @@ 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 anon access to non-hidden events in published calendars" on public.calendar_events
|
||||||
for select
|
for select
|
||||||
to authenticated, anon
|
to authenticated, anon
|
||||||
using (
|
using (
|
||||||
@@ -420,12 +420,67 @@ create policy "Allow individual update access for GMs" on public.characters for
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Categories are public to view but not to insert
|
-- Categories policies
|
||||||
-- Needs to be refactored maybe, if in the future we want a default set AND user defined ones
|
create policy "Allow anonymous access to published event categories" on public.calendar_event_categories
|
||||||
create policy "Allow all read access" on public.calendar_event_categories for select to authenticated, anon using ( true );
|
for select
|
||||||
create policy "Allow all read access" on public.calendar_event_categories_links for select to authenticated, anon using ( true );
|
to authenticated, anon
|
||||||
create policy "Allow logged-in read access" on public.character_categories for select using ( auth.role() = 'authenticated' );
|
using (
|
||||||
create policy "Allow logged-in read access" on public.character_categories_links for select using ( auth.role() = 'authenticated' );
|
exists (
|
||||||
|
select 1
|
||||||
|
from public.calendars c
|
||||||
|
join public.calendar_event_categories_links l on l.calendar_event_category_id = calendar_event_categories.id
|
||||||
|
where c.id = calendar_event_categories.calendar_id
|
||||||
|
and c.state = 'published'
|
||||||
|
)
|
||||||
|
or exists (
|
||||||
|
select 1
|
||||||
|
from public.calendars c
|
||||||
|
where c.id = calendar_event_categories.calendar_id
|
||||||
|
and c.state = 'published'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
create policy "Allow GMs to add new events categories"
|
||||||
|
on public.calendar_event_categories
|
||||||
|
for insert
|
||||||
|
with check (
|
||||||
|
exists (
|
||||||
|
select 1
|
||||||
|
from public.calendars c
|
||||||
|
join public.worlds w on w.id = c.world_id
|
||||||
|
where
|
||||||
|
c.id = calendar_event_categories.calendar_id
|
||||||
|
and w.gm_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
create policy "Allow GMs to update their events categories"
|
||||||
|
on public.calendar_event_categories
|
||||||
|
for update
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1
|
||||||
|
from public.calendars c
|
||||||
|
join public.worlds w on w.id = c.world_id
|
||||||
|
where
|
||||||
|
c.id = calendar_event_categories.calendar_id
|
||||||
|
and w.gm_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
create policy "Allow GMs to delete their events categories"
|
||||||
|
on public.calendar_event_categories
|
||||||
|
for delete
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1
|
||||||
|
from public.calendars c
|
||||||
|
join public.worlds w on w.id = c.world_id
|
||||||
|
where
|
||||||
|
c.id = calendar_event_categories.calendar_id
|
||||||
|
and w.gm_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
-- Send "previous data" on change
|
-- Send "previous data" on change
|
||||||
alter table public.users replica identity full;
|
alter table public.users replica identity full;
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
insert into public.role_permissions (role, permission) values ('sa', 'events.see.hidden');
|
insert into public.role_permissions (role, permission) values ('sa', 'events.see.hidden');
|
||||||
insert into public.role_permissions (role, permission) values ('sa', 'users.ban');
|
insert into public.role_permissions (role, permission) values ('sa', 'users.ban');
|
||||||
|
|
||||||
-- Event categories
|
-- Worlds
|
||||||
insert into public.calendar_event_categories (name, color) values ('Naissance', 'white');
|
insert into public.worlds (name, description, color, state) values ('Léïm', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis aliquet congue aliquet. Curabitur eu iaculis diam. Nunc iaculis nibh orci, eu semper nunc congue congue. Praesent euismod tortor eget metus tristique lobortis vel in risus. In volutpat ligula orci, id pharetra lectus egestas at.', 'white', 'published');
|
||||||
insert into public.calendar_event_categories (name, color) values ('Mort', 'black');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Catastrophe', 'orange');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Catastrophe naturelle', 'red');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Inauguration', 'green');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Religion', 'violet');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Invention', 'teal');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Science', 'indigo');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Bénédiction', 'white');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Joueurs', 'white');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Découverte', 'purple');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Exploration', 'lime');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Construction', 'blue');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Arcanologie', 'cyan');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Criminalité', 'rose');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Scandale', 'pink');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Commerce', 'amber');
|
|
||||||
insert into public.calendar_event_categories (name, color) values ('Législation', 'blue');
|
|
||||||
|
|
||||||
-- Character categories
|
-- Character categories
|
||||||
insert into public.character_categories (name, color) values ('Joueur', 'white');
|
insert into public.character_categories (name, color, world_id) values ('Joueur', 'white', 1);
|
||||||
insert into public.character_categories (name, color) values ('Comte', 'emerald');
|
insert into public.character_categories (name, color, world_id) values ('Comte', 'emerald', 1);
|
||||||
insert into public.character_categories (name, color) values ('Scientifique', 'indigo');
|
insert into public.character_categories (name, color, world_id) values ('Scientifique', 'indigo', 1);
|
||||||
insert into public.character_categories (name, color) values ('Mage', 'cyan');
|
insert into public.character_categories (name, color, world_id) values ('Mage', 'cyan', 1);
|
||||||
insert into public.character_categories (name, color) values ('Professeur', 'teal');
|
insert into public.character_categories (name, color, world_id) values ('Professeur', 'teal', 1);
|
||||||
insert into public.character_categories (name, color) values ('Criminel', 'rose');
|
insert into public.character_categories (name, color, world_id) values ('Criminel', 'rose', 1);
|
||||||
insert into public.character_categories (name, color) values ('Étincelle', 'lime');
|
insert into public.character_categories (name, color, world_id) values ('Étincelle', 'lime', 1);
|
||||||
insert into public.character_categories (name, color) values ('Buse blanche', 'yellow');
|
insert into public.character_categories (name, color, world_id) values ('Buse blanche', 'yellow', 1);
|
||||||
insert into public.character_categories (name, color) values ('Ecclésiastique', 'violet');
|
insert into public.character_categories (name, color, world_id) values ('Ecclésiastique', 'violet', 1);
|
||||||
insert into public.character_categories (name, color) values ('Militaire', 'orange');
|
insert into public.character_categories (name, color, world_id) values ('Militaire', 'orange', 1);
|
||||||
insert into public.character_categories (name, color) values ('Activiste', 'sky');
|
insert into public.character_categories (name, color, world_id) values ('Activiste', 'sky', 1);
|
||||||
insert into public.character_categories (name, color) values ('Commerçant', 'amber');
|
insert into public.character_categories (name, color, world_id) values ('Commerçant', 'amber', 1);
|
||||||
|
|
||||||
-- Worlds
|
|
||||||
insert into public.worlds (name, description, color, state) values ('Léïm', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis aliquet congue aliquet. Curabitur eu iaculis diam. Nunc iaculis nibh orci, eu semper nunc congue congue. Praesent euismod tortor eget metus tristique lobortis vel in risus. In volutpat ligula orci, id pharetra lectus egestas at.', 'black', 'published');
|
|
||||||
|
|
||||||
-- Worlds' calendars
|
-- Worlds' calendars
|
||||||
insert into public.calendars (world_id, name, today, state) values (1, 'Calendrier solaire', '{ "day": 23, "month": 8, "year": 3209 }', 'published');
|
insert into public.calendars (world_id, name, today, color, state) values (1, 'Calendrier solaire', '{ "day": 23, "month": 8, "year": 3209 }', 'white', 'published');
|
||||||
|
|
||||||
-- Calendar's months
|
-- Calendar's months
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Jalen', 32, 1);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Jalen', 32, 1, 1);
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Malsen', 32, 2);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Malsen', 32, 2, 1);
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Verlys', 32, 3);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Verlys', 32, 3, 1);
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Nalys', 32, 4);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Nalys', 32, 4, 1);
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Verdore', 32, 5);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Verdore', 32, 5, 1);
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Sidore', 32, 6);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Sidore', 32, 6, 1);
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Lyllion', 32, 7);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Lyllion', 32, 7, 1);
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Rion', 32, 8);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Rion', 32, 8, 1);
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Farene', 32, 9);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Farene', 32, 9, 1);
|
||||||
insert into public.calendar_months (calendar_id, name, days, position) values (1, 'Dalvene', 32, 10);
|
insert into public.calendar_months (name, days, position, calendar_id) values ('Dalvene', 32, 10, 1);
|
||||||
|
|
||||||
|
-- Event categories
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Naissance', 'white', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Mort', 'black', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Catastrophe', 'orange', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Catastrophe naturelle', 'red', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Inauguration', 'green', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Religion', 'violet', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Invention', 'teal', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Science', 'indigo', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Bénédiction', 'white', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Joueurs', 'white', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Découverte', 'purple', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Exploration', 'lime', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Construction', 'blue', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Magie', 'cyan', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Criminalité', 'rose', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Scandale', 'pink', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Commerce', 'amber', 1);
|
||||||
|
insert into public.calendar_event_categories (name, color, calendar_id) values ('Législation', 'blue', 1);
|
||||||
|
|
||||||
-- Events
|
-- Events
|
||||||
insert into public.calendar_events (title, description, start_date, category, hidden, wiki, calendar_id) values (
|
insert into public.calendar_events (title, description, start_date, category, hidden, wiki, calendar_id) values (
|
||||||
@@ -203,7 +203,7 @@ insert into public.calendar_events (title, description, start_date, end_date, ca
|
|||||||
'Celui qu''on surnomme la Bête d''Ambrose arrive à Handany, où il purgera sa peine.',
|
'Celui qu''on surnomme la Bête d''Ambrose arrive à Handany, où il purgera sa peine.',
|
||||||
'{ "day": 14, "month": 7, "year": 3209 }',
|
'{ "day": 14, "month": 7, "year": 3209 }',
|
||||||
null,
|
null,
|
||||||
18,
|
15,
|
||||||
false,
|
false,
|
||||||
'https://alexcreates.fr/leim/index.php/Tivian_Rodhus',
|
'https://alexcreates.fr/leim/index.php/Tivian_Rodhus',
|
||||||
1
|
1
|
||||||
@@ -221,7 +221,7 @@ insert into public.calendar_events (title, description, location, start_date, en
|
|||||||
'Tourgrise',
|
'Tourgrise',
|
||||||
'{ "day": 4, "month": 8, "year": 3209 }',
|
'{ "day": 4, "month": 8, "year": 3209 }',
|
||||||
null,
|
null,
|
||||||
18,
|
15,
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
1
|
1
|
||||||
@@ -245,7 +245,7 @@ insert into public.calendar_events (title, description, location, start_date, en
|
|||||||
'{ "day": 28, "month": 7, "year": 3209 }',
|
'{ "day": 28, "month": 7, "year": 3209 }',
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
true,
|
false,
|
||||||
null,
|
null,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
@@ -256,7 +256,7 @@ insert into public.calendar_events (title, description, location, start_date, en
|
|||||||
'{ "day": 32, "month": 7, "year": 3209 }',
|
'{ "day": 32, "month": 7, "year": 3209 }',
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
true,
|
false,
|
||||||
null,
|
null,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
@@ -267,7 +267,7 @@ insert into public.calendar_events (title, description, location, start_date, en
|
|||||||
'{ "day": 10, "month": 8, "year": 3209 }',
|
'{ "day": 10, "month": 8, "year": 3209 }',
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
true,
|
false,
|
||||||
null,
|
null,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
@@ -278,7 +278,7 @@ insert into public.calendar_events (title, description, location, start_date, en
|
|||||||
'{ "day": 19, "month": 8, "year": 3209 }',
|
'{ "day": 19, "month": 8, "year": 3209 }',
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
true,
|
false,
|
||||||
null,
|
null,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
@@ -289,7 +289,7 @@ insert into public.calendar_events (title, description, location, start_date, en
|
|||||||
'{ "day": 22, "month": 8, "year": 3209 }',
|
'{ "day": 22, "month": 8, "year": 3209 }',
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
true,
|
false,
|
||||||
null,
|
null,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|||||||
31
utils/TryCatch.ts
Normal file
31
utils/TryCatch.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Types for the result object with discriminated union
|
||||||
|
type Success<T> = {
|
||||||
|
data: T;
|
||||||
|
error: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Failure<E> = {
|
||||||
|
data: null;
|
||||||
|
error: E;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T, E = Error> = Success<T> | Failure<E>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility function to handle promises with try-catch
|
||||||
|
* and return a result object, courtesy of t3.
|
||||||
|
*
|
||||||
|
* @param promise The promise to be executed
|
||||||
|
* @returns A promise that resolves to an object with either the data or the error
|
||||||
|
* @link https://gist.github.com/t3dotgg/a486c4ae66d32bf17c09c73609dacc5b
|
||||||
|
*/
|
||||||
|
export async function tryCatch<T, E = Error>(
|
||||||
|
promise: Promise<T>,
|
||||||
|
): Promise<Result<T, E>> {
|
||||||
|
try {
|
||||||
|
const data = await promise;
|
||||||
|
return { data, error: null };
|
||||||
|
} catch (error) {
|
||||||
|
return { data: null, error: error as E };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user