feat(landing): Add landing Nuxt page

This commit is contained in:
2026-01-03 10:19:39 +01:00
parent 0803b59f0f
commit b3311155c7
28 changed files with 13620 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
<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"
>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Stats grid -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
<div
v-for="(stat, index) in stats"
:key="index"
class="text-center animate-fade-in-up"
:style="{ animationDelay: `${index * 100}ms` }"
>
<div class="relative inline-block mb-3">
<!-- Animated number -->
<span ref="counterRefs" class="font-heading text-4xl sm:text-5xl lg:text-6xl font-bold gradient-text">
{{ animatedValues[index] }}{{ stat.suffix }}
</span>
<!-- Decorative glow -->
<div class="absolute -inset-4 bg-primary-400/20 blur-2xl rounded-full -z-10 animate-pulse-glow" />
</div>
<p class="text-sm sm:text-base text-gray-600 dark:text-gray-300 font-medium">
{{ stat.label }}
</p>
</div>
</div>
<!-- Trust badges -->
<div class="mt-10 pt-8 border-t border-gray-200 dark:border-gray-800">
<p class="text-center text-sm text-gray-500 dark:text-gray-400 mb-8">Vertrauen & Sicherheit</p>
<div class="flex flex-wrap justify-center items-center gap-8 lg:gap-12">
<div
v-for="(badge, index) in trustBadges"
:key="index"
class="flex items-center gap-3 px-4 py-2 rounded-full bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 shadow-sm hover:shadow-md transition-shadow animate-fade-in-up"
: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"
>
<UIcon :name="badge.icon" class="w-4 h-4 text-primary-600 dark:text-primary-400" />
</div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">{{ badge.label }}</span>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const stats = [
{ value: 70, suffix: '%', label: 'Zeitersparnis im Verfahren' },
{ value: 100, suffix: '%', label: 'Revisionssicher dokumentiert' },
{ value: 24, suffix: '/7', label: 'Verfügbarkeit' },
{ value: 0, suffix: '', label: 'Medienbrüche' }
]
const trustBadges = [
{ icon: 'i-lucide-shield-check', label: 'DSGVO-konform' },
{ icon: 'i-lucide-server', label: 'Hosting in Deutschland' },
{ icon: 'i-lucide-lock', label: 'Ende-zu-Ende verschlüsselt' },
{ icon: 'i-lucide-key', label: 'SSO-fähig' }
]
// Animated counter values
const animatedValues = ref(stats.map(() => 0))
const hasAnimated = ref(false)
// Counter animation function
function animateCounter(index: number, target: number, duration: number = 2000) {
const start = 0
const startTime = performance.now()
function update(currentTime: number) {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// Easing function (ease-out cubic)
const easeOut = 1 - Math.pow(1 - progress, 3)
animatedValues.value[index] = Math.round(start + (target - start) * easeOut)
if (progress < 1) {
requestAnimationFrame(update)
}
}
requestAnimationFrame(update)
}
// Start animation when section is visible
onMounted(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !hasAnimated.value) {
hasAnimated.value = true
stats.forEach((stat, index) => {
setTimeout(() => {
animateCounter(index, stat.value)
}, index * 200)
})
}
})
},
{ threshold: 0.3 }
)
const section = document.querySelector('section')
if (section) {
observer.observe(section)
}
onUnmounted(() => {
observer.disconnect()
})
})
</script>