Merge pull request #45 from AlexisNP/features/add-limit-to-event-text-fields
Add limit to event text fields
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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-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>
|
||||
|
||||
@@ -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-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 invalid:border-red-500"
|
||||
>
|
||||
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.location') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 !",
|
||||
|
||||
@@ -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
14
models/Errors.ts
Normal 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[]
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user