All checks were successful
Build and deploy updated apps / Build & deploy (push) Successful in 2m22s
161 lines
5.5 KiB
Vue
161 lines
5.5 KiB
Vue
<template>
|
||
<!-- Contact‑centric layout -->
|
||
<AppFlatSection>
|
||
<!-- Grid: form (fixed max‑width) | host snapshots -->
|
||
<div class="flex flex-col md:flex-row gap-8 items-center">
|
||
<!-- ─── Contact form ─── -->
|
||
<div class="w-full md:w-auto max-w-xl mx-auto lg:mx-0">
|
||
<h1 class="text-3xl sm:text-4xl font-bold mb-4">{{ t('contact.title') }}</h1>
|
||
|
||
<!-- Extended intro -->
|
||
<p class="text-neutral-600 mb-4 max-w-prose">
|
||
{{ t('contact.description') }}
|
||
</p>
|
||
|
||
<!-- Contact shortcuts -->
|
||
<div class="mb-8 space-y-1 text-sm">
|
||
<!-- Phone (phone + WhatsApp icons) -->
|
||
<div class="flex items-center gap-2">
|
||
<!-- Heroicons phone -->
|
||
<UIcon name="i-heroicons-phone" class="w-4 h-4 text-neutral-600"/>
|
||
<!-- WhatsApp icon (any Iconify set you use) -->
|
||
<UIcon name="i-uil-whatsapp" class="w-4 h-4 text-neutral-600"/>
|
||
<a :href="`tel:${t('contact.phone').replace(/\s+/g, '')}`" class="hover:underline text-neutral-600">
|
||
{{ t('contact.phone') }}
|
||
</a>
|
||
</div>
|
||
|
||
<!-- E-mail -->
|
||
<div class="flex items-center gap-2">
|
||
<UIcon name="i-heroicons-envelope" class="w-4 h-4 text-neutral-600"/>
|
||
<a :href="`mailto:${t('contact.email')}`" class="hover:underline text-neutral-600">
|
||
{{ t('contact.email') }}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="text-neutral-600 mb-4 max-w-prose">
|
||
{{ t('contact.online-1') }}
|
||
<UButton :to="variantPath('book')" variant="outline" trailing-icon="i-heroicons-arrow-right">{{
|
||
t('contact.online-2')
|
||
}}
|
||
</UButton>
|
||
{{ t('contact.online-3') }}
|
||
</p>
|
||
|
||
<!-- Form -->
|
||
<UForm :state="state" class="space-y-4" @submit="onSubmit">
|
||
<UFormField name="name" label="Name" :ui="{ label: 'sr-only' }">
|
||
<UInput v-model="state.name" class="w-full" :placeholder="t('contact.form.name')"/>
|
||
</UFormField>
|
||
|
||
<UFormField name="email" label="E-Mail" :ui="{ label: 'sr-only' }">
|
||
<UInput v-model="state.email" type="email" class="w-full"
|
||
:placeholder="t('contact.form.email')"/>
|
||
</UFormField>
|
||
|
||
<UFormField name="subject" label="Betreff" :ui="{ label: 'sr-only' }">
|
||
<UInput v-model="state.subject" class="w-full" :placeholder="t('contact.form.subject')"/>
|
||
</UFormField>
|
||
|
||
<UFormField name="message" label="Nachricht" :ui="{ label: 'sr-only' }">
|
||
<UTextarea v-model="state.message" :rows="6" class="w-full"
|
||
:placeholder="t('contact.form.message')"/>
|
||
</UFormField>
|
||
|
||
<UButton type="submit" color="primary" size="lg" class="w-full sm:w-auto">
|
||
{{ t('contact.form.send') }}
|
||
</UButton>
|
||
</UForm>
|
||
|
||
</div>
|
||
|
||
<!-- ─── Decorative host snapshots ─── -->
|
||
<div id="hosts" class="flex flex-col gap-10 lg:self-center w-full md:w-auto">
|
||
<AppHero
|
||
:src="t('contact.heroes.parents.image')"
|
||
:alt="t('contact.heroes.parents.title')"
|
||
image-side="left"
|
||
:size="50"
|
||
:title="t('contact.heroes.parents.title')"
|
||
:description="t('contact.heroes.parents.description')"
|
||
/>
|
||
|
||
<AppHero
|
||
:src="t('contact.heroes.children.image')"
|
||
:alt="t('contact.heroes.children.title')"
|
||
image-side="right"
|
||
:size="50"
|
||
:title="t('contact.heroes.children.title')"
|
||
:description="t('contact.heroes.children.description')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</AppFlatSection>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
/*TODO form should contain link to privacy statement?*/
|
||
const {t, tm, rt} = useVariantData()
|
||
const { variantPath } = useVariantPath()
|
||
import * as v from 'valibot'
|
||
import type {FormSubmitEvent} from '@nuxt/ui'
|
||
|
||
/* ───── validation schema ───── */
|
||
const schema = v.object({
|
||
name: v.pipe(v.string(), v.minLength(2, 'Bitte Namen eingeben')),
|
||
email: v.pipe(v.string(), v.email('Ungültige E-Mail')),
|
||
subject: v.pipe(v.string(), v.minLength(3, 'Betreff fehlt')),
|
||
message: v.pipe(v.string(), v.minLength(10, 'Nachricht ist zu kurz'))
|
||
})
|
||
type Schema = v.InferOutput<typeof schema>
|
||
|
||
/* ───── reactive form state ───── */
|
||
const state = reactive<Schema>({
|
||
name: '', email: '', subject: '', message: ''
|
||
})
|
||
|
||
const toast = useToast()
|
||
|
||
/* ───── submit handler ───── */
|
||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||
const config = useRuntimeConfig()
|
||
const hotelId = config.public.hotelId
|
||
|
||
if (!hotelId) {
|
||
toast.add({
|
||
title: 'Fehler',
|
||
description: 'Hotel-ID ist nicht konfiguriert!',
|
||
color: 'red'
|
||
})
|
||
throw new Error('Hotel ID not configured')
|
||
}
|
||
|
||
try {
|
||
await $fetch('https://api.dominikmilacher.com/contact', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: {
|
||
...event.data,
|
||
hotel: hotelId
|
||
}
|
||
})
|
||
|
||
toast.add({
|
||
title: 'Nachricht gesendet',
|
||
description: 'Vielen Dank – wir melden uns bald.',
|
||
color: 'green'
|
||
})
|
||
|
||
/* reset fields */
|
||
Object.assign(state, {name: '', email: '', subject: '', message: ''})
|
||
} catch (err: any) {
|
||
toast.add({
|
||
title: 'Fehler',
|
||
description: err?.data?.detail ?? err?.message ?? 'Nachricht konnte nicht gesendet werden.',
|
||
color: 'red'
|
||
})
|
||
}
|
||
}
|
||
|
||
</script> |