feat(pages): add contact page
This commit is contained in:
@@ -14,7 +14,17 @@ vi.mock('#app', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock $fetch globally
|
// Mock $fetch globally
|
||||||
global.$fetch = vi.fn();
|
declare global {
|
||||||
|
var $fetch: ReturnType<typeof vi.fn>;
|
||||||
|
}
|
||||||
|
// global.$fetch = vi.fn();
|
||||||
|
vi.mock('#app', () => ({
|
||||||
|
useRuntimeConfig: vi.fn(() => ({
|
||||||
|
public: {
|
||||||
|
apiBase: 'http://localhost:3100/api'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
describe('useApi', () => {
|
describe('useApi', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { ApiError } from '~/types/api/error';
|
|||||||
import type { HttpMethod } from '~/types/http-method';
|
import type { HttpMethod } from '~/types/http-method';
|
||||||
import { QueryResult } from '~/types/query-result';
|
import { QueryResult } from '~/types/query-result';
|
||||||
|
|
||||||
|
export type UseApiResponse<T, B = unknown> = QueryResult<T, B>;
|
||||||
|
|
||||||
export interface UseApi {
|
export interface UseApi {
|
||||||
get: <T>(path: string, opts?: FetchOptions, immediate?: boolean) => UseApiResponse<T>;
|
get: <T>(path: string, opts?: FetchOptions, immediate?: boolean) => UseApiResponse<T>;
|
||||||
del: <T>(path: string, opts?: FetchOptions, immediate?: boolean) => UseApiResponse<T>;
|
del: <T>(path: string, opts?: FetchOptions, immediate?: boolean) => UseApiResponse<T>;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import type { ContactRequest, ContactResponse } from '~/types/api/contact';
|
import type { ContactRequest, ContactResponse } from '~/types/api/contact';
|
||||||
import type { MetaResponse } from '~/types/api/meta';
|
import type { MetaResponse } from '~/types/api/meta';
|
||||||
|
import type { UseApiResponse } from './useApi';
|
||||||
|
|
||||||
export const useBackend = () => {
|
export const useBackend = () => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
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', false);
|
api.post<ContactResponse, ContactRequest>('/contact', undefined, true);
|
||||||
|
|
||||||
return { getMeta, postContact };
|
return { getMeta, postContact };
|
||||||
};
|
};
|
||||||
|
|||||||
123
app/pages/contact.vue
Normal file
123
app/pages/contact.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<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"
|
||||||
|
name="website"
|
||||||
|
:label="$t('pages.contact.form.honeypot')"
|
||||||
|
:ui="{ label: 'text-text text-lg text-bold' }"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
:ui="{ root: 'relative inline-flex items-center w-full', base: 'placeholder:text-300' }"
|
||||||
|
:placeholder="$t('pages.contact.form.placeholders.honeypot')"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
v-model="state.website"
|
||||||
|
: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"
|
||||||
|
>
|
||||||
|
{{ $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 { 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);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (loading.value) console.log('loading');
|
||||||
|
if (contactResponse.value) console.log('Response:', contactResponse.value.message);
|
||||||
|
if (contactError.value) console.error('Error', contactError.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -38,6 +38,28 @@
|
|||||||
"name": "Languages & Worldbuilding"
|
"name": "Languages & Worldbuilding"
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
|
"form": {
|
||||||
|
"sendButton": "Send Message",
|
||||||
|
"validation": {
|
||||||
|
"shortName": "Must contain at least one character",
|
||||||
|
"longName": "Cannot exceed 100 characters",
|
||||||
|
"shortMessage": "Must contain at least 10 characters",
|
||||||
|
"longMessage": "Cannot exceed 5000 characters",
|
||||||
|
"invalidEmail": "Invalid email address format"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"name": "Name",
|
||||||
|
"email": "Email Address",
|
||||||
|
"message": "Message",
|
||||||
|
"website": "Website"
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"name": "Alex Taylor",
|
||||||
|
"email": "alex.taylor[at]example.com",
|
||||||
|
"message": "Hello, ...",
|
||||||
|
"website": "https://example.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"name": "Contact"
|
"name": "Contact"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -51,5 +73,25 @@
|
|||||||
"frontend": "Frontend Version",
|
"frontend": "Frontend Version",
|
||||||
"backend": "Backend Version"
|
"backend": "Backend Version"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"backend": {
|
||||||
|
"failed": "Error",
|
||||||
|
"errors": {
|
||||||
|
"title": "There was an error",
|
||||||
|
"unknown": "The website encountered an unknown error. Please try again later."
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"success": "Email sent! We’ve also sent you a confirmation email!",
|
||||||
|
"honeypot": "Mmmmmh, I love me some honey from the honeypot!",
|
||||||
|
"errors": {
|
||||||
|
"internal": "The website encountered an internal error. Please try again later.",
|
||||||
|
"validation": {
|
||||||
|
"name": "Incorrect name format. Must contain from 1 to 50 characters.",
|
||||||
|
"email": "Incorrect email format.",
|
||||||
|
"message": "Incorrect message format. Must contain from 10 to 5000 characters.",
|
||||||
|
"other": "Malformed request."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,29 @@
|
|||||||
"name": "Langues et Univers Fictifs"
|
"name": "Langues et Univers Fictifs"
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "Contact"
|
"name": "Contact",
|
||||||
|
"form": {
|
||||||
|
"sendButton": "Envoyer le message",
|
||||||
|
"validation": {
|
||||||
|
"shortName": "Longueur minimale du nom : 1 caractère",
|
||||||
|
"longName": "Longeur maximale du nom : 100 caractères",
|
||||||
|
"shortMessage": "Longueur minimale du message : 10 caractères",
|
||||||
|
"longMessage": "Longueur maximale du message : 5000 caractères",
|
||||||
|
"invalidEmail": "Format adresse courriel invalide"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"name": "Nom",
|
||||||
|
"email": "Addresse courriel",
|
||||||
|
"message": "Message",
|
||||||
|
"website": "Site web"
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"name": "Dominique Dubois",
|
||||||
|
"email": "dominique.dubois[at]example.com",
|
||||||
|
"message": "Bonjour, ...",
|
||||||
|
"website": "https://example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
@@ -48,8 +70,28 @@
|
|||||||
"rust": "Backend fait avec Rust"
|
"rust": "Backend fait avec Rust"
|
||||||
},
|
},
|
||||||
"versions": {
|
"versions": {
|
||||||
"frontend": "Frontend Version",
|
"frontend": "Version frontend",
|
||||||
"backend": "Backend Version"
|
"backend": "Version backend"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backend": {
|
||||||
|
"failed": "Erreur",
|
||||||
|
"errors": {
|
||||||
|
"title": "Une erreur est survenue",
|
||||||
|
"unknown": "Une erreur inconnue est survenue. Veuillez réessayer plus tard."
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"success": "Message envoyé ! Un email de confirmation vous a été également envoyé.",
|
||||||
|
"honeypot": "Miam, du bon miel pour le robot !",
|
||||||
|
"errors": {
|
||||||
|
"internal": "Une erreur interne est survenue, veuillez réessayer plus tard.",
|
||||||
|
"validation": {
|
||||||
|
"name": "Format du nom incorrect. Doit faire de 1 à 50 caractères.",
|
||||||
|
"email": "Format de l’adresse courriel invalide.",
|
||||||
|
"message": "Format du message invalide. Doit faire entre 10 et 5000 caractères.",
|
||||||
|
"other": "Données de la requête malformées."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ export default defineNuxtConfig({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
vueDevTools: true,
|
vueDevTools: true,
|
||||||
telemetry: false,
|
telemetry: false,
|
||||||
|
|
||||||
|
timeline: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
@@ -66,7 +70,6 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
turnstile: {
|
turnstile: {
|
||||||
siteKey: '', // Overridden by NUXT_PUBLIC_TURNSTILE_SITE_KEY
|
|
||||||
addValidateEndpoint: true
|
addValidateEndpoint: true
|
||||||
},
|
},
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
@@ -76,5 +79,5 @@ export default defineNuxtConfig({
|
|||||||
public: {
|
public: {
|
||||||
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:3100/api',
|
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:3100/api',
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user