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">
<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 }}
</div>
@@ -100,18 +100,26 @@ function deployDeleteModal() {
<div class="mb-1 space-y-1">
<template v-if="event.location">
<p class="text-sm italic dark:opacity-75 flex items-center gap-1">
<PhMapPinArea size="16" weight="fill" /> {{ event.location }}
</p>
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhMapPinArea size="16" weight="fill" />
</div>
<p class="max-h-12 overflow-y-auto">{{ event.location }}</p>
</div>
</template>
<p class="text-sm italic dark:opacity-75 flex items-center gap-1">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
</p>
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhAlarm size="16" weight="fill" />
</div>
<p>{{ dateDifference }}</p>
</div>
<template v-if="dateDuration">
<p class="text-sm italic dark:opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" />
{{ $t('entity.calendar.date.while', { duration: dateDuration } )}}
</p>
<div class="text-sm italic dark:opacity-75 flex items-center gap-1">
<div>
<PhHourglassMedium size="16" weight="fill" />
</div>
<p>{{ $t('entity.calendar.date.while', { duration: dateDuration } )}}</p>
</div>
</template>
</div>
@@ -134,7 +142,7 @@ function deployDeleteModal() {
<template v-if="event.description">
<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 }}
</div>
</template>

View File

@@ -5,6 +5,9 @@ import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar())
const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendar()
const popoverOpen = ref(false)
const { t } = useI18n()
const isLoading = ref(false)
const formErrors = reactive<{ message: string | null }>({
@@ -88,7 +91,7 @@ function handleCancel() {
:collision-padding="60"
:disable-outside-pointer-events="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"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@@ -104,8 +107,14 @@ function handleCancel() {
name="new-event-title"
required
: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 class="col-span-2 my-2 pl-8">
@@ -113,8 +122,13 @@ function handleCancel() {
id="new-event-description"
v-model="eventSkeleton.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 class="col-span-2">
@@ -142,7 +156,10 @@ function handleCancel() {
<div class="flex items-center gap-4">
<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>
@@ -158,13 +175,21 @@ function handleCancel() {
<div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" />
<input
id="new-event-location"
v-model="eventSkeleton.location"
type="text"
name="new-event-location"
: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">
<div class="grow">
<input
id="new-event-location"
v-model="eventSkeleton.location"
type="text"
name="new-event-location"
:placeholder="$t('entity.calendar.event.addLocation')"
: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>

View File

@@ -1,9 +1,14 @@
<script lang="ts" setup>
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 { eventSkeleton, lastActiveEvent, isEditEventModalOpen } = storeToRefs(useCalendar())
const { toast } = useToast()
const { t } = useI18n()
const isLoading = ref(false)
const formErrors = reactive<{ message: string | null }>({
@@ -24,15 +29,22 @@ async function handleAction() {
try {
await updateEventFromSkeleton()
isEditEventModalOpen.value = false
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 {
resetSkeleton()
isLoading.value = false
isEditEventModalOpen.value = false
}
}
@@ -85,15 +97,22 @@ function handleCancel() {
<div class="flex items-center gap-4">
<PhPencilSimpleLine size="20" weight="fill" />
<input
id="new-event-title"
v-model="eventSkeleton.title"
type="text"
name="new-event-title"
required
: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"
>
<div class="grow">
<input
id="new-event-title"
v-model="eventSkeleton.title"
type="text"
name="new-event-title"
required
:placeholder="$t('entity.calendar.event.title')"
: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>
@@ -103,8 +122,12 @@ function handleCancel() {
v-model="eventSkeleton.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"
: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 class="col-span-2">
@@ -133,7 +156,10 @@ function handleCancel() {
<PhTag size="18" weight="fill" />
<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>
@@ -143,7 +169,7 @@ function handleCancel() {
<PhTag size="18" weight="fill" />
<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> -->
@@ -152,13 +178,22 @@ function handleCancel() {
<div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" />
<input
id="new-event-location"
v-model="eventSkeleton.location"
type="text"
name="new-event-location"
: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">
<div class="grow">
<input
id="new-event-location"
v-model="eventSkeleton.location"
type="text"
name="new-event-location"
:placeholder="$t('entity.calendar.event.addLocation')"
: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>

View File

@@ -13,5 +13,5 @@ const delegatedProps = computed(() => {
</script>
<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>

View File

@@ -136,10 +136,21 @@ export default defineI18nConfig(() => ({
addLocation: "Add a place",
prevPage: "Previous 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: {
title: "Edit event",
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: {
title: "Delete this event",
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",
prevPage: "Précédente 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: {
title: "Modifier 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: {
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 !",

View File

@@ -20,9 +20,9 @@ export interface CalendarEvent {
*/
export const postEventBodySchema = z.object({
event: z.object({
title: z.string(),
description: z.string().optional().nullable(),
location: z.string().optional().nullable(),
title: z.string().max(120),
description: z.string().max(1200).optional().nullable(),
location: z.string().max(160).optional().nullable(),
startDate: dateSchema.required(),
endDate: dateSchema.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
export default defineNuxtConfig({
devtools: { enabled: true },
devtools: { enabled: false },
modules: [
"@nuxtjs/supabase",
@@ -16,12 +16,21 @@ export default defineNuxtConfig({
i18n: {
vueI18n: "./i18n.config.ts",
strategy: "no_prefix",
detectBrowserLanguage: {
useCookie: true,
cookieKey: "tttools_lang",
redirectOn: "all",
alwaysRedirect: true,
}
},
locales: [
{
code: "en",
name: "English"
},
{
code: "fr",
name: "Français"
}
]
},
supabase: {

View File

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

View File

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

View File

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

View File

@@ -247,17 +247,13 @@ export const useCalendarEvents = defineStore("calendar-events", () => {
abortController = new AbortController()
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)
baseEvents.value[eventIndex] = res
} catch (err) {
console.log(err)
} finally {
abortController = null
isUpdatingEvent.value = false
}
const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
baseEvents.value[eventIndex] = res
abortController = null
isUpdatingEvent.value = false
}
async function deleteEventFromSkeleton() {

View File

@@ -88,6 +88,15 @@ create table public.calendar_events (
);
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
create table public.calendar_event_categories_links (
calendar_event_id bigint references public.calendar_events on delete cascade,