feat(pages): add contact page

This commit is contained in:
2025-11-19 22:03:35 +01:00
parent 0b65e17903
commit 10e51b5da4
7 changed files with 230 additions and 7 deletions

View File

@@ -14,7 +14,17 @@ vi.mock('#app', () => ({
}));
// 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', () => {
beforeEach(() => {

View File

@@ -3,6 +3,8 @@ import type { ApiError } from '~/types/api/error';
import type { HttpMethod } from '~/types/http-method';
import { QueryResult } from '~/types/query-result';
export type UseApiResponse<T, B = unknown> = QueryResult<T, B>;
export interface UseApi {
get: <T>(path: string, opts?: FetchOptions, immediate?: boolean) => UseApiResponse<T>;
del: <T>(path: string, opts?: FetchOptions, immediate?: boolean) => UseApiResponse<T>;

View File

@@ -1,12 +1,13 @@
import type { ContactRequest, ContactResponse } from '~/types/api/contact';
import type { MetaResponse } from '~/types/api/meta';
import type { UseApiResponse } from './useApi';
export const useBackend = () => {
const api = useApi();
const getMeta = (): UseApiResponse<MetaResponse> => api.get<MetaResponse>('/meta');
const postContact = (): UseApiResponse<ContactResponse, ContactRequest> =>
api.post<ContactResponse, ContactRequest>('/contact', false);
api.post<ContactResponse, ContactRequest>('/contact', undefined, true);
return { getMeta, postContact };
};

123
app/pages/contact.vue Normal file
View 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>