From dadd2447b9a877a851832a8debfcd607136ae39a Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 12 Sep 2026 20:06:56 +0200 Subject: [PATCH] fix: the landing figures count when you can see them, and no contact form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The counters ran on mount, which is while the visitor is still reading the hero two screens above — so the animation finished before anybody could see it and the numbers simply appeared. They now start when the figures come into view, and a ref stops a later re-render sending them back to zero. The contact section is gone, and with it the Contact link in the footer. The endpoint behind it is untouched, so the form can come back somewhere else without being rebuilt. And the clinical tools say "the full vaccine schedule" rather than naming the two bodies that publish it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- frontend/src/pages/LandingPage.jsx | 121 +++++------------------ frontend/src/pages/LoginCaptcha.test.jsx | 15 +-- 2 files changed, 30 insertions(+), 106 deletions(-) diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx index 4eefddc..c5f1c2e 100644 --- a/frontend/src/pages/LandingPage.jsx +++ b/frontend/src/pages/LandingPage.jsx @@ -80,6 +80,12 @@ function useCalmMotion() { * depends on an optional API to become visible is content that disappears. */ function useReveal() { + const [ref, shown] = useInView() + return [ref, shown ? 'lp-reveal is-in' : 'lp-reveal'] +} + +/** True once the element has been scrolled into view, and true thereafter. */ +function useInView() { const ref = useRef(null) const [shown, setShown] = useState(typeof IntersectionObserver === 'undefined') @@ -92,7 +98,7 @@ function useReveal() { return () => observer.disconnect() }, [shown]) - return [ref, shown ? 'lp-reveal is-in' : 'lp-reveal'] + return [ref, shown] } function Reveal({ children, className = '' }) { @@ -109,9 +115,16 @@ function Reveal({ children, className = '' }) { */ function useCountUp(target, animate) { const [shown, setShown] = useState(animate ? 0 : target) + //: Whether this counter has already run. It starts when the figures come + //: into view, which is a state change after mount — without this, anything + //: that re-renders the section afterwards would send the number back to zero + //: and count it up again. + const done = useRef(false) useEffect(() => { + if (done.current) { setShown(target); return undefined } if (!animate || typeof requestAnimationFrame !== 'function') { setShown(target); return undefined } + done.current = true const started = Date.now() let frame = requestAnimationFrame(function step() { const progress = Math.min(1, (Date.now() - started) / 1100) @@ -148,6 +161,11 @@ function PublicFigures() { const calm = useCalmMotion() const [stats, setStats] = useState(null) const [failed, setFailed] = useState(false) + // Counted when the figures are on screen. They used to count on mount, which + // is while the visitor is still reading the hero two screens above — so the + // animation had finished long before anybody could see it, and the numbers + // simply appeared. + const [ref, inView] = useInView() useEffect(() => { let live = true @@ -162,7 +180,7 @@ function PublicFigures() { .filter(([, value]) => Number.isFinite(value) && value > 0) return ( -
+

What is in the bank today

@@ -172,7 +190,7 @@ function PublicFigures() { {figures.length > 0 && (
    {figures.map(([label, value]) => ( -
    +
    ))}
)} @@ -214,87 +232,6 @@ function Rotator({ calm }) { } // ── Contact form ────────────────────────────────────────────────────────────── -function ContactForm() { - const [form, setForm] = useState({ name: '', email: '', type: 'question', message: '' }) - const [captchaToken, setCaptchaToken] = useState('') - const [loading, setLoading] = useState(false) - const [sent, setSent] = useState(false) - const [error, setError] = useState('') - - const canSubmit = form.name && form.email && form.message && (!captchaSiteKey() || captchaToken) - - const submit = async (e) => { - e.preventDefault() - setError('') - setLoading(true) - try { - await api.post('/contact', { ...form, captcha_token: captchaToken || null }) - setSent(true) - } catch (err) { - setError(err.response?.data?.detail || 'Something went wrong. Please try again.') - } finally { - setLoading(false) - } - } - - if (sent) return ( -
- -

Message received

-

- {form.type === 'moderator' - ? "We'll review your application and get back to you." - : "We'll get back to you shortly."} -

-
- ) - - return ( -
-
-
- - setForm(f => ({ ...f, name: e.target.value }))} /> -
-
- - setForm(f => ({ ...f, email: e.target.value }))} /> -
-
-
- -
- {[['question', '💬 Question'], ['moderator', '🛡 Apply as Moderator']].map(([val, label]) => ( - - ))} -
- {form.type === 'moderator' && ( -

- Moderators can upload material and manage the question bank. Tell us about your - background and how you would like to contribute. -

- )} -