From 66212b4e475738e61d2ea78f3a7e72d7a610bf12 Mon Sep 17 00:00:00 2001 From: Austin Date: Sat, 23 May 2026 10:25:10 -0400 Subject: [PATCH] feat(board): persist category collapse state in localStorage for guests (#5714) * feat(board): persist category collapse state in localStorage for guests Authenticated users already had their collapse state saved server-side. This extends the same behaviour to unauthenticated (guest) users by reading/writing localStorage keyed by section ID. On mount, if the user is a guest, the stored value is applied to the disclosure. On toggle, guests write to localStorage instead of calling the protected API mutation. Closes #3355 Co-Authored-By: Claude Sonnet 4.6 * refactor: set localStorage value as useDisclosure default instead of useEffect Replaces the useEffect that read localStorage after mount with a direct initial value computation passed to useDisclosure. Removes the effect, the open/close destructuring, and the useEffect import. Note: initialCollapsed is computed during render rather than post-mount, which may cause a hydration mismatch on SSR if localStorage differs from the server-provided section.collapsed. The typeof window guard handles the SSR case. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .../board/sections/category-section.tsx | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/nextjs/src/components/board/sections/category-section.tsx b/apps/nextjs/src/components/board/sections/category-section.tsx index df204998b..592b39c68 100644 --- a/apps/nextjs/src/components/board/sections/category-section.tsx +++ b/apps/nextjs/src/components/board/sections/category-section.tsx @@ -3,6 +3,7 @@ import { useDisclosure } from "@mantine/hooks"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; +import { useSession } from "@homarr/auth/client"; import { useRequiredBoard } from "@homarr/boards/context"; import type { CategorySection } from "~/app/[locale]/boards/_types"; @@ -10,19 +11,36 @@ import { CategoryMenu } from "./category/category-menu"; import { GridStack } from "./gridstack/gridstack"; import classes from "./item.module.css"; +const localStorageKey = (sectionId: string) => `homarr-section-collapsed-${sectionId}`; + interface Props { section: CategorySection; } export const BoardCategorySection = ({ section }: Props) => { + const { status } = useSession(); const { mutate } = clientApi.section.changeCollapsed.useMutation(); const board = useRequiredBoard(); - const [opened, { toggle }] = useDisclosure(section.collapsed, { + + const initialCollapsed = + typeof window !== "undefined" + ? (localStorage.getItem(localStorageKey(section.id)) ?? String(section.collapsed)) === "true" + : section.collapsed; + + const [opened, { toggle }] = useDisclosure(initialCollapsed, { onOpen() { - mutate({ sectionId: section.id, collapsed: true }); + if (status === "authenticated") { + mutate({ sectionId: section.id, collapsed: true }); + } else if (status === "unauthenticated") { + localStorage.setItem(localStorageKey(section.id), "true"); + } }, onClose() { - mutate({ sectionId: section.id, collapsed: false }); + if (status === "authenticated") { + mutate({ sectionId: section.id, collapsed: false }); + } else if (status === "unauthenticated") { + localStorage.setItem(localStorageKey(section.id), "false"); + } }, });