103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { motion, useInView } from 'framer-motion';
|
|||
|
|
import { useRef } from 'react';
|
|||
|
|
import { useLocale } from '@/lib/i18n/locale-context';
|
|||
|
|
import { SectionHeader } from '@/components/ui/SectionHeader';
|
|||
|
|
import { Counter } from '@/components/ui/Counter';
|
|||
|
|
import { cn } from '@/lib/utils';
|
|||
|
|
|
|||
|
|
const FA_DIGITS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'] as const;
|
|||
|
|
const toFa = (s: string) =>
|
|||
|
|
s.replace(/\d/g, (d) => FA_DIGITS[Number(d)]);
|
|||
|
|
|
|||
|
|
export function Expertise() {
|
|||
|
|
const { t, locale } = useLocale();
|
|||
|
|
const barsRef = useRef<HTMLDivElement>(null);
|
|||
|
|
const inView = useInView(barsRef, { once: true, margin: '-80px' });
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<section id="expertise" className="relative px-5 py-28 sm:px-8">
|
|||
|
|
<div className="mx-auto max-w-7xl">
|
|||
|
|
<SectionHeader
|
|||
|
|
eyebrow={t.expertise.eyebrow}
|
|||
|
|
title={t.expertise.title}
|
|||
|
|
sub={t.expertise.sub}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<div className="mt-14 grid grid-cols-1 gap-10 lg:grid-cols-2">
|
|||
|
|
{/* Metric tiles */}
|
|||
|
|
<div className="grid grid-cols-2 gap-4 self-start">
|
|||
|
|
{t.hero.metrics.map((m, i) => (
|
|||
|
|
<motion.div
|
|||
|
|
key={m.label}
|
|||
|
|
initial={{ opacity: 0, y: 20 }}
|
|||
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|||
|
|
viewport={{ once: true, margin: '-60px' }}
|
|||
|
|
transition={{
|
|||
|
|
duration: 0.5,
|
|||
|
|
ease: [0.22, 1, 0.36, 1],
|
|||
|
|
delay: 0.05 * i,
|
|||
|
|
}}
|
|||
|
|
className="glass relative overflow-hidden p-6"
|
|||
|
|
>
|
|||
|
|
<span
|
|||
|
|
aria-hidden
|
|||
|
|
className={cn(
|
|||
|
|
'absolute inset-x-0 top-0 h-px',
|
|||
|
|
'bg-gradient-to-r from-transparent via-electric/60 to-transparent',
|
|||
|
|
)}
|
|||
|
|
/>
|
|||
|
|
<div
|
|||
|
|
className={cn(
|
|||
|
|
'font-display text-[clamp(1.8rem,3.5vw,2.6rem)] font-bold leading-none',
|
|||
|
|
['text-electric', 'text-violet', 'text-magenta', 'text-emerald'][i % 4],
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
<Counter value={m.value} locale={locale} />
|
|||
|
|
</div>
|
|||
|
|
<div className="mt-3 text-sm leading-snug text-slate-400">
|
|||
|
|
{m.label}
|
|||
|
|
</div>
|
|||
|
|
</motion.div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Skill bars */}
|
|||
|
|
<div ref={barsRef} className="glass relative p-7 sm:p-8">
|
|||
|
|
<span
|
|||
|
|
aria-hidden
|
|||
|
|
className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-magenta/60 to-transparent"
|
|||
|
|
/>
|
|||
|
|
<ul className="flex flex-col gap-6">
|
|||
|
|
{t.expertise.bars.map((b, i) => (
|
|||
|
|
<li key={b.label}>
|
|||
|
|
<div className="mb-2 flex items-baseline justify-between text-sm">
|
|||
|
|
<span className="text-slate-200">{b.label}</span>
|
|||
|
|
<span className="font-mono text-xs text-slate-400">
|
|||
|
|
{locale === 'fa' ? toFa(b.value.toString()) + '٪' : `${b.value}%`}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="relative h-1.5 overflow-hidden rounded-full bg-white/[0.05]">
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ width: 0 }}
|
|||
|
|
animate={inView ? { width: `${b.value}%` } : { width: 0 }}
|
|||
|
|
transition={{
|
|||
|
|
duration: 1.2,
|
|||
|
|
ease: [0.22, 1, 0.36, 1],
|
|||
|
|
delay: 0.08 * i,
|
|||
|
|
}}
|
|||
|
|
className="absolute inset-y-0 start-0 rounded-full bg-brand-gradient"
|
|||
|
|
style={{ backgroundSize: '200% 200%' }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|