diff --git a/src/app/(frontend)/customgrey.css b/src/app/(frontend)/customgrey.css new file mode 100644 index 0000000..2d70645 --- /dev/null +++ b/src/app/(frontend)/customgrey.css @@ -0,0 +1,127 @@ +:root { + --card: #f5f5f5; + --ring: #606060; + --input: #e0e0e0; + --muted: #d9d9d9; + --accent: #c0c0c0; + --border: #d0d0d0; + --radius: 0.35rem; + --chart-1: #606060; + --chart-2: #476666; + --chart-3: #909090; + --chart-4: #a8a8a8; + --chart-5: #c0c0c0; + --popover: #f5f5f5; + --primary: #606060; + --sidebar: #eaeaea; + --font-mono: Fira Code, monospace; + --font-sans: Montserrat, sans-serif; + --secondary: #e0e0e0; + --background: #f0f0f0; + --font-serif: Georgia, serif; + --foreground: #333333; + --destructive: #cc3333; + --shadow-blur: 0px; + --shadow-color: hsl(0 0% 20% / 0.1); + --sidebar-ring: #606060; + --shadow-spread: 0px; + --shadow-opacity: 0.15; + --sidebar-accent: #c0c0c0; + --sidebar-border: #d0d0d0; + --card-foreground: #333333; + --shadow-offset-x: 0px; + --shadow-offset-y: 2px; + --sidebar-primary: #606060; + --muted-foreground: #666666; + --accent-foreground: #333333; + --popover-foreground: #333333; + --primary-foreground: #ffffff; + --sidebar-foreground: #333333; + --secondary-foreground: #333333; + --destructive-foreground: #ffffff; + --sidebar-accent-foreground: #333333; + --sidebar-primary-foreground: #ffffff; +} + +.dark { + --card: #202020; + --ring: #a0a0a0; + --input: #303030; + --muted: #2a2a2a; + --accent: #404040; + --border: #353535; + --chart-1: #a0a0a0; + --chart-2: #7e9ca0; + --chart-3: #707070; + --chart-4: #585858; + --chart-5: #404040; + --popover: #202020; + --primary: #a0a0a0; + --sidebar: #1f1f1f; + --font-mono: Fira Code, monospace; + --font-sans: Inter, sans-serif; + --secondary: #303030; + --background: #1a1a1a; + --font-serif: Georgia, serif; + --foreground: #d9d9d9; + --destructive: #e06666; + --sidebar-ring: #a0a0a0; + --sidebar-accent: #404040; + --sidebar-border: #353535; + --card-foreground: #d9d9d9; + --sidebar-primary: #a0a0a0; + --muted-foreground: #808080; + --accent-foreground: #d9d9d9; + --popover-foreground: #d9d9d9; + --primary-foreground: #1a1a1a; + --sidebar-foreground: #d9d9d9; + --secondary-foreground: #d9d9d9; + --destructive-foreground: #ffffff; + --sidebar-accent-foreground: #d9d9d9; + --sidebar-primary-foreground: #1a1a1a; +} + +@theme inline { + --color-card: var(--card); + --color-ring: var(--ring); + --color-input: var(--input); + --color-muted: var(--muted); + --color-accent: var(--accent); + --color-border: var(--border); + --color-radius: var(--radius); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-popover: var(--popover); + --color-primary: var(--primary); + --color-sidebar: var(--sidebar); + --color-font-mono: var(--font-mono); + --color-font-sans: var(--font-sans); + --color-secondary: var(--secondary); + --color-background: var(--background); + --color-font-serif: var(--font-serif); + --color-foreground: var(--foreground); + --color-destructive: var(--destructive); + --color-shadow-blur: var(--shadow-blur); + --color-shadow-color: var(--shadow-color); + --color-sidebar-ring: var(--sidebar-ring); + --color-shadow-spread: var(--shadow-spread); + --color-shadow-opacity: var(--shadow-opacity); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-border: var(--sidebar-border); + --color-card-foreground: var(--card-foreground); + --color-shadow-offset-x: var(--shadow-offset-x); + --color-shadow-offset-y: var(--shadow-offset-y); + --color-sidebar-primary: var(--sidebar-primary); + --color-muted-foreground: var(--muted-foreground); + --color-accent-foreground: var(--accent-foreground); + --color-popover-foreground: var(--popover-foreground); + --color-primary-foreground: var(--primary-foreground); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-secondary-foreground: var(--secondary-foreground); + --color-destructive-foreground: var(--destructive-foreground); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); +} \ No newline at end of file diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/page.tsx index 6aba7d6..b6bddfb 100644 --- a/src/app/(frontend)/page.tsx +++ b/src/app/(frontend)/page.tsx @@ -1,5 +1,16 @@ import PageTemplate, { generateMetadata } from './[slug]/page' +import HeroBackground from "@/components/HeroBackground"; + +export default function Page() { + return ( +
+ +
+ ); +} + + export default PageTemplate export { generateMetadata } diff --git a/src/components/HeroBackground.tsx b/src/components/HeroBackground.tsx new file mode 100644 index 0000000..cbad082 --- /dev/null +++ b/src/components/HeroBackground.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +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 }, +]; + +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; +} + +function initStars(W: number, H: number): Star[] { + return Array.from({ length: 180 }, () => { + const toneIdx = 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[toneIdx], + baseAlpha: 0.5 + toneIdx * 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 initWaves(): 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 twinkle = 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 * twinkle * ga})`; + ctx.fill(); + } +} + +function buildWavePts(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 drawWavePath(ctx: CanvasRenderingContext2D, pts: { x: number; y: number }[], W: number, H: 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 = buildWavePts(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)`); + + drawWavePath(ctx, pts, W, H); + ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath(); + ctx.fillStyle = grad; + ctx.fill(); + + drawWavePath(ctx, pts, W, H); + ctx.strokeStyle = `rgba(${v},${v},${v},${0.85 * ga})`; + ctx.lineWidth = w.width; + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + ctx.stroke(); + } +} + +// ── Component ──────────────────────────────────────────────────────────────── + +interface HeroBackgroundProps { + /** Controlled from outside if you have a global theme toggle. */ + isDark?: boolean; + /** Fires when the internal toggle is clicked (omit to hide the button). */ + onToggle?: (isDark: boolean) => void; + /** Show the built-in toggle button. Defaults to true. */ + showToggle?: boolean; +} + +export default function HeroBackground({ + isDark: externalIsDark, + onToggle, + showToggle = true, +}: HeroBackgroundProps) { + const canvasRef = useRef(null); + const [internalDark, setInternalDark] = useState(true); + + // Use external control if provided, otherwise internal state + const isDark = externalIsDark !== undefined ? externalIsDark : internalDark; + + function handleToggle() { + const next = !isDark; + setInternalDark(next); + onToggle?.(next); + } + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let W = 0, H = 0; + let stars: Star[] = []; + let waves: Wave[] = []; + let rafId: number; + + // Animated bg colour + let bgR = 0, bgG = 0, bgB = 0; + let crossfade = 0; + + function resize() { + W = canvas!.width = canvas!.offsetWidth; + H = canvas!.height = canvas!.offsetHeight; + stars = initStars(W, H); + waves = initWaves(); + } + + function loop() { + // Read current target from closure — updated via ref below + const dark = darkRef.current; + const targetBg = dark ? 0 : 255; + const targetCross = dark ? 0 : 1; + + bgR += (targetBg - bgR) * 0.04; + bgG += (targetBg - bgG) * 0.04; + bgB += (targetBg - bgB) * 0.04; + crossfade += (targetCross - 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; + const 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(); + }; + }, []); + + // Keep a ref so the loop closure always reads the latest isDark + const darkRef = useRef(isDark); + useEffect(() => { darkRef.current = isDark; }, [isDark]); + + return ( +
+ + + {showToggle && ( + + )} +
+ ); +}