From bb39519801b3c1a685f0da6c628d1ef6f83a74a6 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 16 Sep 2026 10:03:41 +0200 Subject: [PATCH] feat(board): refresh the board when the window regains focus --- src/components/board/board.tsx | 9 +++++- src/components/board/use-refresh-on-focus.ts | 34 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/components/board/use-refresh-on-focus.ts diff --git a/src/components/board/board.tsx b/src/components/board/board.tsx index a0bfa44..964522c 100644 --- a/src/components/board/board.tsx +++ b/src/components/board/board.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { DndContext, DragOverlay, @@ -21,6 +21,7 @@ import { CSS } from "@dnd-kit/utilities"; import { BoardColumn } from "@/components/board/board-column"; import { CardTile } from "@/components/board/card-tile"; import { ClientCardControls } from "@/components/board/client-card-controls"; +import { useRefreshOnFocus } from "@/components/board/use-refresh-on-focus"; import { computeDropPosition } from "@/lib/board/move"; import type { CardRecord, ColumnRecord, Role } from "@/lib/pb/types"; @@ -106,6 +107,12 @@ export function Board({ const activeCard = activeId ? cards.find((c) => c.id === activeId) : null; + useRefreshOnFocus(activeId !== null); + + useEffect(() => { + if (activeId === null) setCards(initialCards); + }, [initialCards, activeId]); + function onDragStart(event: DragStartEvent) { setActiveId(String(event.active.id)); } diff --git a/src/components/board/use-refresh-on-focus.ts b/src/components/board/use-refresh-on-focus.ts new file mode 100644 index 0000000..233253f --- /dev/null +++ b/src/components/board/use-refresh-on-focus.ts @@ -0,0 +1,34 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; + +/** + * Re-fetch the server component tree when the tab regains focus. + * + * `router.refresh()` re-runs the page's server components, so the board is + * reloaded through the same read rules as the first render. Nothing is + * fetched client-side and no token is needed. + * + * Skipped while `paused` is true, so a refresh cannot land mid-drag and pull + * the card out from under the pointer. + */ +export function useRefreshOnFocus(paused: boolean) { + const router = useRouter(); + + useEffect(() => { + if (paused) return; + + function onFocus() { + if (document.visibilityState === "visible") router.refresh(); + } + + window.addEventListener("visibilitychange", onFocus); + window.addEventListener("focus", onFocus); + + return () => { + window.removeEventListener("visibilitychange", onFocus); + window.removeEventListener("focus", onFocus); + }; + }, [paused, router]); +} \ No newline at end of file