Files
framit/app/pages/contact.vue
Lucien Cartier-Tilet 37972aa660 feat(contact): add toast notifications for form feedback
- Add toast notifications for contact form success/error responses
- Add toast notifications for backend errors in AppFooter
- Add accessibility explanation for honeypot field
- Add loading state to contact form submit button
- Add i18n translations for toast messages (en/fr)
- Fix honeypot input missing v-model binding
2026-02-05 13:06:38 +01:00

148 lines
4.9 KiB
Vue

<template>
<NuxtLayout name="default">
<UPage>
<h1 class="text-4xl text-highlighted font-bold mb-8">
{{ $t('pages.contact.name') }}
</h1>
<UPageCard class="bg-background-100">
<UForm :schema="schema" :state="state" class="space-y-4" @submit="submitContactForm">
<div class="flex flex-row w-full gap-5">
<UFormField
:label="$t('pages.contact.form.labels.name')"
name="name"
class="w-full"
:ui="{ label: 'text-text text-lg text-bold' }"
>
<UInput
v-model="state.name"
autofocus
:ui="{
root: 'relative inline-flex items-center w-full',
base: 'placeholder:text-300',
}"
required
:placeholder="$t('pages.contact.form.placeholders.name')"
/>
</UFormField>
<UFormField
:label="$t('pages.contact.form.labels.email')"
name="email"
class="w-full"
:ui="{ label: 'text-text text-lg text-bold' }"
>
<UInput
v-model="state.email"
type="email"
:ui="{
root: 'relative inline-flex items-center w-full',
base: 'placeholder:text-300',
}"
required
:placeholder="$t('pages.contact.form.placeholders.email')"
/>
</UFormField>
</div>
<UFormField
class="w-full sr-only"
name="website"
:label="$t('pages.contact.form.labels.website')"
:ui="{ label: 'text-text text-lg text-bold' }"
tabindex="-1"
>
<div>
If you see this input, you may be using accessibility tools to access this website. This input is meant to
be hidden to human visitors, but not bots which do not necessarily render the website the way it is meant
to. Unfortunately, this also affects accessibility tools, such as the ones for visually-impared people. If
that is indeed, please ignore this input, as it is not meant to be filled by human beings. Filling this
input will result in a discarded contact form.
</div>
<UInput
v-model="state.website"
:ui="{ root: 'relative inline-flex items-center w-full', base: 'placeholder:text-300' }"
:placeholder="$t('pages.contact.form.placeholders.website')"
tabindex="-1"
/>
</UFormField>
<UFormField
:label="$t('pages.contact.form.labels.message')"
name="message"
:ui="{ label: 'text-text text-lg text-bold' }"
>
<UTextarea
v-model="state.message"
:ui="{
root: 'relative inline-flex items-center w-full',
base: 'placeholder:text-300',
}"
:placeholder="$t('pages.contact.form.placeholders.message')"
/>
</UFormField>
<UButton
icon="mdi:send-outline"
color="primary"
type="submit"
class="w-full text-center text-lg justify-center-safe"
size="lg"
:loading="loading"
>
{{ $t('pages.contact.form.sendButton') }}
</UButton>
</UForm>
</UPageCard>
</UPage>
</NuxtLayout>
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui';
import { z } from 'zod';
const toast = useToast();
const { postContact } = useBackend();
const schema = z.object({
email: z.email($t('pages.contact.form.validation.invalidEmail')),
name: z
.string()
.min(1, $t('pages.contact.form.validation.shortName'))
.max(100, $t('pages.contact.form.validation.longName')),
message: z
.string()
.min(10, $t('pages.contact.form.validation.shortMessage'))
.max(5000, $t('pages.contact.form.validation.longMessage')),
website: z.string().optional(),
});
type Schema = z.output<typeof schema>;
const state = reactive<Partial<Schema>>({
name: undefined,
email: undefined,
message: undefined,
website: undefined,
});
const { data: contactResponse, error: contactError, loading, run: sendRequest } = postContact();
const submitContactForm = async (event: FormSubmitEvent<Schema>) => await sendRequest!(event.data);
watch(contactResponse, async (response) => {
if (response) {
toast.add({
title: response.success ? $t('pages.contact.toast.success') : $t('pages.contact.toast.error'),
description: $t(response.message),
color: response.success ? 'info' : 'error',
});
}
});
watch(contactError, async (response) => {
if (response) {
toast.add({
title: $t('pages.contact.toast.error'),
description: $t(response.message),
color: 'error',
});
}
});
</script>