This commit is contained in:
Andras Schmelczer 2026-02-02 21:56:35 +00:00
parent 2c613dc0d1
commit a677b9331f
28 changed files with 1647 additions and 1498 deletions

View file

@ -1,164 +1,14 @@
import { useRef, useState, useEffect, useCallback } from 'react';
import { useFadeInRef } from '../hooks/useFadeIn';
import HexCanvas from './HexCanvas';
// --- Floating hex particle canvas that reacts to scroll ---
const HEX_COUNT = 60;
const TAU = Math.PI * 2;
interface Hex {
x: number;
y: number;
baseY: number;
size: number;
opacity: number;
speed: number; // horizontal drift px/s
phase: number; // for gentle bob
}
function initHexes(w: number, h: number): Hex[] {
const hexes: Hex[] = [];
for (let i = 0; i < HEX_COUNT; i++) {
const y = Math.random() * h;
hexes.push({
x: Math.random() * w,
y,
baseY: y,
size: 8 + Math.random() * 20,
opacity: 0.06 + Math.random() * 0.12,
speed: 6 + Math.random() * 14,
phase: Math.random() * TAU,
});
}
return hexes;
}
function drawHex(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number) {
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (TAU / 6) * i - Math.PI / 6;
const px = cx + r * Math.cos(angle);
const py = cy + r * Math.sin(angle);
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
}
function HexCanvas({ scrollProgress, isDark = false }: { scrollProgress: number; isDark?: boolean }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const hexesRef = useRef<Hex[]>([]);
const animRef = useRef(0);
const scrollRef = useRef(scrollProgress);
scrollRef.current = scrollProgress;
const isDarkRef = useRef(isDark);
isDarkRef.current = isDark;
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let w = 0;
let h = 0;
function resize() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas!.parentElement!.getBoundingClientRect();
w = rect.width;
h = rect.height;
canvas!.width = w * dpr;
canvas!.height = h * dpr;
canvas!.style.width = `${w}px`;
canvas!.style.height = `${h}px`;
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
hexesRef.current = initHexes(w, h);
}
resize();
const ro = new ResizeObserver(resize);
ro.observe(canvas.parentElement!);
let prev = performance.now();
function frame(now: number) {
const dt = (now - prev) / 1000;
prev = now;
const scroll = scrollRef.current;
ctx!.clearRect(0, 0, w, h);
// Teal accent color, fade to 0 as user scrolls down
const globalAlpha = Math.max(0, 1 - scroll * 2);
for (const hex of hexesRef.current) {
// drift right, wrap
hex.x = (hex.x + hex.speed * dt) % (w + hex.size * 2);
// gentle vertical bob + parallax push from scroll
const bob = Math.sin(now / 1000 + hex.phase) * 8;
const parallax = scroll * h * 0.3 * (hex.speed / 20);
hex.y = hex.baseY + bob - parallax;
// wrap vertically
if (hex.y < -hex.size * 2) hex.y += h + hex.size * 4;
if (hex.y > h + hex.size * 2) hex.y -= h + hex.size * 4;
const dark = isDarkRef.current;
ctx!.globalAlpha = hex.opacity * globalAlpha * (dark ? 0.6 : 1);
ctx!.fillStyle = dark ? '#058172' : '#00a28c';
drawHex(ctx!, hex.x, hex.y, hex.size);
ctx!.fill();
ctx!.globalAlpha = hex.opacity * 0.5 * globalAlpha * (dark ? 0.6 : 1);
ctx!.strokeStyle = dark ? '#0a665b' : '#05c9aa';
ctx!.lineWidth = 1;
drawHex(ctx!, hex.x, hex.y, hex.size);
ctx!.stroke();
}
animRef.current = requestAnimationFrame(frame);
}
animRef.current = requestAnimationFrame(frame);
return () => {
cancelAnimationFrame(animRef.current);
ro.disconnect();
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 pointer-events-none"
style={{ zIndex: 0 }}
/>
);
}
// --- Fade-in hook ---
function useFadeInRef() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.classList.add('fade-in-visible');
observer.unobserve(el);
}
},
{ threshold: 0.15 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return ref;
}
// --- Page ---
export default function HomePage({ onOpenDashboard, theme = 'light' }: { onOpenDashboard: () => void; theme?: 'light' | 'dark' }) {
export default function HomePage({
onOpenDashboard,
theme = 'light',
}: {
onOpenDashboard: () => void;
theme?: 'light' | 'dark';
}) {
const scrollRef = useRef<HTMLDivElement>(null);
const [scrollProgress, setScrollProgress] = useState(0);
@ -268,7 +118,9 @@ export default function HomePage({ onOpenDashboard, theme = 'light' }: { onOpenD
className="rounded-xl bg-white dark:bg-navy-800 border border-warm-200 dark:border-navy-700 p-4 shadow-sm hover:shadow-md hover:border-teal-300 dark:hover:border-teal-600 transition-all"
>
<div className="text-2xl mb-2">{f.icon}</div>
<div className="font-semibold text-navy-950 dark:text-warm-100 text-sm">{f.label}</div>
<div className="font-semibold text-navy-950 dark:text-warm-100 text-sm">
{f.label}
</div>
<div className="text-xs text-warm-500 dark:text-warm-400 mt-0.5">{f.example}</div>
</div>
))}
@ -289,7 +141,9 @@ export default function HomePage({ onOpenDashboard, theme = 'light' }: { onOpenD
{i + 1}
</span>
<div>
<h3 className="font-semibold text-navy-950 dark:text-warm-100 text-lg">{step.title}</h3>
<h3 className="font-semibold text-navy-950 dark:text-warm-100 text-lg">
{step.title}
</h3>
<p className="text-warm-600 dark:text-warm-400 mt-0.5">{step.body}</p>
</div>
</div>
@ -315,7 +169,9 @@ export default function HomePage({ onOpenDashboard, theme = 'light' }: { onOpenD
{/* Final CTA */}
<div className="max-w-3xl mx-auto px-6 pb-24">
<div ref={ctaRef} className="fade-in-section text-center">
<h2 className="text-3xl font-bold text-navy-950 dark:text-warm-100 mb-3">Ready to narrow it down?</h2>
<h2 className="text-3xl font-bold text-navy-950 dark:text-warm-100 mb-3">
Ready to narrow it down?
</h2>
<p className="text-warm-500 dark:text-warm-400 mb-8 max-w-md mx-auto">
100% open data. No account required. Just set your filters and go.
</p>
@ -332,8 +188,6 @@ export default function HomePage({ onOpenDashboard, theme = 'light' }: { onOpenD
);
}
// --- Data ---
const FILTERS = [
{ icon: '\u00A3', label: 'Sale price', example: 'e.g. under \u00A3400k' },
{ icon: '\uD83D\uDE86', label: 'Commute time', example: 'e.g. < 45 min to Bank' },