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
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
import type { NavigationMenuItem } from '@nuxt/ui';
|
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||||
import { version } from '../../package.json';
|
import { version } from '../../package.json';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
const { isMobile } = useDevice();
|
const { isMobile } = useDevice();
|
||||||
const orientation = computed(() => (isMobile ? 'vertical' : 'horizontal'));
|
const orientation = computed(() => (isMobile ? 'vertical' : 'horizontal'));
|
||||||
const { getMeta } = useBackend();
|
const { getMeta } = useBackend();
|
||||||
@@ -48,4 +49,13 @@ const items = computed<NavigationMenuItem[]>(() => [
|
|||||||
to: 'https://rust-lang.org/',
|
to: 'https://rust-lang.org/',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
watch(error, (value) => {
|
||||||
|
if (value) {
|
||||||
|
toast.add({
|
||||||
|
title: $t('backend.errors.title'),
|
||||||
|
description: $t(value.message ?? 'backend.errors.unknown'),
|
||||||
|
color: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import type { FetchError } from 'ofetch';
|
import type { FetchError } from 'ofetch';
|
||||||
@@ -14,10 +16,6 @@ vi.mock('#app', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock $fetch globally
|
// Mock $fetch globally
|
||||||
declare global {
|
|
||||||
var $fetch: ReturnType<typeof vi.fn>;
|
|
||||||
}
|
|
||||||
// global.$fetch = vi.fn();
|
|
||||||
vi.mock('#app', () => ({
|
vi.mock('#app', () => ({
|
||||||
useRuntimeConfig: vi.fn(() => ({
|
useRuntimeConfig: vi.fn(() => ({
|
||||||
public: {
|
public: {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const useBackend = () => {
|
|||||||
|
|
||||||
const getMeta = (): UseApiResponse<MetaResponse> => api.get<MetaResponse>('/meta');
|
const getMeta = (): UseApiResponse<MetaResponse> => api.get<MetaResponse>('/meta');
|
||||||
const postContact = (): UseApiResponse<ContactResponse, ContactRequest> =>
|
const postContact = (): UseApiResponse<ContactResponse, ContactRequest> =>
|
||||||
api.post<ContactResponse, ContactRequest>('/contact', undefined, true);
|
api.post<ContactResponse, ContactRequest>('/contact', undefined, false);
|
||||||
|
|
||||||
return { getMeta, postContact };
|
return { getMeta, postContact };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,18 +43,27 @@
|
|||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
</div>
|
||||||
<UFormField
|
<UFormField
|
||||||
class="w-full"
|
class="w-full sr-only"
|
||||||
name="website"
|
name="website"
|
||||||
:label="$t('pages.contact.form.honeypot')"
|
:label="$t('pages.contact.form.labels.website')"
|
||||||
:ui="{ label: 'text-text text-lg text-bold' }"
|
: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
|
<UInput
|
||||||
|
v-model="state.website"
|
||||||
:ui="{ root: 'relative inline-flex items-center w-full', base: 'placeholder:text-300' }"
|
:ui="{ root: 'relative inline-flex items-center w-full', base: 'placeholder:text-300' }"
|
||||||
:placeholder="$t('pages.contact.form.placeholders.honeypot')"
|
:placeholder="$t('pages.contact.form.placeholders.website')"
|
||||||
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField
|
<UFormField
|
||||||
v-model="state.website"
|
|
||||||
:label="$t('pages.contact.form.labels.message')"
|
:label="$t('pages.contact.form.labels.message')"
|
||||||
name="message"
|
name="message"
|
||||||
:ui="{ label: 'text-text text-lg text-bold' }"
|
:ui="{ label: 'text-text text-lg text-bold' }"
|
||||||
@@ -74,6 +83,7 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
class="w-full text-center text-lg justify-center-safe"
|
class="w-full text-center text-lg justify-center-safe"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
:loading="loading"
|
||||||
>
|
>
|
||||||
{{ $t('pages.contact.form.sendButton') }}
|
{{ $t('pages.contact.form.sendButton') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
@@ -86,6 +96,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormSubmitEvent } from '@nuxt/ui';
|
import type { FormSubmitEvent } from '@nuxt/ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
const { postContact } = useBackend();
|
const { postContact } = useBackend();
|
||||||
|
|
||||||
@@ -115,9 +126,22 @@ const { data: contactResponse, error: contactError, loading, run: sendRequest }
|
|||||||
|
|
||||||
const submitContactForm = async (event: FormSubmitEvent<Schema>) => await sendRequest!(event.data);
|
const submitContactForm = async (event: FormSubmitEvent<Schema>) => await sendRequest!(event.data);
|
||||||
|
|
||||||
watchEffect(() => {
|
watch(contactResponse, async (response) => {
|
||||||
if (loading.value) console.log('loading');
|
if (response) {
|
||||||
if (contactResponse.value) console.log('Response:', contactResponse.value.message);
|
toast.add({
|
||||||
if (contactError.value) console.error('Error', contactError.value);
|
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>
|
</script>
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
"name": "Languages & Worldbuilding"
|
"name": "Languages & Worldbuilding"
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
|
"name": "Contact",
|
||||||
|
"toast": {
|
||||||
|
"success": "Email sent!",
|
||||||
|
"error": "Failure sending message"
|
||||||
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"sendButton": "Send Message",
|
"sendButton": "Send Message",
|
||||||
"validation": {
|
"validation": {
|
||||||
@@ -59,8 +64,7 @@
|
|||||||
"message": "Hello, ...",
|
"message": "Hello, ...",
|
||||||
"website": "https://example.com"
|
"website": "https://example.com"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"name": "Contact"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
@@ -81,7 +85,7 @@
|
|||||||
"unknown": "The website encountered an unknown error. Please try again later."
|
"unknown": "The website encountered an unknown error. Please try again later."
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"success": "Email sent! We’ve also sent you a confirmation email!",
|
"success": "We’ve also sent you a confirmation email. If you haven’t received anything in a few minutes, please check your junk mail.",
|
||||||
"honeypot": "Mmmmmh, I love me some honey from the honeypot!",
|
"honeypot": "Mmmmmh, I love me some honey from the honeypot!",
|
||||||
"errors": {
|
"errors": {
|
||||||
"internal": "The website encountered an internal error. Please try again later.",
|
"internal": "The website encountered an internal error. Please try again later.",
|
||||||
|
|||||||
@@ -39,6 +39,10 @@
|
|||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "Contact",
|
"name": "Contact",
|
||||||
|
"toast": {
|
||||||
|
"success": "Couriel envoyé !",
|
||||||
|
"error": "Erreur lors de l'envoi du message"
|
||||||
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"sendButton": "Envoyer le message",
|
"sendButton": "Envoyer le message",
|
||||||
"validation": {
|
"validation": {
|
||||||
@@ -81,7 +85,7 @@
|
|||||||
"unknown": "Une erreur inconnue est survenue. Veuillez réessayer plus tard."
|
"unknown": "Une erreur inconnue est survenue. Veuillez réessayer plus tard."
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"success": "Message envoyé ! Un email de confirmation vous a été également envoyé.",
|
"success": "Un email de confirmation vous a été également envoyé. Si vous n’avez rien reçu d’ici quelques minutes, vérifiez vos spams.",
|
||||||
"honeypot": "Miam, du bon miel pour le robot !",
|
"honeypot": "Miam, du bon miel pour le robot !",
|
||||||
"errors": {
|
"errors": {
|
||||||
"internal": "Une erreur interne est survenue, veuillez réessayer plus tard.",
|
"internal": "Une erreur interne est survenue, veuillez réessayer plus tard.",
|
||||||
|
|||||||
Reference in New Issue
Block a user