feat(landing): Add kontakt, datenschutz pages, integrate brevo newsletter and contact, add error-handler

This commit is contained in:
2026-01-11 10:57:45 +01:00
parent b5417ef7c6
commit d8b7e193a9
25 changed files with 1066 additions and 53 deletions

View File

@@ -20,12 +20,12 @@
<template #title>
<NuxtLink to="/" class="flex items-center gap-2 sm:gap-3 group">
<div
class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-cyan-500 flex items-center justify-center shadow-lg shadow-primary-500/25 group-hover:shadow-primary-500/40 transition-shadow"
class="w-10 h-10 rounded-xl bg-linear-to-br from-primary-500 to-cyan-500 flex items-center justify-center shadow-lg shadow-primary-500/25 group-hover:shadow-primary-500/40 transition-shadow"
>
<UIcon name="i-lucide-scale" class="w-5 h-5 text-white" />
</div>
<span
class="hidden sm:inline font-heading text-xl font-bold bg-gradient-to-r from-primary-600 to-cyan-600 bg-clip-text text-transparent"
class="hidden sm:inline font-heading text-xl font-bold bg-linear-to-r from-primary-600 to-cyan-600 bg-clip-text text-transparent"
>
GremiumHub
</span>
@@ -62,7 +62,7 @@
}"
/>
<UButton
to="#newsletter"
to="/#newsletter"
class="hidden sm:flex btn-gradient px-5 py-2.5 rounded-xl font-semibold shadow-lg shadow-primary-500/25 hover:shadow-primary-500/40 transition-shadow"
>
<span>{{ $t('common.stayInformed') }}</span>
@@ -99,7 +99,7 @@
<div class="md:col-span-2">
<NuxtLink to="/" class="flex items-center gap-3 mb-4">
<div
class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-cyan-500 flex items-center justify-center"
class="w-10 h-10 rounded-xl bg-linear-to-br from-primary-500 to-cyan-500 flex items-center justify-center"
>
<UIcon name="i-lucide-scale" class="w-5 h-5 text-white" />
</div>
@@ -190,12 +190,14 @@ const { t, locale, locales, setLocale } = useI18n()
// Locale dropdown menu items
const localeMenuItems = computed<DropdownMenuItem[]>(() =>
(locales.value as Array<{ code: string; name: string }>).map((l) => ({
label: l.name,
icon: locale.value === l.code ? 'i-lucide-check' : undefined,
active: locale.value === l.code,
onSelect: () => setLocale(l.code as 'en' | 'de')
}))
(locales.value as Array<{ code: string; name: string }>)
.map((l) => ({
label: l.name,
icon: locale.value === l.code ? 'i-lucide-check' : undefined,
active: locale.value === l.code,
onSelect: () => setLocale(l.code as 'en' | 'de')
}))
.reverse()
)
// Track scroll position for header styling
@@ -237,7 +239,7 @@ const navigationItems = computed<NavigationMenuItem[]>(() => [
},
{
label: t('nav.contact'),
to: '/#kontakt',
to: '/kontakt',
active: false
}
])

View File

@@ -40,7 +40,7 @@
>
<!-- Gradient border on hover -->
<div
class="absolute inset-0 rounded-2xl bg-gradient-to-br from-primary-500 via-cyan-500 to-accent-500 opacity-0 group-hover:opacity-100 transition-opacity -z-10"
class="absolute inset-0 rounded-2xl bg-linear-to-br from-primary-500 via-cyan-500 to-accent-500 opacity-0 group-hover:opacity-100 transition-opacity -z-10"
/>
<div class="absolute inset-[2px] rounded-[14px] bg-white dark:bg-gray-900 -z-10" />
@@ -48,7 +48,7 @@
<!-- Icon with gradient background -->
<div class="relative shrink-0">
<div
class="w-14 h-14 rounded-2xl bg-gradient-to-br from-primary-100 to-cyan-100 dark:from-primary-900/50 dark:to-cyan-900/50 flex items-center justify-center group-hover:scale-110 transition-transform"
class="w-14 h-14 rounded-2xl bg-linear-to-br from-primary-100 to-cyan-100 dark:from-primary-900/50 dark:to-cyan-900/50 flex items-center justify-center group-hover:scale-110 transition-transform"
>
<UIcon :name="feature.icon" class="w-7 h-7 text-primary-600 dark:text-primary-400" />
</div>

View File

@@ -34,7 +34,7 @@
<div class="space-y-6">
<div class="flex items-center gap-3 mb-6">
<div
class="w-12 h-12 rounded-2xl bg-gradient-to-br from-warning-500 to-orange-500 flex items-center justify-center shadow-lg shadow-warning-500/25"
class="w-12 h-12 rounded-2xl bg-linear-to-br from-warning-500 to-orange-500 flex items-center justify-center shadow-lg shadow-warning-500/25"
>
<UIcon name="i-lucide-x" class="w-6 h-6 text-white" />
</div>
@@ -67,7 +67,7 @@
<div class="space-y-6">
<div class="flex items-center gap-3 mb-6">
<div
class="w-12 h-12 rounded-2xl bg-gradient-to-br from-success-500 to-emerald-500 flex items-center justify-center shadow-lg shadow-success-500/25"
class="w-12 h-12 rounded-2xl bg-linear-to-br from-success-500 to-emerald-500 flex items-center justify-center shadow-lg shadow-success-500/25"
>
<UIcon name="i-lucide-check" class="w-6 h-6 text-white" />
</div>
@@ -116,7 +116,7 @@
class="h-full p-6 rounded-2xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:shadow-xl hover:shadow-accent-500/10 hover:border-accent-200 dark:hover:border-accent-700 transition-all"
>
<div
class="w-12 h-12 rounded-2xl bg-gradient-to-br from-accent-500 to-violet-500 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform shadow-lg shadow-accent-500/25"
class="w-12 h-12 rounded-2xl bg-linear-to-br from-accent-500 to-violet-500 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform shadow-lg shadow-accent-500/25"
>
<UIcon :name="highlight.icon" class="w-6 h-6 text-white" />
</div>

View File

@@ -34,7 +34,7 @@
<div class="space-y-6">
<div class="flex items-center gap-3 mb-6">
<div
class="w-12 h-12 rounded-2xl bg-gradient-to-br from-warning-500 to-orange-500 flex items-center justify-center shadow-lg shadow-warning-500/25"
class="w-12 h-12 rounded-2xl bg-linear-to-br from-warning-500 to-orange-500 flex items-center justify-center shadow-lg shadow-warning-500/25"
>
<UIcon name="i-lucide-x" class="w-6 h-6 text-white" />
</div>
@@ -67,7 +67,7 @@
<div class="space-y-6">
<div class="flex items-center gap-3 mb-6">
<div
class="w-12 h-12 rounded-2xl bg-gradient-to-br from-success-500 to-emerald-500 flex items-center justify-center shadow-lg shadow-success-500/25"
class="w-12 h-12 rounded-2xl bg-linear-to-br from-success-500 to-emerald-500 flex items-center justify-center shadow-lg shadow-success-500/25"
>
<UIcon name="i-lucide-check" class="w-6 h-6 text-white" />
</div>
@@ -116,7 +116,7 @@
class="h-full p-6 rounded-2xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:shadow-xl hover:shadow-primary-500/10 hover:border-primary-200 dark:hover:border-primary-700 transition-all"
>
<div
class="w-12 h-12 rounded-2xl bg-gradient-to-br from-primary-500 to-cyan-500 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform shadow-lg shadow-primary-500/25"
class="w-12 h-12 rounded-2xl bg-linear-to-br from-primary-500 to-cyan-500 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform shadow-lg shadow-primary-500/25"
>
<UIcon :name="highlight.icon" class="w-6 h-6 text-white" />
</div>

View File

@@ -4,7 +4,7 @@
<div class="absolute inset-0 mesh-gradient-cta" />
<!-- Animated gradient overlay -->
<div class="absolute inset-0 bg-gradient-to-br from-primary-600/90 via-cyan-600/90 to-accent-600/90" />
<div class="absolute inset-0 bg-linear-to-br from-primary-600/90 via-cyan-600/90 to-accent-600/90" />
<!-- Grid pattern -->
<div class="absolute inset-0 grid-pattern opacity-10" />

View File

@@ -34,7 +34,7 @@
:style="{ animationDelay: `${index * 100}ms` }"
>
<div
class="mt-1 w-6 h-6 rounded-full bg-gradient-to-br from-primary-500 to-cyan-500 flex items-center justify-center shrink-0"
class="mt-1 w-6 h-6 rounded-full bg-linear-to-br from-primary-500 to-cyan-500 flex items-center justify-center shrink-0"
>
<UIcon name="i-lucide-check" class="w-3.5 h-3.5 text-white" />
</div>
@@ -44,11 +44,11 @@
<!-- Price info -->
<div
class="p-4 rounded-xl bg-gradient-to-r from-primary-50 to-cyan-50 dark:from-primary-950/50 dark:to-cyan-950/50 border border-primary-200 dark:border-primary-800"
class="p-4 rounded-xl bg-linear-to-r from-primary-50 to-cyan-50 dark:from-primary-950/50 dark:to-cyan-950/50 border border-primary-200 dark:border-primary-800"
>
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-cyan-500 flex items-center justify-center shrink-0"
class="w-10 h-10 rounded-xl bg-linear-to-br from-primary-500 to-cyan-500 flex items-center justify-center shrink-0"
>
<UIcon name="i-lucide-tag" class="w-5 h-5 text-white" />
</div>
@@ -92,7 +92,7 @@
class="relative bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-700 shadow-2xl overflow-hidden w-full max-w-[320px] sm:max-w-sm"
>
<!-- Document header -->
<div class="bg-gradient-to-r from-primary-600 to-cyan-600 px-6 py-5">
<div class="bg-linear-to-r from-primary-600 to-cyan-600 px-6 py-5">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-white/20 backdrop-blur-sm flex items-center justify-center">
<UIcon name="i-lucide-file-text" class="w-6 h-6 text-white" />
@@ -123,7 +123,7 @@
:style="{ animationDelay: `${index * 100}ms` }"
>
<div
class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-100 to-cyan-100 dark:from-primary-900/50 dark:to-cyan-900/50 flex items-center justify-center text-sm font-bold text-primary-600 dark:text-primary-400 group-hover:scale-110 transition-transform"
class="w-8 h-8 rounded-lg bg-linear-to-br from-primary-100 to-cyan-100 dark:from-primary-900/50 dark:to-cyan-900/50 flex items-center justify-center text-sm font-bold text-primary-600 dark:text-primary-400 group-hover:scale-110 transition-transform"
>
{{ index + 1 }}
</div>
@@ -159,7 +159,7 @@
<!-- Floating badge - hidden on very small screens to prevent overflow -->
<div class="absolute -top-4 right-0 sm:-right-4 z-10 animate-float-fast">
<div
class="bg-gradient-to-r from-accent-500 to-violet-500 text-white px-3 sm:px-4 py-2 rounded-full shadow-lg text-xs sm:text-sm font-semibold flex items-center gap-2"
class="bg-linear-to-r from-accent-500 to-violet-500 text-white px-3 sm:px-4 py-2 rounded-full shadow-lg text-xs sm:text-sm font-semibold flex items-center gap-2"
>
<UIcon name="i-lucide-download" class="w-4 h-4" />
{{ $t('common.onRequest') }}

View File

@@ -127,7 +127,7 @@
>
<div class="glass rounded-xl px-4 py-3 shadow-lg flex items-center gap-3">
<div
class="w-8 h-8 rounded-full bg-gradient-to-br from-success-400 to-success-500 flex items-center justify-center"
class="w-8 h-8 rounded-full bg-linear-to-br from-success-400 to-success-500 flex items-center justify-center"
>
<UIcon name="i-lucide-sparkles" class="w-4 h-4 text-white" />
</div>

View File

@@ -2,7 +2,7 @@
<section id="newsletter" class="relative py-16 lg:py-20 overflow-hidden scroll-mt-20">
<!-- Background with gradient and particles -->
<div
class="absolute inset-0 bg-gradient-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"
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"
/>
<!-- Animated particles -->
@@ -31,7 +31,7 @@
<!-- Icon -->
<div class="flex justify-center mb-6">
<div
class="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary-500 to-cyan-500 flex items-center justify-center animate-float-slow shadow-lg shadow-primary-500/25"
class="w-16 h-16 rounded-2xl bg-linear-to-br from-primary-500 to-cyan-500 flex items-center justify-center animate-float-slow shadow-lg shadow-primary-500/25"
>
<UIcon name="i-lucide-mail" class="w-8 h-8 text-white" />
</div>

View File

@@ -1,6 +1,6 @@
<template>
<section
class="py-10 lg:py-12 bg-gradient-to-r from-primary-50 via-cyan-50 to-accent-50 dark:from-primary-950/30 dark:via-cyan-950/30 dark:to-accent-950/30"
class="py-10 lg:py-12 bg-linear-to-r from-primary-50 via-cyan-50 to-accent-50 dark:from-primary-950/30 dark:via-cyan-950/30 dark:to-accent-950/30"
>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Stats grid -->
@@ -39,7 +39,7 @@
:style="{ animationDelay: `${(index + 4) * 100}ms` }"
>
<div
class="w-8 h-8 rounded-full bg-gradient-to-br from-primary-100 to-cyan-100 dark:from-primary-900/50 dark:to-cyan-900/50 flex items-center justify-center"
class="w-8 h-8 rounded-full bg-linear-to-br from-primary-100 to-cyan-100 dark:from-primary-900/50 dark:to-cyan-900/50 flex items-center justify-center"
>
<UIcon :name="badge.icon" class="w-4 h-4 text-primary-600 dark:text-primary-400" />
</div>

View File

@@ -57,7 +57,7 @@
<!-- Profile image with gradient border -->
<div class="relative mb-6">
<div
class="absolute -inset-1 rounded-full bg-gradient-to-br from-primary-500 via-cyan-500 to-accent-500 opacity-75 group-hover:opacity-100 blur-sm transition-opacity"
class="absolute -inset-1 rounded-full bg-linear-to-br from-primary-500 via-cyan-500 to-accent-500 opacity-75 group-hover:opacity-100 blur-sm transition-opacity"
aria-hidden="true"
/>
<div
@@ -105,7 +105,7 @@
<!-- Profile image with gradient border -->
<div class="relative mb-6">
<div
class="absolute -inset-1 rounded-full bg-gradient-to-br from-primary-500 via-cyan-500 to-accent-500 opacity-75 group-hover:opacity-100 blur-sm transition-opacity"
class="absolute -inset-1 rounded-full bg-linear-to-br from-primary-500 via-cyan-500 to-accent-500 opacity-75 group-hover:opacity-100 blur-sm transition-opacity"
aria-hidden="true"
/>
<div

View File

@@ -6,7 +6,7 @@
>
<div class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-cyan-500 flex items-center justify-center"
class="w-8 h-8 rounded-lg bg-linear-to-br from-primary-500 to-cyan-500 flex items-center justify-center"
>
<UIcon name="i-lucide-file-text" class="w-4 h-4 text-white" />
</div>
@@ -35,7 +35,7 @@
<span class="text-primary-600 dark:text-primary-400 font-semibold">67%</span>
</div>
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div class="h-full w-[67%] bg-gradient-to-r from-primary-500 to-cyan-500 rounded-full" />
<div class="h-full w-[67%] bg-linear-to-r from-primary-500 to-cyan-500 rounded-full" />
</div>
</div>
@@ -71,7 +71,7 @@
<!-- Action button -->
<button
class="w-full py-3 px-4 rounded-xl bg-gradient-to-r from-primary-500 to-cyan-500 text-white font-semibold text-sm hover:from-primary-600 hover:to-cyan-600 transition-all shadow-lg shadow-primary-500/25"
class="w-full py-3 px-4 rounded-xl bg-linear-to-r from-primary-500 to-cyan-500 text-white font-semibold text-sm hover:from-primary-600 hover:to-cyan-600 transition-all shadow-lg shadow-primary-500/25"
>
{{ $t('hero.cards.continueEditing') }}
</button>

View File

@@ -5,7 +5,7 @@
>
<div :class="compact ? 'flex items-center gap-2 mb-3' : 'flex items-center gap-3 mb-4'">
<div
class="bg-gradient-to-br from-success-400 to-success-500 flex items-center justify-center shrink-0"
class="bg-linear-to-br from-success-400 to-success-500 flex items-center justify-center shrink-0"
:class="compact ? 'w-8 h-8 rounded-lg' : 'w-10 h-10 rounded-xl'"
>
<UIcon name="i-lucide-check-circle" :class="compact ? 'w-4 h-4' : 'w-5 h-5'" class="text-white" />

View File

@@ -5,7 +5,7 @@
>
<div :class="compact ? 'flex items-center gap-2 mb-3' : 'flex items-center gap-3 mb-4'">
<div
class="bg-gradient-to-br from-warning-400 to-warning-500 flex items-center justify-center shrink-0"
class="bg-linear-to-br from-warning-400 to-warning-500 flex items-center justify-center shrink-0"
:class="compact ? 'w-8 h-8 rounded-lg' : 'w-10 h-10 rounded-xl'"
>
<UIcon name="i-lucide-alert-triangle" :class="compact ? 'w-4 h-4' : 'w-5 h-5'" class="text-white" />

View File

@@ -0,0 +1,46 @@
export interface ContactFormData {
name: string
email: string
message: string
}
export function useContactForm() {
const { t } = useI18n()
const isLoading = ref(false)
const isSuccess = ref(false)
const error = ref<string | null>(null)
const submitForm = async (data: ContactFormData) => {
isLoading.value = true
error.value = null
try {
await $fetch('/api/contact/send', {
method: 'POST',
body: data
})
isSuccess.value = true
} catch (e: unknown) {
const fetchError = e as { statusMessage?: string }
error.value = fetchError.statusMessage || t('errors.generic')
throw e
} finally {
isLoading.value = false
}
}
const reset = () => {
isLoading.value = false
isSuccess.value = false
error.value = null
}
return {
isLoading,
isSuccess,
error,
submitForm,
reset
}
}

View File

@@ -4,24 +4,20 @@ export function useNewsletterSignup() {
const isSuccess = ref(false)
const error = ref<string | null>(null)
const submitEmail = async (_email: string) => {
const submitEmail = async (email: string) => {
isLoading.value = true
error.value = null
try {
// Simulate API call - replace with actual newsletter service integration
await new Promise((resolve) => setTimeout(resolve, 1500))
// TODO: Integrate with external newsletter service (e.g., Mailchimp, ConvertKit, etc.)
// Example:
// await $fetch('/api/newsletter/subscribe', {
// method: 'POST',
// body: { email }
// })
await $fetch('/api/newsletter/subscribe', {
method: 'POST',
body: { email }
})
isSuccess.value = true
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : t('errors.generic')
const fetchError = e as { statusMessage?: string }
error.value = fetchError.statusMessage || t('errors.generic')
throw e
} finally {
isLoading.value = false

View File

@@ -0,0 +1,329 @@
<template>
<div class="min-h-screen pt-32 pb-16">
<!-- Hero Section -->
<div class="relative overflow-hidden">
<div class="absolute inset-0 bg-linear-to-br from-primary-50/50 via-transparent to-cyan-50/50 dark:from-primary-950/30 dark:to-cyan-950/30" />
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 class="font-heading text-4xl sm:text-5xl font-bold text-gray-900 dark:text-white mb-4">
Datenschutzerklärung
</h1>
<p class="text-lg text-gray-600 dark:text-gray-400">
Datenschutzerklärung (DSGVO) für gremiumhub.de
</p>
<p class="text-sm text-gray-500 dark:text-gray-500 mt-2">
Stand: 10.01.2026
</p>
</div>
</div>
<!-- Content -->
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="prose prose-gray dark:prose-invert max-w-none">
<!-- 1. Verantwortlicher -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
1. Verantwortlicher
</h2>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Verantwortlicher im Sinne der Datenschutz-Grundverordnung (DSGVO) ist:
</p>
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-6 border border-gray-200 dark:border-gray-700">
<p class="text-gray-700 dark:text-gray-300 mb-0">
<strong class="text-gray-900 dark:text-white">Raphael Lugowski und Denis Lugowski GremiumHub GbR</strong><br />
Brooksheide 4a, 22549 Hamburg, Deutschland<br />
E-Mail: <a href="mailto:kontakt@gremiumhub.de" class="text-primary-600 dark:text-primary-400 hover:underline">kontakt@gremiumhub.de</a><br />
Telefon: <a href="tel:+4917647028443" class="text-primary-600 dark:text-primary-400 hover:underline">+49 176 47028443</a>
</p>
</div>
</section>
<!-- 2. Allgemeine Hinweise -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
2. Allgemeine Hinweise zur Datenverarbeitung
</h2>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Wir verarbeiten personenbezogene Daten nur, soweit dies zur Bereitstellung einer funktionsfähigen Website, unserer Inhalte sowie zur Bearbeitung von Anfragen bzw. zur Durchführung des Newsletter-Versands erforderlich ist.
</p>
<p class="text-gray-700 dark:text-gray-300 mb-2">
<strong class="text-gray-900 dark:text-white">Rechtsgrundlagen</strong> (je nach Verarbeitung):
</p>
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-2">
<li>Art. 6 Abs. 1 lit. <strong>b</strong> DSGVO (Vertrag / vorvertragliche Maßnahmen)</li>
<li>Art. 6 Abs. 1 lit. <strong>f</strong> DSGVO (berechtigtes Interesse, z. B. technische Bereitstellung, IT-Sicherheit)</li>
<li>Art. 6 Abs. 1 lit. <strong>a</strong> DSGVO (Einwilligung, z. B. Newsletter)</li>
</ul>
</section>
<!-- 3. Hosting -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
3. Hosting (netcup) &amp; Server-Logfiles
</h2>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Unsere Website wird bei <strong class="text-gray-900 dark:text-white">netcup</strong> gehostet. Der Hosting-Anbieter verarbeitet personenbezogene Daten in unserem Auftrag als <strong class="text-gray-900 dark:text-white">Auftragsverarbeiter</strong>. Wir haben mit netcup einen Vertrag zur Auftragsverarbeitung (Art. 28 DSGVO) abgeschlossen.
</p>
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-6 border border-gray-200 dark:border-gray-700 mb-6">
<p class="text-gray-700 dark:text-gray-300 mb-0">
<strong class="text-gray-900 dark:text-white">Hosting-Anbieter:</strong><br />
netcup GmbH, Emmy-Noether-Straße 10, 76131 Karlsruhe, Deutschland
</p>
</div>
<h3 class="font-heading text-xl font-semibold text-gray-900 dark:text-white mb-3">
3.1 Server-Logfiles
</h3>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Beim Aufruf unserer Website werden durch den Hosting-Anbieter in unserem Auftrag sogenannte <strong class="text-gray-900 dark:text-white">Server-Logfiles</strong> verarbeitet. Diese Daten fallen technisch bedingt an und können z. B. enthalten:
</p>
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1 mb-4">
<li>IP-Adresse</li>
<li>Datum und Uhrzeit des Zugriffs</li>
<li>aufgerufene Seite/Datei</li>
<li>Referrer-URL</li>
<li>User-Agent (Browser/Betriebssystem)</li>
</ul>
<p class="text-gray-700 dark:text-gray-300 mb-2">
<strong class="text-gray-900 dark:text-white">Zweck:</strong> Auslieferung der Website, Stabilität, Sicherheit (z. B. Missbrauchs-/Angriffserkennung).
</p>
<p class="text-gray-700 dark:text-gray-300 mb-2">
<strong class="text-gray-900 dark:text-white">Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an sicherem, stabilem Betrieb).
</p>
<p class="text-gray-700 dark:text-gray-300">
<strong class="text-gray-900 dark:text-white">Speicherdauer:</strong> Server-Logfiles werden in der Regel nur so lange gespeichert, wie dies zur Sicherstellung des Betriebs und der Sicherheit erforderlich ist (bei netcup typischerweise bis maximal 14 Tage, abhängig von Produkt/Setup).
</p>
</section>
<!-- 4. Kontaktaufnahme -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
4. Kontaktaufnahme (E-Mail, Kontaktformular)
</h2>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Wenn Sie uns per E-Mail oder über ein Kontaktformular kontaktieren, verarbeiten wir die von Ihnen übermittelten Daten (z. B. Name, E-Mail-Adresse, Nachrichteninhalt) zur Bearbeitung Ihrer Anfrage.
</p>
<p class="text-gray-700 dark:text-gray-300 mb-2">
<strong class="text-gray-900 dark:text-white">Zweck:</strong> Bearbeitung und Beantwortung Ihrer Anfrage.
</p>
<p class="text-gray-700 dark:text-gray-300 mb-2">
<strong class="text-gray-900 dark:text-white">Rechtsgrundlage:</strong>
</p>
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1 mb-4">
<li>Art. 6 Abs. 1 lit. b DSGVO (wenn Ihre Anfrage auf einen Vertrag / vorvertragliche Maßnahmen abzielt), sonst</li>
<li>Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an Kommunikation).</li>
</ul>
<p class="text-gray-700 dark:text-gray-300">
<strong class="text-gray-900 dark:text-white">Speicherdauer:</strong> so lange erforderlich zur Bearbeitung; anschließend Löschung nach Löschkonzept bzw. gesetzlichen Aufbewahrungspflichten.
</p>
</section>
<!-- 5. Newsletter -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
5. Newsletter (Versand über Brevo) inkl. Statistik
</h2>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Auf unserer Website können Sie sich zu unserem Newsletter anmelden, um Updates zur Entwicklung der Web-App zu erhalten.
</p>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Für die technische Umsetzung des Newsletters (Versand und Verwaltung) greifen wir auf die Lösungen des Anbieters Sendinblue GmbH (<strong class="text-gray-900 dark:text-white">Brevo</strong>) zurück. Brevo stellt die Einhaltung der EU-DSGVO bei der Verarbeitung von personenbezogenen Daten sicher. Näheres, insbesondere zum Verarbeitungszweck, entnehmen Sie bitte den folgenden Ziffern sowie auch der <a href="https://www.brevo.com/de/legal/privacypolicy/" target="_blank" rel="noopener noreferrer" class="text-primary-600 dark:text-primary-400 hover:underline">Datenschutzerklärung der Sendinblue GmbH</a>.
</p>
<h3 class="font-heading text-xl font-semibold text-gray-900 dark:text-white mb-3">
5.1 Dienstleister (Auftragsverarbeiter)
</h3>
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-6 border border-gray-200 dark:border-gray-700 mb-6">
<p class="text-gray-700 dark:text-gray-300 mb-0">
<strong class="text-gray-900 dark:text-white">Brevo / Sendinblue GmbH</strong> (Marke Brevo")<br />
Köpenicker Straße 126, 10179 Berlin, Deutschland
</p>
</div>
<p class="text-gray-700 dark:text-gray-300 mb-6">
Brevo verarbeitet die Daten in unserem Auftrag als Auftragsverarbeiter. Wir haben mit Brevo eine Vereinbarung zur Auftragsverarbeitung nach Art. 28 DSGVO abgeschlossen (Data Processing Agreement, DPA).
</p>
<h3 class="font-heading text-xl font-semibold text-gray-900 dark:text-white mb-3">
5.2 Welche Daten werden verarbeitet?
</h3>
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-3 mb-6">
<li>
<strong class="text-gray-900 dark:text-white">Pflichtangabe:</strong> E-Mail-Adresse<br />
<span class="text-gray-600 dark:text-gray-400 text-sm ml-5">Zweck: Die Angabe der E-Mail-Adresse ist erforderlich, damit die Ausgaben des Newsletters den jeweiligen Empfängern auch zugehen.</span>
</li>
<li>
<strong class="text-gray-900 dark:text-white">Freiwillige Angabe:</strong> Name (optional)<br />
<span class="text-gray-600 dark:text-gray-400 text-sm ml-5">Zweck: Die Angabe des Vornamens und Nachnamens ist rein freiwillig, damit wir den Abonnenten im Sinne einer persönlicheren Kommunikation namentlich ansprechen können.</span>
</li>
<li>
<strong class="text-gray-900 dark:text-white">Nachweisdaten</strong> im Rahmen des Double-Opt-In (z. B. Zeitpunkt der Anmeldung und Bestätigung; IP-Adresse), um die Einwilligung zu dokumentieren.
</li>
</ul>
<h3 class="font-heading text-xl font-semibold text-gray-900 dark:text-white mb-3">
5.3 Double-Opt-In
</h3>
<p class="text-gray-700 dark:text-gray-300 mb-6">
Nach der Anmeldung erhalten Sie eine E-Mail, in der Sie die Anmeldung bestätigen. Erst danach werden Sie in den Verteiler aufgenommen.
</p>
<h3 class="font-heading text-xl font-semibold text-gray-900 dark:text-white mb-3">
5.4 Statistik/Tracking im Newsletter (Öffnungen &amp; Klicks)
</h3>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Wir werten Newsletter-Kampagnen statistisch aus (z. B. Öffnungs- und Klickraten), um Inhalte zu optimieren. Diesbezüglich gilt Folgendes:
</p>
<p class="text-gray-700 dark:text-gray-300 mb-6">
Die Zahl der E-Mail-Öffnungen sowie der Klicks wird IP-bezogen durch unseren Anbieter Sendinblue GmbH (Brevo) nachverfolgt, damit wir beurteilen können, ob die versandten Newsletter von den eingetragenen Personen geöffnet bzw. weitergehende Inhalte angeklickt werden. Sollten wir feststellen, dass unsere Newsletter von Personen mehrfach nicht (mehr) geöffnet werden, ist es uns bzw. unserem Anbieter Sendinblue GmbH (Brevo) nur über die IP möglich, dies zu erkennen, sodass wir die entsprechenden Personen gemäß dem Grundsatz der Speicherbegrenzung aus dem Newsletter entfernen und ihre Daten löschen können. Weiterhin ist die Erfassung der IP notwendig, um feststellen zu können, ob die in dem Newsletter veröffentlichten Inhalte für unsere Abonnenten inhaltlich auch wirklich interessant sind und von ihnen eingesehen werden. Anhand der Öffnungsrate oder auch Klickrate, sofern weitere Inhalte in den Newslettern platziert werden, können wir die für unsere Abonnenten interessanten Themen bestimmen und unser Angebot entsprechend fortlaufend anpassen und verbessern.
</p>
<h3 class="font-heading text-xl font-semibold text-gray-900 dark:text-white mb-3">
5.5 Rechtsgrundlage, Widerruf
</h3>
<p class="text-gray-700 dark:text-gray-300 mb-2">
<strong class="text-gray-900 dark:text-white">Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. a, 7 DSGVO (Einwilligung).
</p>
<p class="text-gray-700 dark:text-gray-300 mb-6">
<strong class="text-gray-900 dark:text-white">Widerruf:</strong> Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen, z. B. über den Abmeldelink in jedem Newsletter.
</p>
<h3 class="font-heading text-xl font-semibold text-gray-900 dark:text-white mb-3">
5.6 Speicherort / Datenverarbeitung in der EU
</h3>
<p class="text-gray-700 dark:text-gray-300 mb-6">
Brevo gibt an, dass die Hosting-Server und Datenbanken innerhalb der Europäischen Union betrieben werden.
</p>
<h3 class="font-heading text-xl font-semibold text-gray-900 dark:text-white mb-3">
5.7 Unterauftragsverarbeiter &amp; mögliche Drittlandübermittlungen
</h3>
<p class="text-gray-700 dark:text-gray-300">
Brevo setzt Unterauftragsverarbeiter ein. Je nach eingesetzten Funktionen/Unterauftragsverarbeitern können Datenverarbeitungen auch außerhalb des EWR stattfinden. In solchen Fällen werden geeignete Garantien eingesetzt (z. B. EU-Standardvertragsklauseln und ggf. ergänzende Maßnahmen; für bestimmte US-Anbieter ggf. zusätzlich EU-US Data Privacy Framework, soweit anwendbar).
</p>
</section>
<!-- 6. Cookies -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
6. Cookies
</h2>
<p class="text-gray-700 dark:text-gray-300">
Wir verwenden <strong class="text-gray-900 dark:text-white">nur technisch notwendige Cookies</strong>, die für Betrieb und Sicherheit der Website erforderlich sind. Analyse- oder Marketing-Cookies setzen wir nicht ein.
</p>
</section>
<!-- 7. Download der Web-App -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
7. Download der Web-App
</h2>
<p class="text-gray-700 dark:text-gray-300">
Die Web-App wird nicht direkt über die Website betrieben, sondern gesondert heruntergeladen und installiert. Beim Herunterladen können wie beim normalen Seitenaufruf technische Zugriffsdaten (Server-Logfiles) anfallen (siehe Abschnitt „Hosting &amp; Server-Logfiles").
</p>
</section>
<!-- 8. Empfänger -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
8. Empfänger / Weitergabe von Daten
</h2>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Wir geben personenbezogene Daten nur weiter, wenn dies erforderlich ist, z. B. an:
</p>
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-2">
<li><strong class="text-gray-900 dark:text-white">netcup</strong> (Hosting als Auftragsverarbeiter)</li>
<li><strong class="text-gray-900 dark:text-white">Brevo</strong> (Newsletter-Versand als Auftragsverarbeiter)</li>
</ul>
</section>
<!-- 9. Ihre Rechte -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
9. Ihre Rechte
</h2>
<p class="text-gray-700 dark:text-gray-300 mb-4">
Sie haben das Recht auf:
</p>
<ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-2">
<li>Auskunft (Art. 15 DSGVO)</li>
<li>Berichtigung (Art. 16 DSGVO)</li>
<li>Löschung (Art. 17 DSGVO)</li>
<li>Einschränkung der Verarbeitung (Art. 18 DSGVO)</li>
<li>Datenübertragbarkeit (Art. 20 DSGVO)</li>
<li>Widerspruch gegen bestimmte Verarbeitungen (Art. 21 DSGVO)</li>
<li>Widerruf erteilter Einwilligungen (Art. 7 Abs. 3 DSGVO)</li>
</ul>
</section>
<!-- 10. Beschwerderecht -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
10. Beschwerderecht bei der Aufsichtsbehörde
</h2>
<p class="text-gray-700 dark:text-gray-300">
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (z. B. in dem Bundesland Ihres Aufenthalts oder unseres Unternehmenssitzes).
</p>
</section>
<!-- 11. SSL/TLS -->
<section class="mb-10">
<h2 class="font-heading text-2xl font-bold text-gray-900 dark:text-white mb-4">
11. SSL-/TLS-Verschlüsselung
</h2>
<div class="bg-green-50 dark:bg-green-900/20 rounded-xl p-4 border border-green-200 dark:border-green-800">
<div class="flex items-start gap-3">
<UIcon name="i-lucide-shield-check" class="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 shrink-0" />
<p class="text-green-800 dark:text-green-200 text-sm mb-0">
Diese Website nutzt aus Sicherheitsgründen eine SSL-/TLS-Verschlüsselung.
</p>
</div>
</div>
</section>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// SEO Meta
useSeoMeta({
title: 'Datenschutzerklärung GremiumHub',
description: 'Datenschutzerklärung (DSGVO) für gremiumhub.de Informationen zur Verarbeitung Ihrer personenbezogenen Daten.',
ogTitle: 'Datenschutzerklärung GremiumHub',
ogDescription: 'Datenschutzerklärung (DSGVO) für gremiumhub.de Informationen zur Verarbeitung Ihrer personenbezogenen Daten.',
ogImage: '/og-image.png',
ogType: 'website',
twitterCard: 'summary_large_image',
robots: 'noindex, nofollow'
})
// Structured data for SEO
useHead({
htmlAttrs: {
lang: 'de'
},
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Datenschutzerklärung GremiumHub',
description: 'Datenschutzerklärung (DSGVO) für gremiumhub.de',
publisher: {
'@type': 'Organization',
name: 'GremiumHub GbR',
address: {
'@type': 'PostalAddress',
streetAddress: 'Brooksheide 4a',
addressLocality: 'Hamburg',
postalCode: '22549',
addressCountry: 'DE'
},
email: 'kontakt@gremiumhub.de',
telephone: '+4917647028443'
}
})
}
]
})
</script>

View File

@@ -2,7 +2,7 @@
<div class="min-h-screen pt-32 pb-16">
<!-- Hero Section -->
<div class="relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-primary-50/50 via-transparent to-cyan-50/50 dark:from-primary-950/30 dark:to-cyan-950/30" />
<div class="absolute inset-0 bg-linear-to-br from-primary-50/50 via-transparent to-cyan-50/50 dark:from-primary-950/30 dark:to-cyan-950/30" />
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 class="font-heading text-4xl sm:text-5xl font-bold text-gray-900 dark:text-white mb-4">
Impressum
@@ -144,7 +144,7 @@
</p>
<div class="bg-amber-50 dark:bg-amber-900/20 rounded-xl p-4 border border-amber-200 dark:border-amber-800">
<div class="flex items-start gap-3">
<UIcon name="i-lucide-info" class="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
<UIcon name="i-lucide-info" class="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
<p class="text-amber-800 dark:text-amber-200 text-sm mb-0">
Hinweis EU-OS/ODR-Plattform: Die EU-Online-Streitbeilegungsplattform wurde zum 20.07.2025 eingestellt (Verordnung (EU) 2024/3228). Ein Link darauf ist daher nicht mehr erforderlich.
</p>

View 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>

View File

@@ -329,6 +329,48 @@
"errors": {
"generic": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut."
},
"contact": {
"meta": {
"title": "Kontakt GremiumHub",
"description": "Nehmen Sie Kontakt mit uns auf. Wir freuen uns auf Ihre Nachricht und antworten zeitnah."
},
"badge": "Kontakt",
"title": "Sprechen Sie mit {highlight}",
"titleHighlight": "uns",
"description": "Haben Sie Fragen zu GremiumHub oder möchten Sie mehr über unsere Lösung erfahren? Schreiben Sie uns wir melden uns zeitnah bei Ihnen.",
"form": {
"name": "Ihr Name",
"email": "E-Mail-Adresse",
"message": "Ihre Nachricht",
"submit": "Nachricht senden",
"sending": "Wird gesendet..."
},
"validation": {
"nameRequired": "Bitte geben Sie Ihren Namen ein",
"nameMax": "Der Name darf maximal 100 Zeichen lang sein",
"emailRequired": "Bitte geben Sie eine E-Mail-Adresse ein",
"emailInvalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"messageRequired": "Bitte geben Sie eine Nachricht ein",
"messageMin": "Die Nachricht muss mindestens 10 Zeichen lang sein",
"messageMax": "Die Nachricht darf maximal 5000 Zeichen lang sein"
},
"success": {
"title": "Nachricht gesendet!",
"message": "Vielen Dank für Ihre Nachricht. Wir werden uns schnellstmöglich bei Ihnen melden.",
"backToHome": "Zurück zur Startseite",
"sendAnother": "Weitere Nachricht senden"
},
"trust": {
"gdpr": "DSGVO-konform",
"encrypted": "Verschlüsselt",
"responseTime": "Antwort innerhalb von 24h"
},
"alternativeContact": {
"title": "Oder direkt erreichen",
"email": "kontakt{'@'}gremiumhub.de",
"phone": "+49 176 47028443"
}
},
"team": {
"meta": {
"title": "Unser Team GremiumHub",

View File

@@ -329,6 +329,48 @@
"errors": {
"generic": "An error occurred. Please try again later."
},
"contact": {
"meta": {
"title": "Contact GremiumHub",
"description": "Get in touch with us. We look forward to hearing from you and will respond promptly."
},
"badge": "Contact",
"title": "Get in touch with {highlight}",
"titleHighlight": "us",
"description": "Have questions about GremiumHub or want to learn more about our solution? Write to us we'll get back to you promptly.",
"form": {
"name": "Your name",
"email": "Email address",
"message": "Your message",
"submit": "Send message",
"sending": "Sending..."
},
"validation": {
"nameRequired": "Please enter your name",
"nameMax": "Name must be at most 100 characters",
"emailRequired": "Please enter an email address",
"emailInvalid": "Please enter a valid email address",
"messageRequired": "Please enter a message",
"messageMin": "Message must be at least 10 characters",
"messageMax": "Message must be at most 5000 characters"
},
"success": {
"title": "Message sent!",
"message": "Thank you for your message. We will get back to you as soon as possible.",
"backToHome": "Back to home",
"sendAnother": "Send another message"
},
"trust": {
"gdpr": "GDPR compliant",
"encrypted": "Encrypted",
"responseTime": "Response within 24h"
},
"alternativeContact": {
"title": "Or reach us directly",
"email": "kontakt{'@'}gremiumhub.de",
"phone": "+49 176 47028443"
}
},
"team": {
"meta": {
"title": "Our Team GremiumHub",

View File

@@ -3,10 +3,17 @@ export default defineNuxtConfig({
modules: ['@nuxt/ui', '@nuxt/eslint', '@nuxt/fonts', '@nuxtjs/i18n'],
css: ['~/assets/css/main.css'],
devtools: { enabled: true },
// SSR enabled by default for performance
ssr: true,
// Runtime configuration for server-side API keys
runtimeConfig: {
brevoApiKey: 'NOT_SET',
brevoSenderEmail: 'NOT_SET',
brevoSenderName: 'NOT_SET',
brevoContactEmail: 'NOT_SET',
brevoNewsletterListId: 'NOT_SET'
},
// Font configuration - Bricolage Grotesque for headings, DM Sans for body
fonts: {
families: [

View File

@@ -0,0 +1,15 @@
import type { NuxtError } from 'nuxt/app'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('vue:error', (error: unknown, _instance, info) => {
const statusCode = (error as NuxtError)?.statusCode
if (statusCode && statusCode >= 500) {
console.error(`[${statusCode}] Vue Error:`, error, info)
}
// Print out all errors that are not HTTP errors
if (!statusCode) {
console.error('[unknown] Vue Error:', error, info)
}
})
})

View File

@@ -0,0 +1,135 @@
import { z } from 'zod'
const contactSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
message: z.string().min(10).max(5000)
})
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
// Validate request body
const body = await readBody(event)
const result = contactSchema.safeParse(body)
if (!result.success) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid form data',
data: result.error.flatten()
})
}
const { name, email, message } = result.data
// Check if API key is configured
if (!config.brevoApiKey || config.brevoApiKey === 'NOT_SET') {
console.error('BREVO_API_KEY is not configured')
throw createError({
statusCode: 500,
statusMessage: 'Email service is not configured'
})
}
try {
// Build HTML email content
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #0ea5e9, #06b6d4); color: white; padding: 24px; border-radius: 12px 12px 0 0; }
.content { background: #f8fafc; padding: 24px; border: 1px solid #e2e8f0; border-top: none; border-radius: 0 0 12px 12px; }
.field { margin-bottom: 16px; }
.label { font-weight: 600; color: #64748b; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.value { margin-top: 4px; padding: 12px; background: white; border-radius: 8px; border: 1px solid #e2e8f0; }
.message-value { white-space: pre-wrap; }
.footer { margin-top: 24px; padding-top: 16px; border-top: 1px solid #e2e8f0; font-size: 12px; color: #94a3b8; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 style="margin: 0; font-size: 24px;">Neue Kontaktanfrage</h1>
<p style="margin: 8px 0 0; opacity: 0.9;">über GremiumHub Website</p>
</div>
<div class="content">
<div class="field">
<div class="label">Name</div>
<div class="value">${escapeHtml(name)}</div>
</div>
<div class="field">
<div class="label">E-Mail</div>
<div class="value"><a href="mailto:${escapeHtml(email)}">${escapeHtml(email)}</a></div>
</div>
<div class="field">
<div class="label">Nachricht</div>
<div class="value message-value">${escapeHtml(message)}</div>
</div>
<div class="footer">
Diese Nachricht wurde über das Kontaktformular auf gremiumhub.de gesendet.
</div>
</div>
</div>
</body>
</html>
`.trim()
// Send email via Brevo SMTP API
const response = await $fetch<{ messageId?: string }>('https://api.brevo.com/v3/smtp/email', {
method: 'POST',
headers: {
'api-key': config.brevoApiKey,
'Content-Type': 'application/json'
},
body: {
sender: {
name: config.brevoSenderName,
email: config.brevoSenderEmail
},
to: [
{
email: config.brevoContactEmail,
name: 'GremiumHub Team'
}
],
replyTo: {
email,
name
},
subject: `Kontaktanfrage von ${name}`,
htmlContent
}
})
return {
success: true,
messageId: response.messageId
}
} catch (error: unknown) {
const fetchError = error as { statusCode?: number; data?: { code?: string; message?: string } }
console.error('Brevo SMTP API error:', fetchError.data || error)
throw createError({
statusCode: fetchError.statusCode || 500,
statusMessage: fetchError.data?.message || 'Failed to send message'
})
}
})
// Helper function to escape HTML special characters
function escapeHtml(text: string): string {
const htmlEntities: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}
return text.replace(/[&<>"']/g, (char) => htmlEntities[char] ?? char)
}

View File

@@ -0,0 +1,84 @@
import { z } from 'zod'
const subscribeSchema = z.object({
email: z.string().email()
})
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
// Validate request body
const body = await readBody(event)
const result = subscribeSchema.safeParse(body)
if (!result.success) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid email address'
})
}
const { email } = result.data
// Check if API key is configured
if (!config.brevoApiKey) {
console.error('BREVO_API_KEY is not configured')
throw createError({
statusCode: 500,
statusMessage: 'Newsletter service is not configured'
})
}
try {
// Build request body for Brevo Contacts API
const brevoBody: {
email: string
updateEnabled: boolean
listIds?: number[]
} = {
email,
updateEnabled: true
}
// Add list ID if configured
if (config.brevoNewsletterListId) {
const listId = parseInt(config.brevoNewsletterListId, 10)
if (!isNaN(listId)) {
brevoBody.listIds = [listId]
}
}
const response = await $fetch('https://api.brevo.com/v3/contacts', {
method: 'POST',
headers: {
'accept': 'application/json',
'api-key': config.brevoApiKey,
'content-type': 'application/json'
},
body: brevoBody
})
return {
success: true,
id: (response as { id?: number })?.id
}
} catch (error: unknown) {
// Handle Brevo API errors
const fetchError = error as { statusCode?: number; data?: { code?: string; message?: string } }
// Contact already exists (duplicate_parameter error) - treat as success
if (fetchError.data?.code === 'duplicate_parameter') {
return {
success: true,
message: 'Already subscribed'
}
}
console.error('Brevo API error:', fetchError.data || error)
throw createError({
statusCode: fetchError.statusCode || 500,
statusMessage: fetchError.data?.message || 'Failed to subscribe to newsletter'
})
}
})

View File

@@ -0,0 +1,16 @@
import type { H3Error } from 'h3'
// Due to a bug in nitro, the stack trace is missing on the server side (https://github.com/nuxt/nuxt/issues/30102)
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('error', (error, { event }) => {
const statusCode = (error as H3Error)?.statusCode
if (statusCode && statusCode >= 500) {
console.error(`[${statusCode}] ${event?.path}`, error)
}
// Print out all errors that are not HTTP errors
if (!statusCode) {
console.error(`[Error] ${event?.path}`, error)
}
})
})