Files
homarr/packages/boards/src/context.tsx
T

162 lines
4.5 KiB
TypeScript

"use client";
import type { PropsWithChildren } from "react";
import { createContext, useContext, useEffect, useSyncExternalStore } from "react";
import { usePathname } from "next/navigation";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { boardViewportWidthCookieName, getLayoutIdForViewportWidth } from "./layout-selection";
import { updateBoardName } from "./updater";
const BoardContext = createContext<{
board: RouterOutputs["board"]["getBoardByName"];
layoutOverrideId: string | null;
currentLayout: string;
initialViewportWidth: number;
} | null>(null);
const subscribeToViewport = (onStoreChange: () => void) => {
if (typeof window === "undefined") return () => undefined;
let animationFrame = 0;
const handleResize = () => {
if (animationFrame !== 0) return;
animationFrame = window.requestAnimationFrame(() => {
animationFrame = 0;
persistViewportWidth(getViewportWidth());
onStoreChange();
});
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
window.cancelAnimationFrame(animationFrame);
};
};
export const BoardProvider = ({
children,
initialBoard,
initialLayoutId,
initialViewportWidth,
layoutOverrideId = null,
}: PropsWithChildren<{
initialBoard: RouterOutputs["board"]["getBoardByName"];
initialLayoutId: string;
initialViewportWidth: number;
layoutOverrideId?: string | null;
}>) => {
const { data } = clientApi.board.getBoardByName.useQuery(
{ name: initialBoard.name },
{
initialData: initialBoard,
// The route already fetched this board, so keep it fresh long enough to
// avoid an immediate duplicate request while still surfacing remote edits
// when a user later returns to the board.
staleTime: 30_000,
},
);
const currentLayout = useSyncExternalStore(
subscribeToViewport,
() => getCurrentLayout(data, layoutOverrideId),
() => layoutOverrideId ?? initialLayoutId,
);
// Update the board name so it can be used within updateBoard method
updateBoardName(initialBoard.name);
const pathname = usePathname();
const utils = clientApi.useUtils();
// Invalidate the board when the pathname changes
// This allows to refetch the board when it might have changed - e.g. if someone else added an item
useEffect(() => {
return () => {
void utils.board.getBoardByName.invalidate({ name: initialBoard.name });
};
}, [pathname, utils, initialBoard.name]);
useEffect(() => {
persistViewportWidth(getViewportWidth());
}, []);
return (
<BoardContext.Provider
value={{
board: data,
layoutOverrideId,
currentLayout,
initialViewportWidth,
}}
>
{children}
</BoardContext.Provider>
);
};
export const useRequiredBoard = () => {
const optionalBoard = useOptionalBoard();
if (!optionalBoard) {
throw new Error("Board is required");
}
return optionalBoard;
};
export const useOptionalBoard = () => {
const context = useContext(BoardContext);
return context?.board ?? null;
};
export const useLayoutOverride = () => useContext(BoardContext)?.layoutOverrideId ?? null;
export const getCurrentLayout = (
board: RouterOutputs["board"]["getBoardByName"],
layoutOverrideId: string | null = null,
) => {
const layouts = board.layouts;
if (layoutOverrideId && layouts.some((layout) => layout.id === layoutOverrideId)) return layoutOverrideId;
const viewportWidth = typeof window === "undefined" ? 0 : getViewportWidth();
return getLayoutIdForViewportWidth(layouts, viewportWidth);
};
export const useCurrentLayout = () => {
const context = useContext(BoardContext);
if (!context) {
throw new Error("Board is required");
}
return context.currentLayout;
};
export const useInitialViewportWidth = () => {
const context = useContext(BoardContext);
if (!context) {
throw new Error("Board is required");
}
return context.initialViewportWidth;
};
export const getBoardLayouts = (board: RouterOutputs["board"]["getBoardByName"]) =>
board.layouts.map((layout) => layout.id);
export const useLayouts = () => {
const board = useRequiredBoard();
return getBoardLayouts(board);
};
const persistViewportWidth = (viewportWidth: number) => {
document.cookie = `${boardViewportWidthCookieName}=${Math.round(viewportWidth)}; Path=/; Max-Age=31536000; SameSite=Lax`;
};
const getViewportWidth = () => document.documentElement.clientWidth || window.innerWidth;