hero-section, start login

This commit is contained in:
colinislit
2025-11-18 13:55:16 +01:00
parent 00f7452d54
commit d4079b66c5
10 changed files with 872 additions and 59 deletions

View File

@@ -37,13 +37,18 @@ interface HeroSectionProps {
};
slogan?: string;
title: React.ReactNode;
subtitle: string;
subtitle: string | React.ReactNode;
punchline?: string | React.ReactNode;
callToAction: {
text: string;
href: string;
};
secondaryCallToAction?: {
text: string;
href: string;
};
backgroundImage: string;
contactInfo: {
contactInfo?: {
website: string;
phone: string;
address: string;
@@ -52,7 +57,7 @@ interface HeroSectionProps {
}
const HeroSection = React.forwardRef<HTMLDivElement, HeroSectionProps>(
({ className, logo, slogan, title, subtitle, callToAction, backgroundImage, contactInfo, ...props }, ref) => {
({ className, logo, slogan, title, subtitle, punchline, callToAction, secondaryCallToAction, backgroundImage, contactInfo, ...props }, ref) => {
// Animation variants for the container to orchestrate children animations
const containerVariants = {
@@ -108,36 +113,64 @@ const HeroSection = React.forwardRef<HTMLDivElement, HeroSectionProps>(
</motion.header>
<motion.main variants={containerVariants}>
<motion.h1 className="text-4xl font-bold leading-tight text-foreground md:text-5xl" variants={itemVariants}>
<motion.h1 className="text-4xl font-bold leading-tight text-foreground md:text-5xl lg:text-6xl" variants={itemVariants}>
{title}
</motion.h1>
<motion.div className="my-6 h-1 w-20 bg-primary" variants={itemVariants}></motion.div>
<motion.p className="mb-8 max-w-md text-base text-muted-foreground" variants={itemVariants}>
<motion.div className="mb-6 max-w-lg text-base md:text-lg leading-relaxed text-slate-600" variants={itemVariants}>
{subtitle}
</motion.p>
<motion.a href={callToAction.href} className="text-lg font-bold tracking-widest text-primary transition-colors hover:text-primary/80" variants={itemVariants}>
{callToAction.text}
</motion.a>
</motion.div>
{punchline && (
<motion.aside
className="mb-8 max-w-lg md:max-w-2xl border-l-4 border-primary bg-slate-50 p-6 rounded-r-lg"
variants={itemVariants}
role="note"
>
<div className="text-base md:text-lg font-medium text-slate-800 leading-relaxed [&_*:not(br)]:whitespace-normal md:[&_*:not(br)]:whitespace-nowrap">
{punchline}
</div>
</motion.aside>
)}
<motion.div className="flex flex-col sm:flex-row gap-4" variants={itemVariants}>
<a
href={callToAction.href}
className="inline-block px-8 py-4 bg-primary hover:bg-primary/90 text-white font-bold text-base tracking-wide rounded-lg transition-colors text-center"
>
{callToAction.text}
</a>
{secondaryCallToAction && (
<a
href={secondaryCallToAction.href}
className="inline-block px-8 py-4 border-2 border-slate-300 hover:bg-slate-50 text-slate-700 font-medium text-base rounded-lg transition-colors text-center"
>
{secondaryCallToAction.text}
</a>
)}
</motion.div>
</motion.main>
</div>
{/* Bottom Section: Footer Info */}
<motion.footer className="mt-12 w-full" variants={itemVariants}>
<div className="grid grid-cols-1 gap-6 text-xs text-muted-foreground sm:grid-cols-3">
<div className="flex items-center">
<InfoIcon type="website" />
<span>{contactInfo.website}</span>
{contactInfo && (
<motion.footer className="mt-12 w-full" variants={itemVariants}>
<div className="grid grid-cols-1 gap-6 text-xs text-muted-foreground sm:grid-cols-3">
<div className="flex items-center">
<InfoIcon type="website" />
<span>{contactInfo.website}</span>
</div>
<div className="flex items-center">
<InfoIcon type="phone" />
<span>{contactInfo.phone}</span>
</div>
<div className="flex items-center">
<InfoIcon type="address" />
<span>{contactInfo.address}</span>
</div>
</div>
<div className="flex items-center">
<InfoIcon type="phone" />
<span>{contactInfo.phone}</span>
</div>
<div className="flex items-center">
<InfoIcon type="address" />
<span>{contactInfo.address}</span>
</div>
</div>
</motion.footer>
</motion.footer>
)}
</div>
{/* Right Side: Image with Clip Path Animation */}

View File

@@ -0,0 +1,66 @@
'use client'
import { useEffect, useRef, useState, ReactNode } from 'react'
interface ScrollRevealProps {
children: ReactNode
direction?: 'up' | 'down' | 'left' | 'right'
delay?: number
className?: string
}
export function ScrollReveal({
children,
direction = 'up',
delay = 0,
className = '',
}: ScrollRevealProps) {
const [isVisible, setIsVisible] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.disconnect()
}
},
{
threshold: 0.1,
rootMargin: '50px',
}
)
if (ref.current) {
observer.observe(ref.current)
}
return () => {
observer.disconnect()
}
}, [])
const directionClasses = {
up: 'translate-y-8',
down: '-translate-y-8',
left: 'translate-x-8',
right: '-translate-x-8',
}
return (
<div
ref={ref}
className={`transition-all duration-700 ease-out ${
isVisible
? 'opacity-100 translate-x-0 translate-y-0'
: `opacity-0 ${directionClasses[direction]}`
} ${className}`}
style={{
transitionDelay: isVisible ? `${delay}ms` : '0ms',
}}
>
{children}
</div>
)
}

117
components/ui/why-me.tsx Normal file
View File

@@ -0,0 +1,117 @@
'use client'
import { useEffect, useState } from 'react'
import Image from 'next/image'
import { ScrollReveal } from '@/components/ui/scroll-reveal'
import aboutContent from '@/content/nl/about.json'
export function WhyMe() {
const [mounted, setMounted] = useState(false)
const { title, subtitle, image, paragraphs, stats, expertise } = aboutContent
useEffect(() => {
setMounted(true)
}, [])
return (
<section
id="over"
className="section-padding bg-gray-50 relative overflow-hidden"
>
{/* Decorative elements */}
<div className="absolute top-0 right-0 w-64 h-64 bg-teal/5 rounded-full blur-3xl translate-x-1/2 -translate-y-1/2" />
<div className="absolute bottom-0 left-0 w-96 h-96 bg-teal/5 rounded-full blur-3xl -translate-x-1/2 translate-y-1/2" />
<div className="container-custom relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center max-w-6xl mx-auto">
{/* Left: Text Content */}
<ScrollReveal direction="left">
<div className="space-y-8">
<h2 className="text-5xl md:text-6xl lg:text-7xl font-heading font-bold text-text-primary leading-tight">
{title}
</h2>
<p className="text-xl md:text-2xl text-text-secondary font-light leading-relaxed">
{subtitle}
</p>
<div className="space-y-6 text-text-secondary text-lg leading-relaxed">
{paragraphs.map((paragraph, index) => (
<p key={index}>{paragraph}</p>
))}
</div>
<div className="grid grid-cols-2 gap-4 pt-8">
{stats.map((stat, index) => (
<div key={index}>
<div className="text-3xl font-heading font-bold text-text-primary mb-1">{stat.value}</div>
<div className="text-text-secondary text-sm">{stat.label}</div>
</div>
))}
</div>
</div>
</ScrollReveal>
{/* Right: Photo */}
<ScrollReveal direction="right" delay={200}>
<div className="relative aspect-[3/4] lg:aspect-[4/5] max-w-md mx-auto lg:ml-auto">
{/* Optional subtle border/frame effect */}
<div className="absolute inset-0 rounded-2xl bg-teal/10" />
<div className="relative w-full h-full rounded-2xl overflow-hidden">
<Image
src={image.src}
alt={image.alt}
fill
className="object-cover"
priority
/>
</div>
</div>
</ScrollReveal>
</div>
{/* FASE 2: Expertise Section - Full Width with Floating Animation */}
{expertise && (
<div className="mt-16 pt-16 border-t border-gray-200">
<ScrollReveal>
<h3 className="text-2xl md:text-3xl font-heading font-bold text-center mb-3 text-text-primary">
{expertise.label}
</h3>
<p className="text-center text-text-secondary mb-12 max-w-2xl mx-auto">
De tools en methoden waarmee ik jouw AI-uitdaging aanpak
</p>
</ScrollReveal>
<div className="flex flex-wrap justify-center gap-4 max-w-5xl mx-auto">
{expertise.items.map((item, index) => (
<div
key={index}
className={`
group px-5 py-3 rounded-full font-medium text-sm
transition-all duration-300 cursor-default animate-float
border shadow-sm
${
item.highlight
? 'bg-teal-700 text-white border-teal-700 hover:bg-teal-600 hover:-translate-y-2 hover:rotate-2 hover:shadow-md'
: 'bg-white text-gray-800 border-gray-300 hover:bg-teal-50 hover:border-teal-600 hover:text-teal-800 hover:-translate-y-2 hover:rotate-1 hover:shadow-md'
}
`}
style={{
animationDelay: mounted ? `${index * 150}ms` : '0ms',
animationDuration: `${5 + (index % 3) * 0.8}s`,
}}
>
<span className="font-semibold">{item.name}</span>
<span className={`ml-2 text-xs ${item.highlight ? 'opacity-80 group-hover:opacity-100' : 'opacity-70 group-hover:opacity-100'}`}>
{item.category}
</span>
</div>
))}
</div>
</div>
)}
</div>
</section>
)
}