From 6d42c67da06fa6251fad183d040f2b4e9f599c74 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 12 Apr 2026 03:49:38 +0100 Subject: [PATCH 1/2] [#835] Fix SSR hydration mismatch in Writer tab expired badge - Change useState(checkExpired) to useState(false) to avoid Date.now() during server render - Replace 1-second setInterval with a single setTimeout targeting the exact expiry time - Cleanup timeout on unmount Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- src/app/profile/[address]/page.tsx | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index b77f0774..671f8478 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.19", + "version": "0.1.20", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index aa2b95d8..65072c26 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -884,18 +884,19 @@ function StoryRow({ enabled: !!storyline.token_address, }); - const checkExpired = useCallback( - () => !storyline.sunset && storyline.has_deadline && !!storyline.last_plot_time && - Date.now() > new Date(storyline.last_plot_time).getTime() + DEADLINE_MS, - [storyline.sunset, storyline.has_deadline, storyline.last_plot_time], - ); - const [isExpired, setIsExpired] = useState(checkExpired); + const [isExpired, setIsExpired] = useState(false); useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect -- initial sync needed for SSR hydration safety - setIsExpired(checkExpired()); - const interval = setInterval(() => setIsExpired(checkExpired()), 1_000); - return () => clearInterval(interval); - }, [checkExpired]); + if (storyline.sunset || !storyline.has_deadline || !storyline.last_plot_time) return; + const expiryTime = new Date(storyline.last_plot_time).getTime() + DEADLINE_MS; + const remaining = expiryTime - Date.now(); + if (remaining <= 0) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- one-time sync for already-expired storylines + setIsExpired(true); + return; + } + const timeout = setTimeout(() => setIsExpired(true), remaining); + return () => clearTimeout(timeout); + }, [storyline.sunset, storyline.has_deadline, storyline.last_plot_time]); return ( <> From c00207a7141b0b501aeb2426b30c0818bbc2e906 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 12 Apr 2026 03:51:15 +0100 Subject: [PATCH 2/2] [#835] Fix stale isExpired state on prop changes Reset isExpired to false when effect re-runs with changed props, so a previously expired row correctly reverts to active after a deadline extension or new plot. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/profile/[address]/page.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index 65072c26..9ca2b541 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -886,7 +886,11 @@ function StoryRow({ const [isExpired, setIsExpired] = useState(false); useEffect(() => { - if (storyline.sunset || !storyline.has_deadline || !storyline.last_plot_time) return; + if (storyline.sunset || !storyline.has_deadline || !storyline.last_plot_time) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- reset when props change (e.g. deadline extension) + setIsExpired(false); + return; + } const expiryTime = new Date(storyline.last_plot_time).getTime() + DEADLINE_MS; const remaining = expiryTime - Date.now(); if (remaining <= 0) { @@ -894,6 +898,8 @@ function StoryRow({ setIsExpired(true); return; } + // eslint-disable-next-line react-hooks/set-state-in-effect -- reset in case props changed from expired to active + setIsExpired(false); const timeout = setTimeout(() => setIsExpired(true), remaining); return () => clearTimeout(timeout); }, [storyline.sunset, storyline.has_deadline, storyline.last_plot_time]);