Skip to content
File

Blob: src/client/components/share/view-provider.tsx

typescript107 lines
1import { useCallback, useEffect, useMemo, type ReactNode } from "react";
2import { useNavigate } from "@tanstack/react-router";
3import { useQuery } from "@tanstack/react-query";
4import { toApiError } from "@/client/lib/api";
5import { classifyFailure } from "@/client/lib/classify-failure";
6import { shareResolveQueryOptions } from "@/client/lib/queries/share-resolve";
7import { useAuthStore } from "@/client/stores/auth-store";
8import { reportClientError } from "@/client/lib/report-client-error";
9import { ShareViewContext, type ShareViewState, type ShareViewStatus } from "./use-share-view";
10import type { ShareRootPage } from "@/client/lib/share-page-model";
11import type { ResolvedViewerContext } from "@/shared/types";
12 
13interface ShareViewProviderProps {
14 token: string;
15 activePage: string | undefined;
16 children: ReactNode;
17}
18 
19export function ShareViewProvider({ token, activePage, children }: ShareViewProviderProps) {
20 const navigate = useNavigate();
21 const sessionMode = useAuthStore((s) => s.sessionMode);
22 const userId = useAuthStore((s) => s.user?.id ?? null);
23 const shareQuery = useQuery(shareResolveQueryOptions(token, sessionMode, userId));
24 
25 // Layer 1: Token resolution. Owns share-link identity only; page data lives
26 // in the page surface below. Dependencies are scoped to token/session/user
27 // so subpage navigation (`activePage`) keeps the mounted surface alive.
28 useEffect(() => {
29 if (!shareQuery.error) return;
30 const failureKind = classifyFailure(shareQuery.error, { online: navigator.onLine });
31 if (failureKind !== "network") {
32 reportClientError({
33 source: "shared-page.resolve",
34 error: shareQuery.error,
35 context: { sessionMode, failureKind },
36 });
37 }
38 }, [sessionMode, shareQuery.error]);
39 
40 const resolvedShare = shareQuery.data ?? null;
41 const status: ShareViewStatus = shareQuery.isError ? "error" : resolvedShare ? "ready" : "loading";
42 const error =
43 shareQuery.error && classifyFailure(shareQuery.error, { online: navigator.onLine }) === "network"
44 ? "This shared page requires a connection."
45 : shareQuery.error
46 ? toApiError(shareQuery.error).message
47 : null;
48 const workspaceId = resolvedShare?.workspace_id ?? null;
49 const viewer: ResolvedViewerContext | null = resolvedShare?.viewer ?? null;
50 const rootPage = useMemo<ShareRootPage | null>(
51 () =>
52 resolvedShare
53 ? {
54 id: resolvedShare.page_id,
55 kind: resolvedShare.kind,
56 title: resolvedShare.title,
57 icon: resolvedShare.icon,
58 cover_url: resolvedShare.cover_url,
59 permission: resolvedShare.permission,
60 }
61 : null,
62 [resolvedShare],
63 );
64 
65 const rootPageId = rootPage?.id ?? null;
66 const activePageId = rootPageId ? (activePage ?? rootPageId) : null;
67 
68 const navigateToPage = useCallback(
69 (pageId: string) => {
70 if (!rootPageId) return;
71 const page = pageId === rootPageId ? undefined : pageId;
72 navigate({ to: "/s/$token", params: { token }, search: { page } });
73 },
74 [rootPageId, token, navigate],
75 );
76 
77 const view = useMemo<ShareViewState>(
78 () =>
79 status === "ready" && workspaceId && viewer && rootPage && rootPageId && activePageId
80 ? {
81 status: "ready",
82 error: null,
83 token,
84 workspaceId,
85 viewer,
86 rootPageId,
87 rootPage,
88 activePageId,
89 navigate: navigateToPage,
90 }
91 : status === "error" && error
92 ? {
93 status: "error",
94 error,
95 token,
96 }
97 : {
98 status: "loading",
99 error: null,
100 token,
101 },
102 [status, workspaceId, viewer, rootPage, rootPageId, activePageId, token, navigateToPage, error],
103 );
104 
105 return <ShareViewContext.Provider value={view}>{children}</ShareViewContext.Provider>;
106}