From b7bf44498bff64887cd35dfe93e697a356fe91ba Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 2 Sep 2025 19:59:08 -0700 Subject: [PATCH] feat: implement server-side version fetching with hydration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add VersionHydrator component to sync server data to client state - Update UpdateToastClient to use valtio reactivity with useSnapshot - Ensure version data is fetched server-side and properly hydrated - Fix issue where appState.version was never populated from backend 🤖 Generated with Claude Code https://claude.ai/code Co-Authored-By: Claude --- app/components/UpdateToastClient.tsx | 24 +++++++++++++++++------- app/components/VersionHydrator.tsx | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 app/components/VersionHydrator.tsx diff --git a/app/components/UpdateToastClient.tsx b/app/components/UpdateToastClient.tsx index 8b386552..a00189c2 100644 --- a/app/components/UpdateToastClient.tsx +++ b/app/components/UpdateToastClient.tsx @@ -4,24 +4,33 @@ import { useEffect, useState } from 'react' import { usePathname } from 'next/navigation' import { add, format } from 'date-fns' import { getCookie } from 'cookies-next' +import { useSnapshot } from 'valtio' import { appState } from '~/utils/appState' import UpdateToast from '~/components/toasts/UpdateToast' -export default function UpdateToastClient() { +interface UpdateToastClientProps { + initialVersion?: AppUpdate | null +} + +export default function UpdateToastClient({ initialVersion }: UpdateToastClientProps) { const pathname = usePathname() const [updateToastOpen, setUpdateToastOpen] = useState(false) + const { version } = useSnapshot(appState) + + // Use initialVersion for SSR, then switch to appState version after hydration + const effectiveVersion = version?.updated_at ? version : initialVersion useEffect(() => { - if (appState.version) { + if (effectiveVersion && effectiveVersion.updated_at) { const cookie = getToastCookie() const now = new Date() - const updatedAt = new Date(appState.version.updated_at) + const updatedAt = new Date(effectiveVersion.updated_at) const validUntil = add(updatedAt, { days: 7 }) if (now < validUntil && !cookie.seen) setUpdateToastOpen(true) } - }, []) + }, [effectiveVersion?.updated_at]) function getToastCookie() { if (appState.version && appState.version.updated_at !== '') { @@ -47,14 +56,15 @@ export default function UpdateToastClient() { const path = pathname.replaceAll('/', '') - if (!['about', 'updates', 'roadmap'].includes(path) && appState.version) { + // Only render toast if we have valid version data with update_type + if (!['about', 'updates', 'roadmap'].includes(path) && effectiveVersion && effectiveVersion.update_type) { return ( ) } diff --git a/app/components/VersionHydrator.tsx b/app/components/VersionHydrator.tsx new file mode 100644 index 00000000..6623db03 --- /dev/null +++ b/app/components/VersionHydrator.tsx @@ -0,0 +1,18 @@ +'use client' + +import { useEffect } from 'react' +import { appState } from '~/utils/appState' + +interface VersionHydratorProps { + version: AppUpdate | null +} + +export default function VersionHydrator({ version }: VersionHydratorProps) { + useEffect(() => { + if (version && version.updated_at) { + appState.version = version + } + }, [version]) + + return null +} \ No newline at end of file