feat(landing): Add kontakt, datenschutz pages, integrate brevo newsletter and contact, add error-handler
This commit is contained in:
299
landing/app/pages/kontakt.vue
Normal file
299
landing/app/pages/kontakt.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div class="min-h-screen pt-24 pb-16">
|
||||
<!-- Background with gradient -->
|
||||
<div class="fixed inset-0 -z-10">
|
||||
<div
|
||||
class="absolute inset-0 bg-linear-to-br from-primary-50 via-cyan-50 to-accent-50 dark:from-gray-950 dark:via-primary-950/30 dark:to-accent-950/30"
|
||||
/>
|
||||
<!-- Gradient orbs -->
|
||||
<div class="absolute top-20 left-1/4 w-96 h-96 bg-primary-400/20 rounded-full blur-3xl animate-orb-float" />
|
||||
<div
|
||||
class="absolute bottom-20 right-1/4 w-80 h-80 bg-accent-400/20 rounded-full blur-3xl animate-orb-float"
|
||||
style="animation-delay: 3s"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Header section -->
|
||||
<div class="text-center mb-12">
|
||||
<!-- Badge -->
|
||||
<span
|
||||
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-sm font-medium mb-6"
|
||||
>
|
||||
<UIcon name="i-lucide-message-circle" class="w-4 h-4" />
|
||||
{{ $t('contact.badge') }}
|
||||
</span>
|
||||
|
||||
<!-- Title -->
|
||||
<h1
|
||||
class="font-heading text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-white mb-6 animate-fade-in-up"
|
||||
>
|
||||
{{ $t('contact.title', { highlight: '' })
|
||||
}}<span class="gradient-text">{{ $t('contact.titleHighlight') }}</span>
|
||||
</h1>
|
||||
|
||||
<!-- Description -->
|
||||
<p
|
||||
class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto animate-fade-in-up"
|
||||
style="animation-delay: 100ms"
|
||||
>
|
||||
{{ $t('contact.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Form section -->
|
||||
<div class="animate-fade-in-up" style="animation-delay: 150ms">
|
||||
<div class="glass rounded-3xl p-8 sm:p-10 shadow-2xl">
|
||||
<!-- Success state -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-500 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition-all duration-300 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div v-if="isSuccess" class="text-center py-8">
|
||||
<div
|
||||
class="w-20 h-20 mx-auto mb-6 rounded-full bg-success-100 dark:bg-success-900/30 flex items-center justify-center"
|
||||
>
|
||||
<UIcon name="i-lucide-check-circle" class="w-10 h-10 text-success-600 dark:text-success-400" />
|
||||
</div>
|
||||
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-3">
|
||||
{{ $t('contact.success.title') }}
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-8">
|
||||
{{ $t('contact.success.message') }}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<UButton to="/" variant="outline" class="btn-outline-gradient">
|
||||
<UIcon name="i-lucide-home" class="w-4 h-4 mr-2" />
|
||||
{{ $t('contact.success.backToHome') }}
|
||||
</UButton>
|
||||
<UButton class="btn-gradient" @click="resetForm">
|
||||
<UIcon name="i-lucide-mail-plus" class="w-4 h-4 mr-2" />
|
||||
{{ $t('contact.success.sendAnother') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Form -->
|
||||
<UForm v-if="!isSuccess" :state="formState" :schema="schema" class="space-y-6" @submit="onSubmit">
|
||||
<!-- Name field -->
|
||||
<UFormField :label="$t('contact.form.name')" name="name" required class="w-full">
|
||||
<UInput
|
||||
v-model="formState.name"
|
||||
size="xl"
|
||||
:disabled="isLoading"
|
||||
autocomplete="name"
|
||||
class="w-full"
|
||||
:ui="{
|
||||
base: 'w-full rounded-xl bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 focus:ring-2 focus:ring-primary-500 focus:border-transparent'
|
||||
}"
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-lucide-user" class="w-5 h-5 text-gray-400" />
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<!-- Email field -->
|
||||
<UFormField :label="$t('contact.form.email')" name="email" required class="w-full">
|
||||
<UInput
|
||||
v-model="formState.email"
|
||||
type="email"
|
||||
size="xl"
|
||||
:disabled="isLoading"
|
||||
autocomplete="email"
|
||||
class="w-full"
|
||||
:ui="{
|
||||
base: 'w-full rounded-xl bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 focus:ring-2 focus:ring-primary-500 focus:border-transparent'
|
||||
}"
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-lucide-mail" class="w-5 h-5 text-gray-400" />
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<!-- Message field -->
|
||||
<UFormField :label="$t('contact.form.message')" name="message" required class="w-full">
|
||||
<UTextarea
|
||||
v-model="formState.message"
|
||||
:rows="6"
|
||||
:disabled="isLoading"
|
||||
class="w-full"
|
||||
size="xl"
|
||||
:ui="{
|
||||
base: 'w-full rounded-xl bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none'
|
||||
}"
|
||||
/>
|
||||
<template #hint>
|
||||
<span class="text-xs text-gray-400"> {{ formState.message.length }} / 5000 </span>
|
||||
</template>
|
||||
</UFormField>
|
||||
|
||||
<!-- Error message -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
>
|
||||
<div
|
||||
v-if="error"
|
||||
class="p-4 rounded-xl bg-error-100 dark:bg-error-900/30 border border-error-200 dark:border-error-800"
|
||||
role="alert"
|
||||
>
|
||||
<div class="flex items-center gap-3 text-error-700 dark:text-error-300">
|
||||
<UIcon name="i-lucide-alert-circle" class="w-5 h-5 shrink-0" />
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Submit button -->
|
||||
<UButton
|
||||
type="submit"
|
||||
size="xl"
|
||||
block
|
||||
:loading="isLoading"
|
||||
class="btn-gradient rounded-xl font-semibold shadow-lg shadow-primary-500/25"
|
||||
>
|
||||
<template v-if="isLoading">
|
||||
<span>{{ $t('contact.form.sending') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<UIcon name="i-lucide-send" class="w-5 h-5 mr-2" />
|
||||
<span>{{ $t('contact.form.submit') }}</span>
|
||||
</template>
|
||||
</UButton>
|
||||
</UForm>
|
||||
</div>
|
||||
|
||||
<!-- Trust indicators -->
|
||||
<div class="mt-8 flex flex-wrap justify-center gap-6 animate-fade-in-up" style="animation-delay: 300ms">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div class="w-6 h-6 rounded-full bg-success-100 dark:bg-success-900/30 flex items-center justify-center">
|
||||
<UIcon name="i-lucide-shield-check" class="w-3.5 h-3.5 text-success-600 dark:text-success-400" />
|
||||
</div>
|
||||
<span>{{ $t('contact.trust.gdpr') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div class="w-6 h-6 rounded-full bg-success-100 dark:bg-success-900/30 flex items-center justify-center">
|
||||
<UIcon name="i-lucide-lock" class="w-3.5 h-3.5 text-success-600 dark:text-success-400" />
|
||||
</div>
|
||||
<span>{{ $t('contact.trust.encrypted') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div class="w-6 h-6 rounded-full bg-success-100 dark:bg-success-900/30 flex items-center justify-center">
|
||||
<UIcon name="i-lucide-clock" class="w-3.5 h-3.5 text-success-600 dark:text-success-400" />
|
||||
</div>
|
||||
<span>{{ $t('contact.trust.responseTime') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
import type { ContactFormData } from '~/composables/useContactForm'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { isLoading, isSuccess, error, submitForm, reset } = useContactForm()
|
||||
|
||||
// SEO Meta
|
||||
useSeoMeta({
|
||||
title: () => t('contact.meta.title'),
|
||||
description: () => t('contact.meta.description'),
|
||||
ogTitle: () => t('contact.meta.title'),
|
||||
ogDescription: () => t('contact.meta.description'),
|
||||
ogImage: '/og-image.png',
|
||||
ogType: 'website',
|
||||
twitterCard: 'summary_large_image'
|
||||
})
|
||||
|
||||
// Structured data
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: () => locale.value
|
||||
},
|
||||
script: [
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ContactPage',
|
||||
name: t('contact.meta.title'),
|
||||
description: t('contact.meta.description')
|
||||
})
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Form validation schema
|
||||
const schema = computed(() =>
|
||||
z.object({
|
||||
name: z.string().min(1, t('contact.validation.nameRequired')).max(100, t('contact.validation.nameMax')),
|
||||
email: z.string().min(1, t('contact.validation.emailRequired')).email(t('contact.validation.emailInvalid')),
|
||||
message: z.string().min(10, t('contact.validation.messageMin')).max(5000, t('contact.validation.messageMax'))
|
||||
})
|
||||
)
|
||||
|
||||
// Form state
|
||||
const formState = reactive<ContactFormData>({
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
// Submit handler
|
||||
async function onSubmit(event: FormSubmitEvent<ContactFormData>) {
|
||||
await submitForm(event.data)
|
||||
}
|
||||
|
||||
// Reset form
|
||||
function resetForm() {
|
||||
formState.name = ''
|
||||
formState.email = ''
|
||||
formState.message = ''
|
||||
reset()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Orb float animation */
|
||||
@keyframes orb-float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-orb-float {
|
||||
animation: orb-float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Fade in up animation */
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user