feat(landing): Add landing Nuxt page
This commit is contained in:
122
landing/app/components/landing/StatsSection.vue
Normal file
122
landing/app/components/landing/StatsSection.vue
Normal 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>
|
||||
Reference in New Issue
Block a user