Merge pull request #24 from AlexisNP/features/add-categories-management-to-event-cr

Add categories to event
This commit is contained in:
AlexisNP
2024-06-11 22:36:13 +02:00
committed by GitHub
20 changed files with 498 additions and 199 deletions

View File

@@ -77,6 +77,134 @@
} }
} }
/* Colors */
.event-red {
@apply text-slate-900 bg-red-500 hover:bg-red-600
}
.event-orange {
@apply text-slate-900 bg-orange-500 hover:bg-orange-600
}
.event-amber {
@apply text-slate-900 bg-amber-500 hover:bg-amber-600
}
.event-yellow {
@apply text-slate-900 bg-yellow-600 hover:bg-yellow-700
}
.event-lime {
@apply text-slate-900 bg-lime-500 hover:bg-lime-600
}
.event-green {
@apply text-white bg-green-600 hover:bg-green-700
}
.event-emerald {
@apply text-white bg-emerald-600 hover:bg-emerald-700
}
.event-teal {
@apply text-white bg-teal-600 hover:bg-teal-700
}
.event-cyan {
@apply text-white bg-cyan-600 hover:bg-cyan-700
}
.event-sky {
@apply text-white bg-sky-600 hover:bg-sky-700
}
.event-blue {
@apply text-white bg-blue-600 hover:bg-blue-700
}
.event-indigo {
@apply text-white bg-indigo-600 hover:bg-indigo-700
}
.event-violet {
@apply text-white bg-violet-600 hover:bg-violet-700
}
.event-purple {
@apply text-white bg-purple-600 hover:bg-purple-700
}
.event-fuchsia {
@apply text-white bg-fuchsia-600 hover:bg-fuchsia-700
}
.event-pink {
@apply text-white bg-pink-700 hover:bg-pink-800
}
.event-rose {
@apply text-white bg-rose-700 hover:bg-rose-800
}
.event-black {
@apply text-white bg-stone-500 hover:bg-stone-700
}
.event-white {
@apply text-slate-900 bg-white hover:bg-yellow-200
}
.event-details-red {
--base-color: var(--color-red-800);
}
.event-details-orange {
--base-color: var(--color-orange-800);
}
.event-details-amber {
--base-color: var(--color-amber-800);
}
.event-details-yellow {
--base-color: var(--color-yellow-800);
}
.event-details-lime {
--base-color: var(--color-lime-800);
}
.event-details-green {
--base-color: var(--color-green-800);
}
.event-details-emerald {
--base-color: var(--color-emerald-800);
}
.event-details-teal {
--base-color: var(--color-teal-800);
}
.event-details-cyan {
--base-color: var(--color-cyan-800);
}
.event-details-sky {
--base-color: var(--color-sky-800);
}
.event-details-blue {
--base-color: var(--color-blue-800);
}
.event-details-indigo {
--base-color: var(--color-indigo-800);
}
.event-details-violet {
--base-color: var(--color-violet-800);
}
.event-details-purple {
--base-color: var(--color-purple-800);
}
.event-details-fuchsia {
--base-color: var(--color-fuchsia-800);
}
.event-details-pink {
--base-color: var(--color-pink-800);
}
.event-details-rose {
--base-color: var(--color-rose-800);
}
.event-details-black {
--base-color: var(--color-stone-800);
}
.event-details-white {
--base-color: var(--color-white);
}
.event-details {
--bg-color: color-mix(in srgb, var(--base-color), var(--color-slate-950) 80%);
--border-color: color-mix(in srgb, var(--base-color), var(--color-slate-800) 50%);
background-color: var(--bg-color);
border-color: var(--border-color);
hr {
border-color: var(--border-color);
}
}
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: all .5s ease; transition: all .5s ease;

View File

@@ -12,11 +12,12 @@ const route = useRoute()
const worldId = route.params.id const worldId = route.params.id
const { setCalendarId, setMonths, setDefaultDate, currentConfig, selectedDate, jumpToDate } = useCalendar() const { setCalendarId, setMonths, setDefaultDate, currentConfig, selectedDate, jumpToDate } = useCalendar()
const { setEvents } = useCalendarEvents() const { setEvents, setCategories } = useCalendarEvents()
const { setCharacters } = useCharacters() const { setCharacters } = useCharacters()
const { data: calendar, pending: calPending, refresh: calRefresh } = await useLazyFetch(`/api/calendars/query?world_id=${worldId}`) const { data: calendar, pending: calPending, refresh: calRefresh } = await useLazyFetch(`/api/calendars/query?world_id=${worldId}`)
const { data: characters, pending: charPending, refresh: charRefresh } = await useLazyFetch(`/api/characters/query?world_id=${worldId}`) const { data: characters, pending: charPending, refresh: charRefresh } = await useLazyFetch(`/api/characters/query?world_id=${worldId}`)
const { data: categories, pending: categoryPending, refresh: categoryRefresh } = await useLazyFetch(`/api/calendars/categories/query`)
if (!calendar.value) { if (!calendar.value) {
await calRefresh() await calRefresh()
@@ -37,6 +38,15 @@ if (!calendar.value) {
setEvents(calendar.value?.data?.events) setEvents(calendar.value?.data?.events)
} }
} }
if (!categories.value) {
await categoryRefresh()
} else {
if (categories.value?.data) {
setCategories(categories.value?.data)
}
}
if (!characters.value) { if (!characters.value) {
await charRefresh() await charRefresh()
} else { } else {
@@ -61,6 +71,13 @@ watch(calPending, (newStatus) => {
} }
} }
}) })
watch(categoryPending, (newStatus) => {
if (!newStatus) {
if (categories.value?.data) {
setCategories(categories.value?.data)
}
}
})
watch(charPending, (newStatus) => { watch(charPending, (newStatus) => {
if (!newStatus) { if (!newStatus) {
if (characters.value?.data) { if (characters.value?.data) {
@@ -103,7 +120,7 @@ onMounted(() => {
<template> <template>
<div class="h-full"> <div class="h-full">
<template v-if="calPending || charPending"> <template v-if="calPending || charPending || categoryPending">
<div class="h-full grid place-items-center"> <div class="h-full grid place-items-center">
Loading notamment Loading notamment
</div> </div>

View File

@@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { RPGDate } from '@/models/Date' import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '~/models/CalendarEvent' import type { CalendarEvent } from '~/models/CalendarEvent'
@@ -13,9 +12,9 @@ const { areDatesIdentical } = useCalendar()
const { revealEditEventModal, revealDeleteEventModal } = useCalendarEvents() const { revealEditEventModal, revealDeleteEventModal } = useCalendarEvents()
const { lastActiveEvent } = storeToRefs(useCalendarEvents()) const { lastActiveEvent } = storeToRefs(useCalendarEvents())
const spansMultipleDays = Boolean(props.event.startDate && props.event.endDate) const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate))
const isStartEvent = spansMultipleDays && areDatesIdentical(props.tileDate, props.event.startDate) const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate))
const isEndEvent = spansMultipleDays && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate) const isEndEvent = computed(() => spansMultipleDays.value && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate))
const titleCharLimit = 50; const titleCharLimit = 50;
const eventTitle = computed<string>(() => props.event.title.length <= titleCharLimit ? props.event.title : `${props.event.title.slice(0, titleCharLimit)}`) const eventTitle = computed<string>(() => props.event.title.length <= titleCharLimit ? props.event.title : `${props.event.title.slice(0, titleCharLimit)}`)
@@ -59,30 +58,14 @@ onMounted(() => {
<button <button
class="text-xs px-2 py-1 block w-full text-left rounded-sm focus-visible:bg-red-200" class="text-xs px-2 py-1 block w-full text-left rounded-sm focus-visible:bg-red-200"
:class=" :class="
cn({ cn(
event.category ? `event-${event.category.color}` : '',
{
'italic': event.hidden, 'italic': event.hidden,
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
'text-white bg-lime-600 hover:bg-lime-700': event.category?.name === 'naissance',
'text-white bg-stone-500 hover:bg-stone-700': event.category?.name === 'mort',
'text-white bg-orange-600 hover:bg-orange-700': event.category?.name === 'catastrophe',
'text-white bg-pink-600 hover:bg-pink-700': event.category?.name === 'catastrophe naturelle',
'text-white bg-sky-600 hover:bg-sky-700': event.category?.name === 'législation',
'text-white bg-purple-600 hover:bg-purple-700': event.category?.name === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': event.category?.name === 'joueurs',
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category?.name === 'inauguration',
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category?.name === 'invention',
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category?.name === 'science',
'text-slate-900 bg-white hover:bg-yellow-200': event.category?.name === 'bénédiction',
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category?.name === 'découverte',
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category?.name === 'exploration',
'text-white bg-amber-600 hover:bg-amber-700': event.category?.name === 'construction',
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category?.name === 'arcanologie',
'text-white bg-rose-600 hover:bg-rose-700': event.category?.name === 'criminalité',
'text-white bg-stone-600 hover:bg-stone-700': event.category?.name === 'scandale',
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category?.name === 'commerce',
'rounded-r-none': isStartEvent, 'rounded-r-none': isStartEvent,
'rounded-l-none': isEndEvent 'rounded-l-none': isEndEvent,
}) }
)
" "
@dblclick="handleDoubleClick" @dblclick="handleDoubleClick"
@keydown.delete="handleDelete" @keydown.delete="handleDelete"

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from '@/lib/utils'
import type { RPGDate } from '@/models/Date' import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/CalendarEvent' import type { CalendarEvent } from '@/models/CalendarEvent'
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from '@/stores/CalendarStore'
@@ -62,33 +63,18 @@ function deployDeleteModal() {
<template> <template>
<UiPopoverContent <UiPopoverContent
class="event-details w-96 bg-slate-900" class=" w-96 bg-slate-900 border-slate-800"
:align="'center'" :align="'center'"
:align-offset="50" :align-offset="50"
:side="'left'" :side="'left'"
:collision-padding="60" :collision-padding="60"
:hide-when-detached="true" :hide-when-detached="true"
:class="{ :class="cn(
'border-slate-800': !event.category, event.category ? `event-details-${event.category.color}` : '',
'border-lime-800': event.category?.name === 'naissance', {
'border-stone-600': event.category?.name === 'mort', 'event-details': event.category
'border-orange-800': event.category?.name === 'catastrophe', }
'border-pink-800': event.category?.name === 'catastrophe naturelle', )"
'border-sky-800': event.category?.name === 'législation',
'border-purple-800': event.category?.name === 'religion',
'border-emerald-800': event.category?.name === 'joueurs',
'border-amber-800': event.category?.name === 'inauguration',
'border-green-800': event.category?.name === 'invention',
'border-cyan-800': event.category?.name === 'science',
'border-slate-600': event.category?.name === 'bénédiction',
'border-purple-700': event.category?.name === 'découverte',
'border-indigo-700': event.category?.name === 'exploration',
'border-amber-700': event.category?.name === 'construction',
'border-violet-700': event.category?.name === 'arcanologie',
'border-rose-800': event.category?.name === 'criminalité',
'border-stone-700': event.category?.name === 'scandale',
'border-yellow-600': event.category?.name === 'commerce'
}"
> >
<div ref="eventDetails" class="grid gap-1"> <div ref="eventDetails" class="grid gap-1">
<header class="pr-12"> <header class="pr-12">
@@ -128,13 +114,13 @@ function deployDeleteModal() {
<template v-if="event.category || event.secondaryCategories"> <template v-if="event.category || event.secondaryCategories">
<ul class="flex gap-1"> <ul class="flex gap-1">
<li v-if="event.category"> <li v-if="event.category">
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary"> <UiBadge class="mix-blend-luminosity font-bold bg-gray-600 lowercase" variant="secondary">
{{ event.category?.name }} {{ event.category?.name }}
</UiBadge> </UiBadge>
</li> </li>
<li v-for="cat in event.secondaryCategories" :key="cat.id"> <li v-for="cat in event.secondaryCategories" :key="cat.id">
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary"> <UiBadge class="mix-blend-luminosity bg-gray-600 lowercase" variant="secondary">
{{ cat.name }} {{ cat.name }}
</UiBadge> </UiBadge>
</li> </li>
@@ -205,72 +191,3 @@ function deployDeleteModal() {
</UiTooltipProvider> </UiTooltipProvider>
</UiPopoverContent> </UiPopoverContent>
</template> </template>
<style lang="scss">
.border-slate-800 {
--base-color: var(--color-slate-800);
}
.border-lime-800 {
--base-color: var(--color-lime-800);
}
.border-stone-600 {
--base-color: var(--color-stone-600);
}
.border-orange-800 {
--base-color: var(--color-orange-800);
}
.border-pink-800 {
--base-color: var(--color-pink-800);
}
.border-sky-800 {
--base-color: var(--color-sky-800);
}
.border-purple-800 {
--base-color: var(--color-purple-800);
}
.border-emerald-800 {
--base-color: var(--color-emerald-800);
}
.border-amber-800 {
--base-color: var(--color-amber-800);
}
.border-green-800 {
--base-color: var(--color-green-800);
}
.border-cyan-800 {
--base-color: var(--color-cyan-800);
}
.border-slate-600 {
--base-color: var(--color-slate-600);
}
.border-purple-700 {
--base-color: var(--color-purple-700);
}
.border-indigo-700 {
--base-color: var(--color-indigo-700);
}
.border-amber-700 {
--base-color: var(--color-amber-700);
}
.border-violet-700 {
--base-color: var(--color-violet-700);
}
.border-rose-800 {
--base-color: var(--color-rose-800);
}
.border-stone-700 {
--base-color: var(--color-stone-700);
}
.border-yellow-600 {
--base-color: var(--color-yellow-600);
}
.event-details {
--bg-color: color-mix(in srgb, var(--base-color), var(--color-slate-950) 85%);
background-color: var(--bg-color);
hr {
border-color: var(--base-color);
}
}
</style>

View File

@@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from '~/models/Date'; import type { RPGDate } from '~/models/Date';
import { PopoverAnchor } from 'radix-vue'; import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from '@phosphor-icons/vue'
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea } from '@phosphor-icons/vue'
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendarEvents()) const { eventSkeleton, operationInProgress } = storeToRefs(useCalendarEvents())
const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendarEvents() const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendarEvents()
@@ -80,9 +79,9 @@ function handleCancel() {
<template> <template>
<UiPopover v-model:open="popoverOpen"> <UiPopover v-model:open="popoverOpen">
<PopoverAnchor as-child> <UiPopoverTrigger as-child>
<button :class="btnClass" @dblclick="openEventCreatePopover()" /> <button :class="btnClass" @click="openEventCreatePopover()" />
</PopoverAnchor> </UiPopoverTrigger>
<UiPopoverContent <UiPopoverContent
:align="'center'" :align="'center'"
:side="'right'" :side="'right'"
@@ -96,7 +95,7 @@ function handleCancel() {
@pointer-down-outside="handleClosing" @pointer-down-outside="handleClosing"
> >
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
<div class="grid grid-cols-2 gap-y-6"> <div class="grid grid-cols-2 gap-y-3">
<div class="col-span-2 pl-8"> <div class="col-span-2 pl-8">
<input <input
id="new-event-title" id="new-event-title"
@@ -108,7 +107,7 @@ function handleCancel() {
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"> class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600">
</div> </div>
<div class="col-span-2 pl-8"> <div class="col-span-2 my-2 pl-8">
<textarea <textarea
id="new-event-description" id="new-event-description"
v-model="eventSkeleton.description" v-model="eventSkeleton.description"
@@ -140,6 +139,22 @@ function handleCancel() {
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<CalendarInputEventCategory v-model="eventSkeleton.category" placeholder="Ajouter une catégorie principale" />
</div>
</div>
<!-- <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" placeholder="Ajouter des catégories secondaires" />
</div>
</div> -->
<div class="col-span-2 mb-2">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" /> <PhMapPinArea size="18" weight="fill" />
@@ -149,7 +164,7 @@ function handleCancel() {
type="text" type="text"
name="new-event-location" name="new-event-location"
placeholder="Ajouter un endroit" placeholder="Ajouter un endroit"
class="w-full -my-1 py-1 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"> class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600">
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine } from '@phosphor-icons/vue' import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from '@phosphor-icons/vue'
import { VisuallyHidden } from 'radix-vue' import { VisuallyHidden } from 'radix-vue'
const { isEditEventModalOpen } = storeToRefs(useCalendarEvents()) const { isEditEventModalOpen } = storeToRefs(useCalendarEvents())
@@ -81,7 +81,7 @@ function handleCancel() {
</VisuallyHidden> </VisuallyHidden>
<form @submit.prevent="handleAction"> <form @submit.prevent="handleAction">
<div class="grid grid-cols-2 gap-y-6"> <div class="grid grid-cols-2 gap-y-3">
<div class="col-span-2"> <div class="col-span-2">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhPencilSimpleLine size="20" weight="fill" /> <PhPencilSimpleLine size="20" weight="fill" />
@@ -98,7 +98,7 @@ function handleCancel() {
</div> </div>
</div> </div>
<div class="col-span-2 ml-8"> <div class="col-span-2 my-2 ml-8">
<textarea <textarea
id="new-event-description" id="new-event-description"
v-model="eventSkeleton.description" v-model="eventSkeleton.description"
@@ -130,6 +130,26 @@ function handleCancel() {
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<div class="w-1/2">
<CalendarInputEventCategory v-model="eventSkeleton.category" placeholder="Ajouter une catégorie principale" />
</div>
</div>
</div>
<!-- <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<div class="w-1/2">
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" placeholder="Ajouter des catégories secondaires" />
</div>
</div>
</div> -->
<div class="col-span-2 mb-2">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" /> <PhMapPinArea size="18" weight="fill" />
@@ -139,7 +159,7 @@ function handleCancel() {
type="text" type="text"
name="new-event-location" name="new-event-location"
placeholder="Ajouter un endroit" placeholder="Ajouter un endroit"
class="w-full -my-1 py-1 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"> class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600">
</div> </div>
</div> </div>

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import type { Category } from '~/models/Category';
import { PhCaretDown, PhCheck } from '@phosphor-icons/vue';
const isPopoverOpen = ref<boolean>(false)
const props = defineProps<{
placeholder?: string
}>()
const model = defineModel<Category[]>({ default: [] })
const modelBuffer = ref<Category[]>([])
watch(modelBuffer.value, () => {
model.value = [ ...modelBuffer.value ]
})
const { categories: availableCategories } = useCalendarEvents()
const searchTerm = ref<string>('')
const filteredCategories = computed(() =>
searchTerm.value === ''
? availableCategories
: availableCategories.filter((category) => {
return category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
})
)
</script>
<template>
<UiPopover v-model:open="isPopoverOpen">
<UiPopoverTrigger as-child>
<UiButton
variant="outline"
role="combobox"
class="relative w-full max-w-full h-fit justify-between"
>
<template v-if="!model.length">
{{ props.placeholder }}
</template>
<template v-else>
<ul class="flex flex-wrap gap-1">
<li v-for="category in model" :key="`selected-cat-${category.id}`">
<UiBadge class="lowercase" variant="secondary">
{{ category.name }}
</UiBadge>
</li>
</ul>
</template>
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</UiButton>
</UiPopoverTrigger>
<UiPopoverContent
align="start"
side="bottom"
:collision-padding="50"
class="w-fit h-[33vh] p-0"
>
<UiCommand v-model="modelBuffer" v-model:searchTerm="searchTerm" :multiple="true">
<UiCommandInput placeholder="Rechercher les catégories" />
<UiCommandEmpty>Aucune catégorie trouvée.</UiCommandEmpty>
<UiCommandList>
<UiCommandGroup>
<UiCommandItem
v-for="category in filteredCategories"
:key="category.id"
:value="category"
class="cursor-pointer flex justify-between items-center"
:class="cn({
})"
>
<span>
{{ category.name }}
</span>
<PhCheck v-if="model.find(cat => cat.id === category.id)" />
</UiCommandItem>
</UiCommandGroup>
</UiCommandList>
</UiCommand>
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -0,0 +1,73 @@
<script lang="ts" setup>
import { PhCaretDown } from '@phosphor-icons/vue';
import type { Category } from '~/models/Category';
const isPopoverOpen = ref<boolean>(false)
const props = defineProps<{
placeholder?: string
}>()
const model = defineModel<Category>()
const { categories: availableCategories } = useCalendarEvents()
const searchTerm = ref<string>('')
function handleCatSelect() {
isPopoverOpen.value = false
}
const filteredCategories = computed(() =>
searchTerm.value === ''
? availableCategories
: availableCategories.filter((category) => {
return category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
})
)
</script>
<template>
<UiPopover v-model:open="isPopoverOpen">
<UiPopoverTrigger as-child>
<UiButton
variant="outline"
role="combobox"
class="w-full max-w-full justify-between"
>
<template v-if="!model">
{{ props.placeholder }}
</template>
<template v-else>
{{ model.name }}
</template>
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</UiButton>
</UiPopoverTrigger>
<UiPopoverContent
align="start"
side="bottom"
:collision-padding="50"
class="w-fit h-[33vh] p-0"
>
<UiCommand v-model="model" v-model:searchTerm="searchTerm">
<UiCommandInput placeholder="Rechercher les catégories" />
<UiCommandEmpty>Aucune catégorie trouvée.</UiCommandEmpty>
<UiCommandList>
<UiCommandGroup>
<UiCommandItem
v-for="category in filteredCategories"
:key="category.id"
:value="category"
class="cursor-pointer"
@select="handleCatSelect"
>
{{ category.name }}
</UiCommandItem>
</UiCommandGroup>
</UiCommandList>
</UiCommand>
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from '@/lib/utils'
import type { RPGDate } from '@/models/Date' import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/CalendarEvent' import type { CalendarEvent } from '@/models/CalendarEvent'
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from '@/stores/CalendarStore'
@@ -26,28 +27,11 @@ const dateDuration: string | null = props.event.endDate
<template> <template>
<button <button
class="relative block w-full text-left py-3 px-4 rounded-sm transition-colors" class="relative block w-full text-left py-3 px-4 rounded-sm transition-colors"
:class="{ :class="cn(
event.category ? `event-${event.category.color}` : '',
{
'pt-4': event.hidden, 'pt-4': event.hidden,
'text-white bg-slate-600 hover:bg-slate-700': !event.category, })"
'text-white bg-lime-600 hover:bg-lime-700': event.category?.name === 'naissance',
'text-white bg-stone-500 hover:bg-stone-700': event.category?.name === 'mort',
'text-white bg-orange-600 hover:bg-orange-700': event.category?.name === 'catastrophe',
'text-white bg-pink-600 hover:bg-pink-700': event.category?.name === 'catastrophe naturelle',
'text-white bg-sky-600 hover:bg-sky-700': event.category?.name === 'législation',
'text-white bg-purple-600 hover:bg-purple-700': event.category?.name === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': event.category?.name === 'joueurs',
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category?.name === 'inauguration',
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category?.name === 'invention',
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category?.name === 'science',
'text-slate-900 bg-white hover:bg-yellow-200': event.category?.name === 'bénédiction',
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category?.name === 'découverte',
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category?.name === 'exploration',
'text-white bg-amber-600 hover:bg-amber-700': event.category?.name === 'construction',
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category?.name === 'arcanologie',
'text-white bg-rose-600 hover:bg-rose-700': event.category?.name === 'criminalité',
'text-white bg-stone-600 hover:bg-stone-700': event.category?.name === 'scandale',
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category?.name === 'commerce'
}"
@click="$emit('query:date-jump', event.startDate)" @click="$emit('query:date-jump', event.startDate)"
> >
<div class="flex gap-2 items-center mb-1"> <div class="flex gap-2 items-center mb-1">
@@ -97,13 +81,13 @@ const dateDuration: string | null = props.event.endDate
<div v-if="event.category || event.secondaryCategories" class="absolute top-3 right-4"> <div v-if="event.category || event.secondaryCategories" class="absolute top-3 right-4">
<ul class="flex gap-1"> <ul class="flex gap-1">
<li v-if="event.category"> <li v-if="event.category">
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary"> <UiBadge class="mix-blend-luminosity font-bold bg-gray-600 lowercase" variant="secondary">
{{ event.category?.name }} {{ event.category?.name }}
</UiBadge> </UiBadge>
</li> </li>
<li v-for="cat in event.secondaryCategories" :key="cat.id"> <li v-for="cat in event.secondaryCategories" :key="cat.id">
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary"> <UiBadge class="mix-blend-luminosity bg-gray-600 lowercase" variant="secondary">
{{ cat.name }} {{ cat.name }}
</UiBadge> </UiBadge>
</li> </li>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue'
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue'
import { Check } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CheckboxRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CheckboxRoot
v-bind="forwarded"
:class="
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class)"
>
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
<slot>
<Check class="h-4 w-4" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>

View File

@@ -0,0 +1 @@
export { default as Checkbox } from './Checkbox.vue'

View File

@@ -1,5 +1,5 @@
import { z } from 'zod' import { z } from 'zod'
import type { Category } from './Category' import { categorySchema, type Category } from './Category'
import { dateSchema, type RPGDate } from './Date' import { dateSchema, type RPGDate } from './Date'
export interface CalendarEvent { export interface CalendarEvent {
@@ -25,7 +25,8 @@ export const postEventBodySchema = z.object({
location: z.string().optional().nullable(), location: z.string().optional().nullable(),
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(),
}), }),
calendarId: z.number({ coerce: true }).int().positive() calendarId: z.number({ coerce: true }).int().positive()
}) })

View File

@@ -1,4 +1,14 @@
import { z } from 'zod'
import type { RPGColor } from './Color'
export interface Category { export interface Category {
id: number id: number
name: string name: string
color?: RPGColor
} }
export const categorySchema = z.object({
id: z.number().int(),
name: z.string(),
color: z.string().default("black")
})

1
models/Color.ts Normal file
View File

@@ -0,0 +1 @@
export type RPGColor = "red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose" | "black" | "white"

View File

@@ -0,0 +1,15 @@
import { serverSupabaseClient } from "#supabase/server";
import type { Category } from "~/models/Category";
export default defineEventHandler(async (event) => {
const client = await serverSupabaseClient(event)
const output = client
.from('calendar_event_categories')
.select(`
id,
name
`)
return output.returns<Category[]>()
})

View File

@@ -22,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,
@@ -42,6 +41,7 @@ export default defineEventHandler(async (event) => {
description: bodyData.event.description, description: bodyData.event.description,
location: bodyData.event.location, location: bodyData.event.location,
hidden: bodyData.event.hidden, hidden: bodyData.event.hidden,
category: bodyData.event.category?.id,
calendar_id: bodyData?.calendarId calendar_id: bodyData?.calendarId
} as never } as never
) )

View File

@@ -26,6 +26,7 @@ export default defineEventHandler(async (event) => {
description: bodyData.event.description, description: bodyData.event.description,
location: bodyData.event.location, location: bodyData.event.location,
hidden: bodyData.event.hidden, hidden: bodyData.event.hidden,
category: bodyData.event.category?.id,
calendar_id: bodyData?.calendarId calendar_id: bodyData?.calendarId
} as never } as never
) )

View File

@@ -1,7 +1,8 @@
import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/CalendarEvent' import type { CalendarEvent } from '@/models/CalendarEvent'
import type { RPGDate } from '@/models/Date'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, watch, type Ref } from 'vue' import { ref, watch, type Ref } from 'vue'
import type { Category } from '~/models/Category'
import { useCalendar } from './CalendarStore' import { useCalendar } from './CalendarStore'
export const useCalendarEvents = defineStore('calendar-events', () => { export const useCalendarEvents = defineStore('calendar-events', () => {
@@ -14,6 +15,12 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
baseEvents.value = data baseEvents.value = data
} }
const categories = ref<Category[]>([])
function setCategories(data: Category[]) {
categories.value = data
}
// Order base events by dates // Order base events by dates
const allEvents = computed(() => baseEvents.value.sort((a, b) => { const allEvents = computed(() => baseEvents.value.sort((a, b) => {
return compareDates(a.startDate, b.startDate, 'desc') return compareDates(a.startDate, b.startDate, 'desc')
@@ -269,6 +276,8 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
return { return {
allEvents, allEvents,
setEvents, setEvents,
categories,
setCategories,
currentEvents, currentEvents,
getRelativeEventFromDate, getRelativeEventFromDate,
getRelativeEventFromEvent, getRelativeEventFromEvent,

View File

@@ -39,7 +39,7 @@ 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, color app_colors default 'black',
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.';
@@ -49,6 +49,7 @@ create table public.calendars (
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,
today json not null, today json not null,
color app_colors default 'black',
world_id bigint references public.worlds on delete cascade not null world_id bigint references public.worlds on delete cascade not null
); );
comment on table public.calendars is 'Calendar settings and configuration attached to a single world.'; comment on table public.calendars is 'Calendar settings and configuration attached to a single world.';
@@ -67,6 +68,7 @@ comment on table public.calendar_months is 'A calendar month.';
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',
unique (name) unique (name)
); );
comment on table public.calendar_event_categories is 'Categories describing events.'; comment on table public.calendar_event_categories is 'Categories describing events.';
@@ -98,6 +100,7 @@ comment on table public.calendar_event_categories_links is 'Link tables for mult
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',
unique (name) unique (name)
); );
comment on table public.character_categories is 'Categories describing characters'; comment on table public.character_categories is 'Categories describing characters';

View File

@@ -2,38 +2,38 @@ insert into public.role_permissions (role, permission) values ('sa', 'events.see
insert into public.role_permissions (role, permission) values ('sa', 'users.ban'); insert into public.role_permissions (role, permission) values ('sa', 'users.ban');
-- Event categories -- Event categories
insert into public.calendar_event_categories (name) values ('naissance'); insert into public.calendar_event_categories (name, color) values ('Naissance', 'white');
insert into public.calendar_event_categories (name) values ('mort'); insert into public.calendar_event_categories (name, color) values ('Mort', 'black');
insert into public.calendar_event_categories (name) values ('catastrophe'); insert into public.calendar_event_categories (name, color) values ('Catastrophe', 'orange');
insert into public.calendar_event_categories (name) values ('catastrophe naturelle'); insert into public.calendar_event_categories (name, color) values ('Catastrophe naturelle', 'red');
insert into public.calendar_event_categories (name) values ('inauguration'); insert into public.calendar_event_categories (name, color) values ('Inauguration', 'green');
insert into public.calendar_event_categories (name) values ('religion'); insert into public.calendar_event_categories (name, color) values ('Religion', 'violet');
insert into public.calendar_event_categories (name) values ('invention'); insert into public.calendar_event_categories (name, color) values ('Invention', 'teal');
insert into public.calendar_event_categories (name) values ('science'); insert into public.calendar_event_categories (name, color) values ('Science', 'indigo');
insert into public.calendar_event_categories (name) values ('bénédiction'); insert into public.calendar_event_categories (name, color) values ('Bénédiction', 'white');
insert into public.calendar_event_categories (name) values ('joueurs'); insert into public.calendar_event_categories (name, color) values ('Joueurs', 'white');
insert into public.calendar_event_categories (name) values ('découverte'); insert into public.calendar_event_categories (name, color) values ('Découverte', 'purple');
insert into public.calendar_event_categories (name) values ('exploration'); insert into public.calendar_event_categories (name, color) values ('Exploration', 'lime');
insert into public.calendar_event_categories (name) values ('construction'); insert into public.calendar_event_categories (name, color) values ('Construction', 'blue');
insert into public.calendar_event_categories (name) values ('arcanologie'); insert into public.calendar_event_categories (name, color) values ('Arcanologie', 'cyan');
insert into public.calendar_event_categories (name) values ('criminalité'); insert into public.calendar_event_categories (name, color) values ('Criminalité', 'rose');
insert into public.calendar_event_categories (name) values ('scandale'); insert into public.calendar_event_categories (name, color) values ('Scandale', 'pink');
insert into public.calendar_event_categories (name) values ('commerce'); insert into public.calendar_event_categories (name, color) values ('Commerce', 'amber');
insert into public.calendar_event_categories (name) values ('législation'); insert into public.calendar_event_categories (name, color) values ('Législation', 'blue');
-- Character categories -- Character categories
insert into public.character_categories (name) values ('joueur'); insert into public.character_categories (name, color) values ('Joueur', 'white');
insert into public.character_categories (name) values ('comte'); insert into public.character_categories (name, color) values ('Comte', 'emerald');
insert into public.character_categories (name) values ('scientifique'); insert into public.character_categories (name, color) values ('Scientifique', 'indigo');
insert into public.character_categories (name) values ('mage'); insert into public.character_categories (name, color) values ('Mage', 'cyan');
insert into public.character_categories (name) values ('professeur'); insert into public.character_categories (name, color) values ('Professeur', 'teal');
insert into public.character_categories (name) values ('criminel'); insert into public.character_categories (name, color) values ('Criminel', 'rose');
insert into public.character_categories (name) values ('étincelle'); insert into public.character_categories (name, color) values ('Étincelle', 'lime');
insert into public.character_categories (name) values ('buse blanche'); insert into public.character_categories (name, color) values ('Buse blanche', 'yellow');
insert into public.character_categories (name) values ('ecclésiastique'); insert into public.character_categories (name, color) values ('Ecclésiastique', 'violet');
insert into public.character_categories (name) values ('militaire'); insert into public.character_categories (name, color) values ('Militaire', 'orange');
insert into public.character_categories (name) values ('activiste'); insert into public.character_categories (name, color) values ('Activiste', 'sky');
insert into public.character_categories (name) values ('commerçant'); insert into public.character_categories (name, color) values ('Commerçant', 'amber');
-- Worlds -- Worlds
insert into public.worlds (name, description, color) values ('Léïm', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis aliquet congue aliquet. Curabitur eu iaculis diam. Nunc iaculis nibh orci, eu semper nunc congue congue. Praesent euismod tortor eget metus tristique lobortis vel in risus. In volutpat ligula orci, id pharetra lectus egestas at.', 'black'); insert into public.worlds (name, description, color) 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');
@@ -239,7 +239,7 @@ insert into public.calendar_events (title, description, location, start_date, en
1 1
); );
insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values ( insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values (
'1ère disparation', '1ère disparition',
'Taleb Vaht décède dans une grotte à la suite d''une attaque d''ischiels enragées.', 'Taleb Vaht décède dans une grotte à la suite d''une attaque d''ischiels enragées.',
'Cantane', 'Cantane',
'{ "day": 28, "month": 7, "year": 3209 }', '{ "day": 28, "month": 7, "year": 3209 }',
@@ -250,7 +250,7 @@ insert into public.calendar_events (title, description, location, start_date, en
1 1
); );
insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values ( insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values (
'2ème disparation', '2ème disparition',
'Donovane le mineur kéturien disparait sans laisser de traces.', 'Donovane le mineur kéturien disparait sans laisser de traces.',
'Cantane', 'Cantane',
'{ "day": 32, "month": 7, "year": 3209 }', '{ "day": 32, "month": 7, "year": 3209 }',
@@ -261,7 +261,7 @@ insert into public.calendar_events (title, description, location, start_date, en
1 1
); );
insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values ( insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values (
'3ème disparation', '3ème disparition',
'Disparition de Sébastien, gredin sauride.', 'Disparition de Sébastien, gredin sauride.',
'Cantane', 'Cantane',
'{ "day": 10, "month": 8, "year": 3209 }', '{ "day": 10, "month": 8, "year": 3209 }',
@@ -272,7 +272,7 @@ insert into public.calendar_events (title, description, location, start_date, en
1 1
); );
insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values ( insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values (
'4ème disparation', '4ème disparition',
'Disparition de Thérence, patrouilleur sauride de la Vieille Garde.', 'Disparition de Thérence, patrouilleur sauride de la Vieille Garde.',
'Cantane', 'Cantane',
'{ "day": 19, "month": 8, "year": 3209 }', '{ "day": 19, "month": 8, "year": 3209 }',
@@ -283,7 +283,7 @@ insert into public.calendar_events (title, description, location, start_date, en
1 1
); );
insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values ( insert into public.calendar_events (title, description, location, start_date, end_date, category, hidden, wiki, calendar_id) values (
'5ème disparation', '5ème disparition',
'Disparition de Mathilda Boulais, vendeuse de pierres.', 'Disparition de Mathilda Boulais, vendeuse de pierres.',
'Cantane', 'Cantane',
'{ "day": 22, "month": 8, "year": 3209 }', '{ "day": 22, "month": 8, "year": 3209 }',