Added category input component
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<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 { PopoverAnchor } from 'radix-vue';
|
||||||
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea } from '@phosphor-icons/vue'
|
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from '@phosphor-icons/vue'
|
||||||
|
|
||||||
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendarEvents())
|
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendarEvents())
|
||||||
const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendarEvents()
|
const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendarEvents()
|
||||||
@@ -96,7 +96,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"
|
||||||
@@ -140,6 +140,14 @@ 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" />
|
||||||
|
|
||||||
|
<CalendarInputEventCategories v-model="eventSkeleton.category" placeholder="Ajouter une catégorie principale" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2 mb-4">
|
||||||
<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 +157,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>
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -130,6 +130,14 @@ 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" />
|
||||||
|
|
||||||
|
<CalendarInputEventCategories v-model="eventSkeleton.category" placeholder="Ajouter une catégorie principale" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2 mb-4">
|
||||||
<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 +147,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>
|
||||||
|
|
||||||
|
|||||||
72
components/calendar/input/EventCategories.vue
Normal file
72
components/calendar/input/EventCategories.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhCaretDown } from '@phosphor-icons/vue';
|
||||||
|
import type { Category } from '~/models/Category';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
placeholder?: string
|
||||||
|
multiple?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const model = defineModel<Category[] | Category>()
|
||||||
|
|
||||||
|
const { categories: availableCategories } = useCalendarEvents()
|
||||||
|
|
||||||
|
const isPopoverOpen = ref<boolean>(false)
|
||||||
|
|
||||||
|
function handleCatSelect() {
|
||||||
|
if (!props.multiple) {
|
||||||
|
isPopoverOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedTextValue = computed(() => {
|
||||||
|
if (model.value) {
|
||||||
|
if ("name" in model.value) {
|
||||||
|
return model.value.name
|
||||||
|
} else {
|
||||||
|
return model.value[0].name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return props.placeholder
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiPopover v-model:open="isPopoverOpen">
|
||||||
|
<UiPopoverTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
class="w-fit max-w-full justify-between capitalize"
|
||||||
|
>
|
||||||
|
{{ computedTextValue }}
|
||||||
|
|
||||||
|
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</UiButton>
|
||||||
|
</UiPopoverTrigger>
|
||||||
|
<UiPopoverContent
|
||||||
|
:align="'start'"
|
||||||
|
class="w-fit p-0"
|
||||||
|
>
|
||||||
|
<UiCommand v-model="model" :multiple>
|
||||||
|
<UiCommandInput placeholder="Rechercher les catégories" />
|
||||||
|
<UiCommandEmpty>Aucune catégorie trouvée.</UiCommandEmpty>
|
||||||
|
<UiCommandList>
|
||||||
|
<UiCommandGroup>
|
||||||
|
<UiCommandItem
|
||||||
|
v-for="category in availableCategories"
|
||||||
|
:key="category.id"
|
||||||
|
:value="category"
|
||||||
|
@select="handleCatSelect"
|
||||||
|
>
|
||||||
|
<span class="capitalize">
|
||||||
|
{{ category.name }}
|
||||||
|
</span>
|
||||||
|
</UiCommandItem>
|
||||||
|
</UiCommandGroup>
|
||||||
|
</UiCommandList>
|
||||||
|
</UiCommand>
|
||||||
|
</UiPopoverContent>
|
||||||
|
</UiPopover>
|
||||||
|
</template>
|
||||||
33
components/ui/checkbox/Checkbox.vue
Normal file
33
components/ui/checkbox/Checkbox.vue
Normal 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>
|
||||||
1
components/ui/checkbox/index.ts
Normal file
1
components/ui/checkbox/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Checkbox } from './Checkbox.vue'
|
||||||
@@ -25,7 +25,10 @@ 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: z.object({
|
||||||
|
id: z.number().int()
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
calendarId: z.number({ coerce: true }).int().positive()
|
calendarId: z.number({ coerce: true }).int().positive()
|
||||||
})
|
})
|
||||||
|
|||||||
15
server/api/calendars/categories/query.get.ts
Normal file
15
server/api/calendars/categories/query.get.ts
Normal 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[]>()
|
||||||
|
})
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user