From 09d5da7cf72af9c2a882d22596142429821d7550 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sun, 26 Apr 2026 01:41:58 -0700 Subject: [PATCH 1/2] fix(home): resync form and results to URL on back/forward navigation Round-2 review: the previous version only consumed search params on initial mount, so router.push from compare/swap/reset put history entries that the back button couldn't restore. Now a useEffect watches searchParams and resyncs username1/username2 + triggers a re-fetch when the URL pair differs from the last fetched pair. lastFetchedPairRef short-circuits the loop with our own router.push. --- app/page.tsx | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 8504569..45e5e38 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useMemo, useState } from "react"; +import { Suspense, useEffect, useEffectEvent, useMemo, useRef, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; import { CompareForm } from "../components/compare-form"; import { ResultDashboard } from "../components/result-dashboard"; import { DashboardSkeleton } from "../components/skeletons"; @@ -16,14 +17,24 @@ type ApiResponse = { error?: string; }; -export default function HomePage() { +function HomePageInner() { const { t } = useTranslation(); + const router = useRouter(); + const searchParams = useSearchParams(); + const initialUsernames = searchParams.getAll("username"); + const initialUsername1 = initialUsernames[0] ?? ""; + const initialUsername2 = initialUsernames[1] ?? ""; const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [username1, setUsername1] = useState(initialUsername1); + const [username2, setUsername2] = useState(initialUsername2); const [data, setData] = useState<{ user1: UserResult; user2: UserResult; } | null>(null); + // Track the URL pair we last fetched against so back/forward navigation + // can resync the form and results without re-fetching identical pairs. + const lastFetchedPairRef = useRef<[string, string] | null>(null); const localizeErrorMessage = (message?: string) => { switch (message) { @@ -43,6 +54,11 @@ export default function HomePage() { }; const handleCompare = async (u1: string, u2: string) => { + lastFetchedPairRef.current = [u1, u2]; + router.push( + `/?username=${encodeURIComponent(u1)}&username=${encodeURIComponent(u2)}`, + { scroll: false } + ); setLoading(true); setError(null); setData(null); @@ -79,14 +95,56 @@ export default function HomePage() { } }; + // Resync form + results to whatever the URL says — handles initial mount + // AND back/forward navigation. We fetch only when the URL pair differs from + // the last pair we fetched, so no infinite loop with the router.push above. + const syncToUrl = useEffectEvent((u1: string, u2: string) => { + setUsername1(u1); + setUsername2(u2); + + if (!u1 || !u2) { + // Empty params: clear results so the empty state matches the URL. + lastFetchedPairRef.current = null; + setData(null); + setError(null); + return; + } + + const last = lastFetchedPairRef.current; + if (last && last[0] === u1 && last[1] === u2) { + // URL already reflects the most recent fetch; nothing to do. + return; + } + + void handleCompare(u1, u2); + }); + + useEffect(() => { + const params = searchParams.getAll("username"); + syncToUrl(params[0] ?? "", params[1] ?? ""); + }, [searchParams]); + const skeleton = useMemo(() => , []); const reset = () => { setData(null); setError(null); + setUsername1(""); + setUsername2(""); + router.push("/", { scroll: false }); }; const swapUsers = () => { + const nextUsername1 = username2; + const nextUsername2 = username1; + + setUsername1(nextUsername1); + setUsername2(nextUsername2); + router.push( + `/?username=${encodeURIComponent(nextUsername1)}&username=${encodeURIComponent(nextUsername2)}`, + { scroll: false } + ); + if (!data) return; setData((d) => ({ user1: d!.user2, user2: d!.user1 })); }; @@ -97,6 +155,10 @@ export default function HomePage() {
); } + +export default function HomePage() { + return ( + }> + + + ); +} From 68fa4d383c5e83baa6f77107c802e1082b3a8361 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sun, 26 Apr 2026 13:34:20 -0700 Subject: [PATCH 2/2] fix(compare-form): accept username state as props The PR call site lifted username1/username2 and their setters into HomePageInner so URL searchParams could drive the form, but the component itself still owned its own useState pair. That broke the build and meant the prop-passed values were ignored at runtime. Move the four values into CompareFormProps, drop the internal useState, and let parent-supplied swapUsers/reset handle clearing and swapping. The local placeholder defaults ("pbiggar", "CoralineAda") are gone; the parent now seeds initial state from searchParams. --- components/compare-form.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/components/compare-form.tsx b/components/compare-form.tsx index 56c4d5a..350790d 100644 --- a/components/compare-form.tsx +++ b/components/compare-form.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from "react"; +import { useRef, useEffect } from "react"; import { Button } from "./ui/button"; import { ArrowLeftRight, RefreshCw } from "lucide-react"; import { @@ -12,6 +12,10 @@ import { Alert, AlertDescription } from "./ui/alert"; import { useTranslation } from "./language-provider"; type CompareFormProps = { + username1: string; + username2: string; + setUsername1: (value: string) => void; + setUsername2: (value: string) => void; data?: boolean; onSubmit: (u1: string, u2: string) => void; loading?: boolean; @@ -21,16 +25,17 @@ type CompareFormProps = { }; export function CompareForm({ + username1, + username2, + setUsername1, + setUsername2, onSubmit, - data, loading, swapUsers, reset, error, }: CompareFormProps) { const { t } = useTranslation(); - const [username1, setUsername1] = useState("pbiggar"); - const [username2, setUsername2] = useState("CoralineAda"); const firstInputRef = useRef(null); // Auto-focus first input on page load @@ -42,14 +47,10 @@ export function CompareForm({ const isEmpty = !username1.trim() && !username2.trim(); const handleSwap = () => { - setUsername1(username2); - setUsername2(username1); if (swapUsers) swapUsers(); }; const handleReset = () => { - setUsername1(""); - setUsername2(""); if (reset) reset(); };