189 lines
6.1 KiB
TypeScript
189 lines
6.1 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { useLocale } from '@/lib/i18n/locale-context';
|
|
import { cn } from '@/lib/utils';
|
|
import { ParticleCanvas } from './ParticleCanvas';
|
|
import { Typewriter } from './Typewriter';
|
|
import { Counter } from '@/components/ui/Counter';
|
|
|
|
const fadeUp = (delay = 0) => ({
|
|
initial: { opacity: 0, y: 28 },
|
|
animate: { opacity: 1, y: 0 },
|
|
transition: { duration: 0.7, ease: [0.22, 1, 0.36, 1], delay },
|
|
});
|
|
|
|
export function Hero() {
|
|
const { t, locale } = useLocale();
|
|
|
|
return (
|
|
<section
|
|
id="top"
|
|
className={cn(
|
|
'relative isolate overflow-hidden',
|
|
// Full-screen on desktop, generous on mobile — leaves room for hero
|
|
// metrics without forcing a scroll on first paint at 1080p.
|
|
'min-h-[100svh] pt-28 pb-20 sm:pt-32',
|
|
)}
|
|
>
|
|
{/* Particle network background */}
|
|
<div className="pointer-events-none absolute inset-0 -z-10">
|
|
<ParticleCanvas />
|
|
{/* Edge fade so particles don't fight section seams */}
|
|
<div
|
|
aria-hidden
|
|
className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-b from-transparent to-base"
|
|
/>
|
|
</div>
|
|
|
|
<div className="mx-auto flex max-w-7xl flex-col items-center px-5 text-center sm:px-8">
|
|
{/* Availability chip */}
|
|
<motion.div {...fadeUp(0)} className="mb-7">
|
|
<span className="chip">
|
|
<span className="relative inline-flex h-2 w-2">
|
|
<span className="absolute inset-0 animate-pulse-dot rounded-full bg-emerald" />
|
|
<span className="relative inline-block h-2 w-2 rounded-full bg-emerald" />
|
|
</span>
|
|
{t.hero.availability}
|
|
</span>
|
|
</motion.div>
|
|
|
|
{/* Eyebrow */}
|
|
<motion.p
|
|
{...fadeUp(0.08)}
|
|
className="label-mono mb-6 inline-flex items-center gap-3 text-[clamp(0.65rem,1vw,0.75rem)]"
|
|
>
|
|
<span className="h-px w-10 bg-electric/60" aria-hidden />
|
|
{t.hero.eyebrow}
|
|
<span className="h-px w-10 bg-electric/60" aria-hidden />
|
|
</motion.p>
|
|
|
|
{/* Name */}
|
|
<motion.h1
|
|
{...fadeUp(0.15)}
|
|
className={cn(
|
|
'font-display text-balance text-[clamp(2.4rem,7vw,5.4rem)] font-extrabold leading-[1.02] tracking-tight text-white',
|
|
locale === 'fa' && 'font-fa',
|
|
)}
|
|
>
|
|
{t.hero.name}
|
|
</motion.h1>
|
|
|
|
{/* Headline */}
|
|
<motion.p
|
|
{...fadeUp(0.25)}
|
|
className={cn(
|
|
'mt-5 max-w-4xl text-balance text-[clamp(1.15rem,2.2vw,1.75rem)] font-medium leading-[1.25] text-slate-200',
|
|
)}
|
|
>
|
|
{t.hero.headlineLead}{' '}
|
|
<span className="gradient-text font-semibold">
|
|
{t.hero.headlineAccent}
|
|
</span>{' '}
|
|
{t.hero.headlineTrail}
|
|
</motion.p>
|
|
|
|
{/* Role typewriter */}
|
|
<motion.div
|
|
{...fadeUp(0.35)}
|
|
className="mt-5 flex items-center gap-3 font-mono text-[clamp(0.9rem,1.4vw,1.05rem)] uppercase tracking-[0.15em] text-slate-400"
|
|
>
|
|
<span className="h-px w-6 bg-slate-700" aria-hidden />
|
|
<Typewriter words={t.hero.roles} />
|
|
<span className="h-px w-6 bg-slate-700" aria-hidden />
|
|
</motion.div>
|
|
|
|
{/* Sub */}
|
|
<motion.p
|
|
{...fadeUp(0.42)}
|
|
className="mt-7 max-w-2xl text-balance text-[clamp(0.95rem,1.4vw,1.08rem)] leading-relaxed text-slate-400"
|
|
>
|
|
{t.hero.sub}
|
|
</motion.p>
|
|
|
|
{/* CTAs */}
|
|
<motion.div
|
|
{...fadeUp(0.5)}
|
|
className="mt-9 flex flex-wrap items-center justify-center gap-3"
|
|
>
|
|
<a href="#contact" className="btn-primary">
|
|
{t.hero.ctaPrimary}
|
|
<Arrow locale={locale} />
|
|
</a>
|
|
<a href="#services" className="btn-ghost">
|
|
{t.hero.ctaSecondary}
|
|
</a>
|
|
</motion.div>
|
|
|
|
{/* Metrics */}
|
|
<motion.div
|
|
{...fadeUp(0.6)}
|
|
className="mt-16 grid w-full max-w-4xl grid-cols-2 gap-4 sm:grid-cols-4"
|
|
>
|
|
{t.hero.metrics.map((m, i) => (
|
|
<div
|
|
key={m.label}
|
|
className="glass relative overflow-hidden px-5 py-5 text-start"
|
|
>
|
|
<span
|
|
aria-hidden
|
|
className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-electric/50 to-transparent"
|
|
/>
|
|
<div
|
|
className={cn(
|
|
'font-display text-[clamp(1.6rem,3vw,2.25rem)] font-bold leading-none',
|
|
// Cycle the accent colors across the 4 tiles
|
|
[
|
|
'text-electric',
|
|
'text-violet',
|
|
'text-magenta',
|
|
'text-emerald',
|
|
][i % 4],
|
|
)}
|
|
>
|
|
<Counter value={m.value} locale={locale} />
|
|
</div>
|
|
<div className="mt-2 text-[0.78rem] leading-snug text-slate-400">
|
|
{m.label}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</motion.div>
|
|
|
|
{/* Scroll cue */}
|
|
<motion.a
|
|
href="#services"
|
|
{...fadeUp(0.75)}
|
|
aria-label={t.hero.scroll}
|
|
className="mt-14 inline-flex flex-col items-center gap-2 text-slate-500 transition-colors hover:text-slate-200"
|
|
>
|
|
<span className="label-mono">{t.hero.scroll}</span>
|
|
<span className="relative block h-9 w-5 rounded-full border border-slate-700">
|
|
<span className="absolute left-1/2 top-1.5 inline-block h-1.5 w-0.5 -translate-x-1/2 animate-float-y rounded-full bg-electric" />
|
|
</span>
|
|
</motion.a>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function Arrow({ locale }: { locale: 'fa' | 'en' }) {
|
|
return (
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="16"
|
|
height="16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2.4"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className={locale === 'fa' ? 'rotate-180' : ''}
|
|
aria-hidden
|
|
>
|
|
<path d="M5 12 H19" />
|
|
<path d="M13 6 L19 12 L13 18" />
|
|
</svg>
|
|
);
|
|
}
|