profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM

Commit

add scroll reveal for below-the-fold sections

commit 5664155

2 changed files with +55 and −2

Jump to a changed file
  1. src/app/page.tsx +3 −2
  2. src/components/reveal.tsx +52 −0
modified src/app/page.tsx +3 −2
@@ -3,6 +3,7 @@import Link from 'next/link';
3 3 import { LandingBackground } from '@/components/landing-background';
4 4 import { LandingHero } from '@/components/landing-hero';
5 5 import { LandingMarquee } from '@/components/landing-marquee';
6 +import { Reveal } from '@/components/reveal';
6 7 import { Button } from '@/components/ui/button';
7 8 import type { DebateResult } from '@/core/types';
8 9 import { listDemoDebates, loadDebateResult } from '@/db/repositories';
@@ -35,12 +36,12 @@export default async function HomePage() {
35 36
36 37 {/* The five moves of a debate, as a colored pipeline (no boxes). */}
37 38 <section className="container border-t py-16 sm:py-24">
38 - <div className="mx-auto max-w-2xl text-center">
39 + <Reveal className="mx-auto max-w-2xl text-center">
39 40 <p className="font-mono text-xs uppercase tracking-[0.22em] text-muted-foreground">how a debate unfolds</p>
40 41 <h2 className="mt-3 font-display text-3xl font-medium tracking-tight sm:text-4xl">
41 42 Five moves from question to verdict
42 43 </h2>
43 - </div>
44 + </Reveal>
44 45 <ol className="relative mx-auto mt-14 grid max-w-5xl gap-y-10 sm:grid-cols-5 sm:gap-x-4">
45 46 <div
46 47 aria-hidden
added src/components/reveal.tsx +52 −0
@@ -0,0 +1,52 @@
1 +'use client';
2 +
3 +import { useEffect, useRef, useState } from 'react';
4 +import { cn } from '@/lib/utils';
5 +
6 +/** Reveals its children with a soft slide-up the first time they scroll into view. */
7 +export function Reveal({
8 + children,
9 + className,
10 + delay = 0,
11 +}: {
12 + children: React.ReactNode;
13 + className?: string;
14 + delay?: number;
15 +}) {
16 + const ref = useRef<HTMLDivElement>(null);
17 + const [shown, setShown] = useState(false);
18 +
19 + useEffect(() => {
20 + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
21 + setShown(true);
22 + return;
23 + }
24 + const el = ref.current;
25 + if (!el) return;
26 + const observer = new IntersectionObserver(
27 + (entries) => {
28 + if (entries[0]?.isIntersecting) {
29 + setShown(true);
30 + observer.disconnect();
31 + }
32 + },
33 + { threshold: 0.15 },
34 + );
35 + observer.observe(el);
36 + return () => observer.disconnect();
37 + }, []);
38 +
39 + return (
40 + <div
41 + ref={ref}
42 + style={{ transitionDelay: `${delay}ms` }}
43 + className={cn(
44 + 'transition-all duration-700 ease-out motion-reduce:transition-none',
45 + shown ? 'translate-y-0 opacity-100' : 'translate-y-5 opacity-0',
46 + className,
47 + )}
48 + >
49 + {children}
50 + </div>
51 + );
52 +}