hero-section, start login
This commit is contained in:
@@ -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 */}
|
||||
|
||||
66
components/ui/scroll-reveal.tsx
Normal file
66
components/ui/scroll-reveal.tsx
Normal 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
117
components/ui/why-me.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user