Merge pull request #45 from AlexisNP/features/add-limit-to-event-text-fields

Add limit to event text fields
This commit is contained in:
AlexisNP
2024-11-25 18:17:04 +01:00
committed by GitHub
13 changed files with 211 additions and 73 deletions

View File

@@ -75,7 +75,7 @@ function deployDeleteModal() {
> >
<div ref="eventDetails" class="grid gap-1"> <div ref="eventDetails" class="grid gap-1">
<header class="pr-12"> <header class="pr-12">
<div class="text-lg font-semibold"> <div class="mb-2 max-h-24 overflow-y-auto text-lg font-semibold">
{{ event.title }} {{ event.title }}
</div> </div>
@@ -100,18 +100,26 @@ function deployDeleteModal() {
<div class="mb-1 space-y-1"> <div class="mb-1 space-y-1">
<template v-if="event.location"> <template v-if="event.location">
<p class="text-sm italic dark:opacity-75 flex items-center gap-1"> <div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<PhMapPinArea size="16" weight="fill" /> {{ event.location }} <div>
</p> <PhMapPinArea size="16" weight="fill" />
</div>
<p class="max-h-12 overflow-y-auto">{{ event.location }}</p>
</div>
</template> </template>
<p class="text-sm italic dark:opacity-75 flex items-center gap-1"> <div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }} <div>
</p> <PhAlarm size="16" weight="fill" />
</div>
<p>{{ dateDifference }}</p>
</div>
<template v-if="dateDuration"> <template v-if="dateDuration">
<p class="text-sm italic dark:opacity-75 flex items-center gap-1"> <div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhHourglassMedium size="16" weight="fill" /> <PhHourglassMedium size="16" weight="fill" />
{{ $t('entity.calendar.date.while', { duration: dateDuration } )}} </div>
</p> <p>{{ $t('entity.calendar.date.while', { duration: dateDuration } )}}</p>
</div>
</template> </template>
</div> </div>
@@ -134,7 +142,7 @@ function deployDeleteModal() {
<template v-if="event.description"> <template v-if="event.description">
<hr class="border-slate-500 mt-2" > <hr class="border-slate-500 mt-2" >
<div class="mt-2 text-sm text-slate-600 dark:text-slate-300"> <div class="max-h-48 overflow-y-auto mt-2 text-sm text-slate-600 dark:text-slate-300">
{{ event.description }} {{ event.description }}
</div> </div>
</template> </template>

View File

@@ -5,6 +5,9 @@ import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar()) const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar())
const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendar() const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendar()
const popoverOpen = ref(false) const popoverOpen = ref(false)
const { t } = useI18n()
const isLoading = ref(false) const isLoading = ref(false)
const formErrors = reactive<{ message: string | null }>({ const formErrors = reactive<{ message: string | null }>({
@@ -88,7 +91,7 @@ function handleCancel() {
:collision-padding="60" :collision-padding="60"
:disable-outside-pointer-events="true" :disable-outside-pointer-events="true"
:trap-focus="true" :trap-focus="true"
class="pl-3 min-w-96 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="handleClosing"
@focus-outside="handleClosing" @focus-outside="handleClosing"
@interact-outside="handleClosing" @interact-outside="handleClosing"
@@ -104,8 +107,14 @@ function handleCancel() {
name="new-event-title" name="new-event-title"
required required
:placeholder="$t('entity.calendar.event.title')" :placeholder="$t('entity.calendar.event.title')"
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" :maxlength="120"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}"
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 invalid:border-red-500"
> >
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }}
</div>
</div> </div>
<div class="col-span-2 my-2 pl-8"> <div class="col-span-2 my-2 pl-8">
@@ -113,8 +122,13 @@ function handleCancel() {
id="new-event-description" id="new-event-description"
v-model="eventSkeleton.description" v-model="eventSkeleton.description"
name="new-event-description" name="new-event-description"
:placeholder="$t('entity.addDescription')" class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" :placeholder="$t('entity.addDescription')"
:maxlength="1200"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500"
/> />
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }}
</div>
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
@@ -142,7 +156,10 @@ function handleCancel() {
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhTag size="18" weight="fill" /> <PhTag size="18" weight="fill" />
<CalendarInputEventCategory v-model="eventSkeleton.category" :placeholder="$t('entity.category.addPrimary')" /> <CalendarInputEventCategory
v-model="eventSkeleton.category"
:placeholder="$t('entity.category.addPrimary')"
/>
</div> </div>
</div> </div>
@@ -158,13 +175,21 @@ function handleCancel() {
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" /> <PhMapPinArea size="18" weight="fill" />
<div class="grow">
<input <input
id="new-event-location" id="new-event-location"
v-model="eventSkeleton.location" v-model="eventSkeleton.location"
type="text" type="text"
name="new-event-location" name="new-event-location"
:placeholder="$t('entity.calendar.event.addLocation')" :placeholder="$t('entity.calendar.event.addLocation')"
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"> :maxlength="160"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
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 class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.location') }}
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue" import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast";
import type { APIError } from "~/models/Errors";
const { resetSkeleton, updateEventFromSkeleton, cancelLatestRequest } = useCalendar() const { resetSkeleton, updateEventFromSkeleton, cancelLatestRequest } = useCalendar()
const { eventSkeleton, lastActiveEvent, isEditEventModalOpen } = storeToRefs(useCalendar()) const { eventSkeleton, lastActiveEvent, isEditEventModalOpen } = storeToRefs(useCalendar())
const { toast } = useToast()
const { t } = useI18n()
const isLoading = ref(false) const isLoading = ref(false)
const formErrors = reactive<{ message: string | null }>({ const formErrors = reactive<{ message: string | null }>({
@@ -24,15 +29,22 @@ async function handleAction() {
try { try {
await updateEventFromSkeleton() await updateEventFromSkeleton()
isEditEventModalOpen.value = false
} catch (err) { } catch (err) {
if (err instanceof Error) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
formErrors.message = err.message const apiError = (err as any).data as APIError
}
apiError.data.errors.forEach((error) => {
toast({
title: t("entity.calendar.event.editErrors.toastTitle"),
variant: "destructive",
description: t(`entity.calendar.event.editErrors.${error.path[1]}_${error.code}`),
duration: 2000,
})
})
} finally { } finally {
resetSkeleton() resetSkeleton()
isLoading.value = false isLoading.value = false
isEditEventModalOpen.value = false
} }
} }
@@ -85,6 +97,7 @@ function handleCancel() {
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhPencilSimpleLine size="20" weight="fill" /> <PhPencilSimpleLine size="20" weight="fill" />
<div class="grow">
<input <input
id="new-event-title" id="new-event-title"
v-model="eventSkeleton.title" v-model="eventSkeleton.title"
@@ -92,8 +105,14 @@ function handleCancel() {
name="new-event-title" name="new-event-title"
required required
:placeholder="$t('entity.calendar.event.title')" :placeholder="$t('entity.calendar.event.title')"
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" :maxlength="120"
pattern="([A-Za--ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}"
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 invalid:border-red-500"
> >
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }}
</div>
</div>
</div> </div>
</div> </div>
@@ -103,8 +122,12 @@ function handleCancel() {
v-model="eventSkeleton.description" v-model="eventSkeleton.description"
name="new-event-description" name="new-event-description"
:placeholder="$t('entity.addDescription')" :placeholder="$t('entity.addDescription')"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" :maxlength="1200"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500"
/> />
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }}
</div>
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
@@ -133,7 +156,10 @@ function handleCancel() {
<PhTag size="18" weight="fill" /> <PhTag size="18" weight="fill" />
<div class="w-1/2"> <div class="w-1/2">
<CalendarInputEventCategory v-model="eventSkeleton.category" placeholder="Ajouter une catégorie principale" /> <CalendarInputEventCategory
v-model="eventSkeleton.category"
:placeholder="$t('entity.category.addPrimary')"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -143,7 +169,7 @@ function handleCancel() {
<PhTag size="18" weight="fill" /> <PhTag size="18" weight="fill" />
<div class="w-1/2"> <div class="w-1/2">
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" :placeholder="Ajouter des catégories secondaires" /> <CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" placeholder="Ajouter des catégories secondaires" />
</div> </div>
</div> </div>
</div> --> </div> -->
@@ -152,13 +178,22 @@ function handleCancel() {
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" /> <PhMapPinArea size="18" weight="fill" />
<div class="grow">
<input <input
id="new-event-location" id="new-event-location"
v-model="eventSkeleton.location" v-model="eventSkeleton.location"
type="text" type="text"
name="new-event-location" name="new-event-location"
:placeholder="$t('entity.calendar.event.addLocation')" :placeholder="$t('entity.calendar.event.addLocation')"
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"> :maxlength="160"
pattern="([A-Za--ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
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 invalid:border-red-500"
>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.location') }}
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -13,5 +13,5 @@ const delegatedProps = computed(() => {
</script> </script>
<template> <template>
<ToastViewport v-bind="delegatedProps" :class="cn('fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]', props.class)" /> <ToastViewport v-bind="delegatedProps" :class="cn('fixed top-0 z-[100] flex gap-y-2 max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]', props.class)" />
</template> </template>

View File

@@ -136,10 +136,21 @@ export default defineI18nConfig(() => ({
addLocation: "Add a place", addLocation: "Add a place",
prevPage: "Previous page with events", prevPage: "Previous page with events",
nextPage: "Next page with events", nextPage: "Next page with events",
patterns: {
title: "Between 3 and 120 caracters, without special symbols, except & - ~ ()",
description: "1200 caracters max.",
location: "Between 3 and 160 caracters, without special symbols, except & - ~ ()",
},
editDialog: { editDialog: {
title: "Edit event", title: "Edit event",
subtitle: "Update event data", subtitle: "Update event data",
}, },
editErrors: {
toastTitle: "Event wasn't updated.",
title_too_big: "Title should be less than 120 characters long.",
description_too_big: "Description should be less than 1200 characters long.",
location_too_big: "Location should be less than 160 characters long.",
},
deleteDialog: { deleteDialog: {
title: "Delete this event", title: "Delete this event",
subtitle: "Data associated with this event will be lost and you won't be able to retrieve it !", subtitle: "Data associated with this event will be lost and you won't be able to retrieve it !",
@@ -344,10 +355,21 @@ export default defineI18nConfig(() => ({
addLocation: "Ajouter un endroit", addLocation: "Ajouter un endroit",
prevPage: "Précédente page à évènements", prevPage: "Précédente page à évènements",
nextPage: "Prochaine page à évènements", nextPage: "Prochaine page à évènements",
patterns: {
title: "Entre 3 et 120 charactères, sans symboles spéciaux, sauf & - ~ ()",
description: "Maximum 1200 charactères.",
location: "Entre 3 et 160 charactères, sans symboles spéciaux, sauf & - ~ ()",
},
editDialog: { editDialog: {
title: "Modifier l'évènement", title: "Modifier l'évènement",
subtitle: "Mettre à jour les données de l'évènement", subtitle: "Mettre à jour les données de l'évènement",
}, },
editErrors: {
toastTitle: "L'évènement n'a pas été modifié",
title_too_big: "Le titre doit être inférieur à 120 caractères.",
description_too_big: "La description doit être inférieure à 1200 caractères.",
location_too_big: "La localisation doit être inférieure à 160 caractères.",
},
deleteDialog: { deleteDialog: {
title: "Supprimer l'évènement", title: "Supprimer l'évènement",
subtitle: "Les données associés à cet évènement seront supprimées et vous ne pourrez plus les récupérer !", subtitle: "Les données associés à cet évènement seront supprimées et vous ne pourrez plus les récupérer !",

View File

@@ -20,9 +20,9 @@ export interface CalendarEvent {
*/ */
export const postEventBodySchema = z.object({ export const postEventBodySchema = z.object({
event: z.object({ event: z.object({
title: z.string(), title: z.string().max(120),
description: z.string().optional().nullable(), description: z.string().max(1200).optional().nullable(),
location: z.string().optional().nullable(), location: z.string().max(160).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(),

14
models/Errors.ts Normal file
View File

@@ -0,0 +1,14 @@
export interface ValidationError {
path: (string | number)[]
message: string
code: string
}
export interface APIError {
statusCode: number
statusMessage: string
message: string
data: {
errors: ValidationError[]
}
}

View File

@@ -1,6 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
devtools: { enabled: true }, devtools: { enabled: false },
modules: [ modules: [
"@nuxtjs/supabase", "@nuxtjs/supabase",
@@ -16,12 +16,21 @@ export default defineNuxtConfig({
i18n: { i18n: {
vueI18n: "./i18n.config.ts", vueI18n: "./i18n.config.ts",
strategy: "no_prefix",
detectBrowserLanguage: { detectBrowserLanguage: {
useCookie: true, useCookie: true,
cookieKey: "tttools_lang", cookieKey: "tttools_lang",
redirectOn: "all", },
alwaysRedirect: true, locales: [
{
code: "en",
name: "English"
},
{
code: "fr",
name: "Français"
} }
]
}, },
supabase: { supabase: {

View File

@@ -22,12 +22,22 @@ export default defineEventHandler(async (event) => {
} }
if (bodyError) { if (bodyError) {
throw createError({ const error = createError({
cause: "Utilisateur", cause: "Utilisateur",
fatal: false, fatal: false,
message: "Le schéma de la requête n'est pas complet ou mal renseigné.", statusCode: 401,
status: 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 { try {

View File

@@ -7,12 +7,22 @@ export default defineEventHandler(async (event) => {
const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => postEventBodySchema.safeParse(body)) const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => postEventBodySchema.safeParse(body))
if (schemaError) { if (schemaError) {
throw createError({ const error = createError({
cause: "Utilisateur", cause: "Utilisateur",
fatal: false, fatal: false,
message: "Le schéma de la requête n'est pas complet ou mal renseigné.", statusCode: 401,
status: 401, statusMessage: "Validation Error",
message: "Erreur de validation du schéma, probablement dûe à une erreur utilisateur.",
data: {
errors: schemaError.issues.map(issue => ({
path: issue.path,
message: issue.message,
code: issue.code
}))
}
}) })
throw error
} }
try { try {

View File

@@ -869,8 +869,8 @@ export const useCalendar = defineStore("calendar", () => {
const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id) const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
baseEvents.value[eventIndex] = res baseEvents.value[eventIndex] = res
} catch (err) {
console.log(err) return res
} finally { } finally {
abortController = null abortController = null
isUpdatingEvent.value = false isUpdatingEvent.value = false

View File

@@ -247,18 +247,14 @@ export const useCalendarEvents = defineStore("calendar-events", () => {
abortController = new AbortController() abortController = new AbortController()
isUpdatingEvent.value = true isUpdatingEvent.value = true
try {
const res = await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: "PATCH", body: { event : eventSkeleton.value, calendarId: activeCalendar?.id }, signal: abortController.signal }) 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) const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
baseEvents.value[eventIndex] = res baseEvents.value[eventIndex] = res
} catch (err) {
console.log(err)
} finally {
abortController = null abortController = null
isUpdatingEvent.value = false isUpdatingEvent.value = false
} }
}
async function deleteEventFromSkeleton() { async function deleteEventFromSkeleton() {
abortController = new AbortController() abortController = new AbortController()

View File

@@ -88,6 +88,15 @@ create table public.calendar_events (
); );
comment on table public.calendar_events is 'Events linked to a world'; comment on table public.calendar_events is 'Events linked to a world';
-- Calendar Events restrictions
alter table public.calendar_events
add constraint calendar_events_maxlen_check
check (
char_length(title) <= 120 AND
(description IS NULL OR char_length(description) <= 1200) AND
(location IS NULL OR char_length(location) <= 160)
);
-- Link table for events - categories -- Link table for events - categories
create table public.calendar_event_categories_links ( create table public.calendar_event_categories_links (
calendar_event_id bigint references public.calendar_events on delete cascade, calendar_event_id bigint references public.calendar_events on delete cascade,