Files
soroushasadi/components/sections/DataFlow.tsx
T
soroush.asadi add78d8460
ci / build (push) Failing after 23s
deploy / deploy (push) Failing after 10m12s
first commit
2026-05-31 12:47:02 +03:30

195 lines
6.9 KiB
TypeScript

'use client';
import { motion } from 'framer-motion';
import { useLocale } from '@/lib/i18n/locale-context';
import { SectionHeader } from '@/components/ui/SectionHeader';
/**
* Animated RAG pipeline: ingest → embed → retrieve → rerank → generate.
*
* The diagram itself is always laid out left-to-right (dir="ltr") regardless of
* page locale — a data pipeline reads forward in both languages — while the
* labels/descriptions come from the localized dictionary. The flowing dashes
* are pure SVG (animated stroke-dashoffset), so there is no per-frame JS.
*/
type Accent = 'electric' | 'violet' | 'cyan' | 'magenta' | 'emerald';
const ACCENT_HEX: Record<Accent, string> = {
electric: '#38bdf8',
violet: '#818cf8',
cyan: '#22d3ee',
magenta: '#e879f9',
emerald: '#34d399',
};
// Literal class maps so Tailwind's JIT scanner can see every variant.
const ACCENT_TEXT: Record<Accent, string> = {
electric: 'text-electric',
violet: 'text-violet',
cyan: 'text-cyan',
magenta: 'text-magenta',
emerald: 'text-emerald',
};
const ACCENT_BORDER: Record<Accent, string> = {
electric: 'border-electric/40',
violet: 'border-violet/40',
cyan: 'border-cyan/40',
magenta: 'border-magenta/40',
emerald: 'border-emerald/40',
};
const ACCENT_HOVER_SHADOW: Record<Accent, string> = {
electric: 'hover:shadow-[0_0_30px_-12px_#38bdf8]',
violet: 'hover:shadow-[0_0_30px_-12px_#818cf8]',
cyan: 'hover:shadow-[0_0_30px_-12px_#22d3ee]',
magenta: 'hover:shadow-[0_0_30px_-12px_#e879f9]',
emerald: 'hover:shadow-[0_0_30px_-12px_#34d399]',
};
function asAccent(value: string | undefined): Accent {
return value === 'violet' ||
value === 'cyan' ||
value === 'magenta' ||
value === 'emerald' ||
value === 'electric'
? value
: 'electric';
}
export function DataFlow() {
const { t } = useLocale();
const data = t.dataflow;
const nodes = data.nodes;
return (
<section id="dataflow" className="relative px-5 py-28 sm:px-8">
<div className="mx-auto max-w-7xl">
<SectionHeader eyebrow={data.eyebrow} title={data.title} sub={data.sub} />
{/* Diagram canvas — fixed LTR reading order. */}
<div dir="ltr" className="relative mt-14">
{/* SVG connectors sit behind the cards on md+ (horizontal flow). */}
<svg
aria-hidden
viewBox="0 0 1000 120"
preserveAspectRatio="none"
className="pointer-events-none absolute inset-x-0 top-1/2 hidden h-28 -translate-y-1/2 md:block"
>
<defs>
<linearGradient id="flow-line" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#38bdf8" />
<stop offset="25%" stopColor="#818cf8" />
<stop offset="50%" stopColor="#22d3ee" />
<stop offset="75%" stopColor="#e879f9" />
<stop offset="100%" stopColor="#34d399" />
</linearGradient>
</defs>
{/* Static base rail */}
<line
x1="40"
y1="60"
x2="960"
y2="60"
stroke="url(#flow-line)"
strokeWidth="1.5"
strokeOpacity="0.28"
/>
{/* Animated travelling packets */}
<line
x1="40"
y1="60"
x2="960"
y2="60"
stroke="url(#flow-line)"
strokeWidth="2.5"
strokeLinecap="round"
strokeDasharray="6 60"
className="animate-flow-dash"
/>
</svg>
<ol className="relative grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-5 md:gap-3">
{nodes.map((node, i) => {
const accent = asAccent(node.accent);
return (
<motion.li
key={node.id}
initial={{ opacity: 0, y: 22 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{
duration: 0.5,
ease: [0.22, 1, 0.36, 1],
delay: 0.08 * i,
}}
className="relative"
>
<div
className={`glass group relative flex h-full flex-col gap-3 rounded-2xl border ${ACCENT_BORDER[accent]} bg-white/[0.02] p-5 transition-shadow duration-500 ${ACCENT_HOVER_SHADOW[accent]}`}
>
{/* Step index + pulsing node dot */}
<div className="flex items-center justify-between">
<span className="font-mono text-[0.7rem] text-slate-500">
{String(i + 1).padStart(2, '0')}
</span>
<span className="relative flex h-2.5 w-2.5">
<span
className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-60"
style={{ backgroundColor: ACCENT_HEX[accent] }}
/>
<span
className="relative inline-flex h-2.5 w-2.5 rounded-full"
style={{ backgroundColor: ACCENT_HEX[accent] }}
/>
</span>
</div>
<h3
className={`font-display text-lg font-semibold ${ACCENT_TEXT[accent]}`}
>
{node.label}
</h3>
<p className="text-sm leading-relaxed text-slate-400">
{node.desc}
</p>
</div>
{/* Arrow connector for stacked (mobile / sm) layouts */}
{i < nodes.length - 1 && (
<span
aria-hidden
className="absolute left-1/2 top-full z-10 -translate-x-1/2 text-slate-600 sm:hidden"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path
d="M12 4v16m0 0l6-6m-6 6l-6-6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
)}
</motion.li>
);
})}
</ol>
{data.caption && (
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}
className="mt-10 text-center font-mono text-[0.72rem] uppercase tracking-[0.18em] text-slate-500"
>
{data.caption}
</motion.p>
)}
</div>
</div>
</section>
);
}