first commit
ci / build (push) Failing after 23s
deploy / deploy (push) Failing after 10m12s

This commit is contained in:
soroush.asadi
2026-05-31 12:47:02 +03:30
commit add78d8460
100 changed files with 15221 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
'use client';
import { useRef } from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import { useLocale } from '@/lib/i18n/locale-context';
import { SectionHeader } from '@/components/ui/SectionHeader';
import { cn } from '@/lib/utils';
import { ServiceIcon, type ServiceIconKind } from './ServiceIcon';
const FA_DIGITS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'] as const;
function num(n: number, locale: 'fa' | 'en') {
const str = n.toString().padStart(2, '0');
return locale === 'fa'
? str.replace(/\d/g, (d) => FA_DIGITS[Number(d)])
: str;
}
const COLOR_MAP: Record<
string,
{ text: string; ring: string; glow: string; chip: string }
> = {
electric: {
text: 'text-electric',
ring: 'group-hover:border-electric/50',
glow: 'group-hover:shadow-glow-electric',
chip: 'border-electric/30 bg-electric/5 text-electric/90',
},
violet: {
text: 'text-violet',
ring: 'group-hover:border-violet/50',
glow: 'group-hover:shadow-glow-violet',
chip: 'border-violet/30 bg-violet/5 text-violet/90',
},
magenta: {
text: 'text-magenta',
ring: 'group-hover:border-magenta/50',
glow: 'group-hover:shadow-glow-magenta',
chip: 'border-magenta/30 bg-magenta/5 text-magenta/90',
},
emerald: {
text: 'text-emerald',
ring: 'group-hover:border-emerald/50',
glow: 'group-hover:shadow-glow-emerald',
chip: 'border-emerald/30 bg-emerald/5 text-emerald/90',
},
cyan: {
text: 'text-cyan',
ring: 'group-hover:border-cyan/50',
glow: 'group-hover:shadow-glow-electric',
chip: 'border-cyan/30 bg-cyan/5 text-cyan/90',
},
};
export function Services() {
const { t, locale } = useLocale();
return (
<section id="services" className="relative px-5 py-28 sm:px-8">
<div className="mx-auto max-w-7xl">
<SectionHeader
eyebrow={t.services.eyebrow}
title={t.services.title}
sub={t.services.sub}
/>
<div className="mt-14 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{t.services.items.map((item, i) => (
<ServiceCard
key={item.id}
index={i}
numLabel={num(i + 1, locale)}
title={item.title}
description={item.description}
tags={item.tags}
color={item.color}
iconKind={item.id as ServiceIconKind}
href={`/services/${item.id}`}
locale={locale}
/>
))}
</div>
</div>
</section>
);
}
function ServiceCard({
index,
numLabel,
title,
description,
tags,
color,
iconKind,
href,
locale,
}: {
index: number;
numLabel: string;
title: string;
description: string;
tags: readonly string[];
color: string;
iconKind: ServiceIconKind;
href: string;
locale: 'fa' | 'en';
}) {
const ref = useRef<HTMLDivElement>(null);
const mx = useMotionValue(50);
const my = useMotionValue(50);
const rotateX = useMotionValue(0);
const rotateY = useMotionValue(0);
// Subtle 3D tilt on pointer move — keeps the card "alive" without
// forcing GPU work when the cursor isn't over it.
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
const el = ref.current;
if (!el) return;
const r = el.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width;
const y = (e.clientY - r.top) / r.height;
mx.set(x * 100);
my.set(y * 100);
rotateY.set((x - 0.5) * 8);
rotateX.set((0.5 - y) * 8);
};
const onPointerLeave = () => {
rotateX.set(0);
rotateY.set(0);
};
const spotlight = useMotionTemplate`radial-gradient(220px circle at ${mx}% ${my}%, rgba(255,255,255,0.08), transparent 60%)`;
const c = COLOR_MAP[color] ?? COLOR_MAP.electric;
return (
<motion.article
ref={ref}
onPointerMove={onPointerMove}
onPointerLeave={onPointerLeave}
style={{
rotateX,
rotateY,
transformStyle: 'preserve-3d',
transformPerspective: 1000,
}}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{
duration: 0.6,
ease: [0.22, 1, 0.36, 1],
delay: 0.05 * index,
}}
className={cn(
'group relative isolate overflow-hidden p-6 sm:p-7',
'glass transition-all duration-300',
c.ring,
c.glow,
)}
>
{/* Spotlight */}
<motion.div
aria-hidden
style={{ background: spotlight }}
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
/>
{/* Number + icon row */}
<div className="relative flex items-start justify-between">
<span
className={cn(
'font-mono text-[0.78rem] tracking-[0.18em] text-slate-500',
locale === 'fa' && 'fa-nums',
)}
>
{numLabel}
</span>
<span className={cn('transition-colors duration-300', c.text)}>
<ServiceIcon kind={iconKind} className="h-7 w-7" />
</span>
</div>
{/* Title */}
<h3
className={cn(
'relative mt-6 font-display text-[clamp(1.15rem,1.8vw,1.4rem)] font-semibold leading-snug text-white',
locale === 'fa' && 'font-fa',
)}
>
{title}
</h3>
{/* Description */}
<p className="relative mt-3 text-[0.94rem] leading-relaxed text-slate-400">
{description}
</p>
{/* Tags */}
<div className="relative mt-5 flex flex-wrap gap-1.5">
{tags.map((tag) => (
<span
key={tag}
className={cn(
'rounded-full border px-2.5 py-0.5 font-mono text-[0.65rem] uppercase tracking-wider',
c.chip,
)}
>
{tag}
</span>
))}
</div>
{/* Hairline */}
<span
aria-hidden
className="absolute inset-x-6 bottom-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"
/>
</motion.article>
);
}