diff --git a/src/components/HeroPage.tsx b/src/components/HeroPage.tsx new file mode 100644 index 0000000..ab97b52 --- /dev/null +++ b/src/components/HeroPage.tsx @@ -0,0 +1,420 @@ +'use client' + +import React, { useEffect, useRef } from 'react' +import { cn } from '@/utilities/ui' +import { Button, buttonVariants } from '@/components/ui/button' +import { ArrowRightIcon, PhoneCallIcon } from 'lucide-react' +import { createPortal } from 'react-dom' +import { MenuToggleIcon } from '@/components/ui/menu-toggle-icon' +import { useScroll } from '@/components/ui/use-scroll' +// ADD this line after the other imports: +import { useThemeMode } from '@/hooks/useThemeMode' + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface Star { + x: number; y: number; r: number + v: number; baseAlpha: number + phase: number; twinkleSpeed: number + vx: number; vy: number +} + +interface Wave { + amp: number; freq: number; speed: number + phase: number; yBase: number; width: number; v: number +} + +// ─── Constants ─────────────────────────────────────────────────────────────── + +const STAR_TONES = [255, 210, 160, 110, 65] +const WAVE_TONES = [55, 90, 125, 165, 205] +const WAVE_CONFIGS = [ + { amp: 26, freq: 0.011, speed: 0.35, phase: 0.0, yBase: 0.38, width: 1.6, toneIdx: 0 }, + { amp: 20, freq: 0.015, speed: 0.50, phase: 1.3, yBase: 0.52, width: 1.8, toneIdx: 1 }, + { amp: 30, freq: 0.008, speed: 0.28, phase: 2.6, yBase: 0.63, width: 2.2, toneIdx: 2 }, + { amp: 16, freq: 0.019, speed: 0.65, phase: 0.8, yBase: 0.74, width: 2.0, toneIdx: 3 }, + { amp: 22, freq: 0.013, speed: 0.42, phase: 3.2, yBase: 0.85, width: 2.6, toneIdx: 4 }, +] + +// ─── Canvas helpers ─────────────────────────────────────────────────────────── + +function makeStars(W: number, H: number): Star[] { + return Array.from({ length: 180 }, () => { + const i = Math.floor(Math.random() * STAR_TONES.length) + return { + x: Math.random() * W, y: Math.random() * H, + r: 0.4 + Math.random() * 1.6, + v: STAR_TONES[i], baseAlpha: 0.5 + i * 0.1, + phase: Math.random() * Math.PI * 2, + twinkleSpeed: 0.8 + Math.random() * 2.0, + vx: (Math.random() - 0.5) * 0.3, + vy: (Math.random() - 0.5) * 0.15, + } + }) +} + +function makeWaves(): Wave[] { + return WAVE_CONFIGS.map(c => ({ ...c, v: WAVE_TONES[c.toneIdx] })) +} + +function drawStars(ctx: CanvasRenderingContext2D, stars: Star[], W: number, H: number, ga: number) { + const now = Date.now() / 1000 + for (const s of stars) { + s.x += s.vx; s.y += s.vy + if (s.x < 0) s.x = W; if (s.x > W) s.x = 0 + if (s.y < 0) s.y = H; if (s.y > H) s.y = 0 + const tw = 0.35 + 0.65 * ((Math.sin(now * s.twinkleSpeed + s.phase) + 1) / 2) + ctx.beginPath() + ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2) + ctx.fillStyle = `rgba(${s.v},${s.v},${s.v},${s.baseAlpha * tw * ga})` + ctx.fill() + } +} + +function wavePts(w: Wave, W: number, H: number, now: number) { + const pts: { x: number; y: number }[] = [] + for (let x = 0; x <= W; x += 8) + pts.push({ x, y: H * w.yBase + Math.sin(x * w.freq + w.phase + now * w.speed) * w.amp }) + return pts +} + +function wavePath(ctx: CanvasRenderingContext2D, pts: { x: number; y: number }[]) { + ctx.beginPath() + ctx.moveTo(pts[0].x, pts[0].y) + for (let i = 0; i < pts.length - 1; i++) { + const mx = (pts[i].x + pts[i + 1].x) / 2 + const my = (pts[i].y + pts[i + 1].y) / 2 + ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my) + } + ctx.lineTo(pts[pts.length - 1].x, pts[pts.length - 1].y) +} + +function drawWaves(ctx: CanvasRenderingContext2D, waves: Wave[], W: number, H: number, ga: number, bgR: number) { + const now = Date.now() / 1000 + const bg = Math.round(bgR) + for (const w of waves) { + const v = w.v + const yBase = H * w.yBase + const pts = wavePts(w, W, H, now) + const grad = ctx.createLinearGradient(0, yBase - w.amp, 0, yBase + w.amp * 3) + grad.addColorStop(0, `rgba(${v},${v},${v},${0.030 * ga})`) + grad.addColorStop(0.4, `rgba(${v},${v},${v},${0.012 * ga})`) + grad.addColorStop(1, `rgba(${bg},${bg},${bg},0)`) + wavePath(ctx, pts) + ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath() + ctx.fillStyle = grad + ctx.fill() + wavePath(ctx, pts) + ctx.strokeStyle = `rgba(${v},${v},${v},${0.85 * ga})` + ctx.lineWidth = w.width + ctx.lineJoin = 'round' + ctx.lineCap = 'round' + ctx.stroke() + } +} + +// ─── Background canvas ──────────────────────────────────────────────────────── + +function Background({ isDark }: { isDark: boolean }) { + const canvasRef = useRef(null) + const darkRef = useRef(isDark) + useEffect(() => { darkRef.current = isDark }, [isDark]) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + let W = 0, H = 0 + let stars: Star[] = [], waves: Wave[] = [] + let bgR = 0, bgG = 0, bgB = 0, crossfade = 0 + let rafId: number + + function resize() { + W = canvas!.width = canvas!.offsetWidth + H = canvas!.height = canvas!.offsetHeight + stars = makeStars(W, H) + waves = makeWaves() + } + + function loop() { + const dark = darkRef.current + const tBg = dark ? 0 : 255 + const tCross = dark ? 0 : 1 + bgR += (tBg - bgR) * 0.04 + bgG += (tBg - bgG) * 0.04 + bgB += (tBg - bgB) * 0.04 + crossfade += (tCross - crossfade) * 0.03 + ctx!.fillStyle = `rgb(${Math.round(bgR)},${Math.round(bgG)},${Math.round(bgB)})` + ctx!.fillRect(0, 0, W, H) + const sa = 1 - crossfade, wa = crossfade + if (sa > 0.01) drawStars(ctx!, stars, W, H, sa) + if (wa > 0.01) drawWaves(ctx!, waves, W, H, wa, bgR) + rafId = requestAnimationFrame(loop) + } + + const ro = new ResizeObserver(resize) + ro.observe(canvas) + resize() + rafId = requestAnimationFrame(loop) + return () => { cancelAnimationFrame(rafId); ro.disconnect() } + }, []) + + return +} + +// ─── Header ─────────────────────────────────────────────────────────────────── + +function Header({ isDark, onToggle }: { isDark: boolean; onToggle: () => void }) { + const [open, setOpen] = useState(false) + const scrolled = useScroll(10) + + const links = [ + { label: 'Work', href: '#work' }, + { label: 'About', href: '#about' }, + { label: 'Contact', href: '#contact' }, + ] + + useEffect(() => { + document.body.style.overflow = open ? 'hidden' : '' + return () => { document.body.style.overflow = '' } + }, [open]) + + const textColor = isDark ? 'text-white/80 hover:text-white' : 'text-black/70 hover:text-black' + const borderColor = isDark ? 'border-white/10' : 'border-black/10' + + return ( +
+ + + {/* Mobile menu */} + {open && typeof window !== 'undefined' && createPortal( +
+
+ {links.map(l => ( + setOpen(false)} + className={cn('px-3 py-2 rounded-md text-sm font-medium', textColor)} + >{l.label} + ))} +
+
+ + Hire me +
+
, + document.body + )} +
+ ) +} + +// ─── Hero section ───────────────────────────────────────────────────────────── + +function HeroSection({ isDark }: { isDark: boolean }) { + const text = isDark ? 'text-white' : 'text-black' + const muted = isDark ? 'text-white/55' : 'text-black/55' + const badge = isDark ? 'bg-white/8 border-white/15 text-white/70' : 'bg-black/5 border-black/12 text-black/60' + const borderLine = isDark ? 'bg-white/10' : 'bg-black/10' + const borderLineFaint = isDark ? 'bg-white/5' : 'bg-black/5' + + return ( +
+ {/* Vertical border lines */} +
+ ) +} + +// ─── Logo cloud (trusted by section) ───────────────────────────────────────── + +const LOGOS = [ + { src: 'https://storage.efferd.com/logo/nvidia-wordmark.svg', alt: 'Nvidia' }, + { src: 'https://storage.efferd.com/logo/supabase-wordmark.svg', alt: 'Supabase' }, + { src: 'https://storage.efferd.com/logo/openai-wordmark.svg', alt: 'OpenAI' }, + { src: 'https://storage.efferd.com/logo/vercel-wordmark.svg', alt: 'Vercel' }, + { src: 'https://storage.efferd.com/logo/github-wordmark.svg', alt: 'GitHub' }, + { src: 'https://storage.efferd.com/logo/claude-wordmark.svg', alt: 'Claude' }, +] + +function LogosSection({ isDark }: { isDark: boolean }) { + const text = isDark ? 'text-white/40' : 'text-black/40' + const bold = isDark ? 'text-white/70' : 'text-black/70' + const border = isDark ? 'border-white/10' : 'border-black/10' + const logoFilter = isDark ? 'brightness-0 invert opacity-30' : 'brightness-0 opacity-25' + + return ( +
+

+ built with tools trusted by world-class teams +

+
+ {LOGOS.map(l => ( + {l.alt} + ))} +
+
+ ) +} + +// ─── Main export ────────────────────────────────────────────────────────────── + +// REPLACE WITH: +export default function HeroPage() { + const { isDark, toggle } = useThemeMode() + + return ( +
+ + {/* Canvas background — fixed behind everything */} +
+ +
+ + {/* Page content */} +
+
+
+ + +
+
+ +
+ ) +}