Refactor
This commit is contained in:
parent
2c613dc0d1
commit
a677b9331f
28 changed files with 1647 additions and 1498 deletions
|
|
@ -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' },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue