Import source: src/components/Hero
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,186 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
import { ArrowDown } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function Hero() {
|
||||||
|
const t = useTranslations('Hero');
|
||||||
|
const locale = useLocale();
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
let animationFrameId: number;
|
||||||
|
let particles: Array<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
radius: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
opacity: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
canvas.width = canvas.offsetWidth;
|
||||||
|
canvas.height = canvas.offsetHeight;
|
||||||
|
initParticles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const initParticles = () => {
|
||||||
|
particles = [];
|
||||||
|
const density = Math.floor((canvas.width * canvas.height) / 18000);
|
||||||
|
const count = Math.min(Math.max(density, 15), 50); // Keep it light and subtle
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
particles.push({
|
||||||
|
x: Math.random() * canvas.width,
|
||||||
|
y: Math.random() * canvas.height,
|
||||||
|
radius: Math.random() * 1.5 + 0.5,
|
||||||
|
vx: (Math.random() - 0.5) * 0.15,
|
||||||
|
vy: (Math.random() - 0.5) * 0.15,
|
||||||
|
opacity: Math.random() * 0.35 + 0.1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
|
// Select subtle colors based on theme: sand or teal
|
||||||
|
const particleColor = isDark ? '15, 107, 115' : '217, 93, 57'; // Teal for dark theme, terracotta for light theme
|
||||||
|
|
||||||
|
particles.forEach((p) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `rgba(${particleColor}, ${p.opacity})`;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Update positions
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
|
||||||
|
// Bounce off bounds
|
||||||
|
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
|
||||||
|
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
|
};
|
||||||
|
|
||||||
|
resizeCanvas();
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
window.removeEventListener('resize', resizeCanvas);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.15,
|
||||||
|
delayChildren: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { y: 20, opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
y: 0,
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative w-full h-[calc(100vh-4rem)] min-h-[500px] flex items-center justify-center overflow-hidden bg-warm-white dark:bg-near-black transition-colors duration-300">
|
||||||
|
{/* Subtle particle canvas */}
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="absolute inset-0 w-full h-full pointer-events-none opacity-60"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Decorative gradient overlay */}
|
||||||
|
<div className="absolute inset-0 bg-radial-gradient from-transparent via-warm-white/10 to-warm-white dark:to-near-black pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="max-w-[1200px] mx-auto px-6 w-full relative z-10 flex flex-col items-center text-center">
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
{/* Main Typographic Treatment */}
|
||||||
|
<motion.h1
|
||||||
|
variants={itemVariants}
|
||||||
|
className={`text-5xl md:text-8xl font-black tracking-tight text-charcoal dark:text-warm-white ${locale === 'fa' ? 'font-farsi font-black' : 'font-sans'}`}
|
||||||
|
>
|
||||||
|
{locale === 'fa' ? 'آرسام صباغ' : 'Arsam Sabbagh'}
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
{/* Editorial Tagline */}
|
||||||
|
<motion.p
|
||||||
|
variants={itemVariants}
|
||||||
|
className={`text-lg md:text-2xl text-teal-brand dark:text-sand font-medium uppercase tracking-widest ${locale === 'fa' ? 'font-farsi font-normal' : 'font-sans'}`}
|
||||||
|
>
|
||||||
|
{t('tagline')}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
{/* Action CTAs */}
|
||||||
|
<motion.div
|
||||||
|
variants={itemVariants}
|
||||||
|
className="flex flex-wrap items-center justify-center gap-4 pt-4"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#projects"
|
||||||
|
className="px-6 py-3 rounded-full bg-teal-brand hover:bg-teal-brand/90 text-warm-white text-sm font-semibold transition-all hover:scale-102 hover:shadow-md cursor-pointer"
|
||||||
|
>
|
||||||
|
{t('ctaWork')}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#writing"
|
||||||
|
className="px-6 py-3 rounded-full border border-sand dark:border-charcoal hover:border-teal-brand hover:text-teal-brand text-charcoal dark:text-warm-white text-sm font-semibold transition-all hover:scale-102 cursor-pointer"
|
||||||
|
>
|
||||||
|
{t('ctaWriting')}
|
||||||
|
</a>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Scroll Indicator */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1, duration: 0.8 }}
|
||||||
|
className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 cursor-pointer opacity-70 hover:opacity-100 transition-opacity"
|
||||||
|
onClick={() => {
|
||||||
|
document.getElementById('about')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={`text-xs uppercase tracking-widest ${locale === 'fa' ? 'font-farsi' : 'font-sans'}`}>
|
||||||
|
{t('scroll')}
|
||||||
|
</span>
|
||||||
|
<motion.div
|
||||||
|
animate={{ y: [0, 6, 0] }}
|
||||||
|
transition={{ repeat: Infinity, duration: 1.5 }}
|
||||||
|
>
|
||||||
|
<ArrowDown className="w-4 h-4 text-teal-brand" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user