Skip to content
File

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

typescript154 lines
1import { useCallback } from "react";
2import { ChevronRight } from "lucide-react";
3import { DocumentPage } from "@/client/components/editor/document-page";
4import { CanvasPage } from "@/client/components/canvas/canvas-page";
5import { DEFAULT_PAGE_TITLE } from "@/shared/constants";
6import { PageBreadcrumbs } from "@/client/components/ui/page-breadcrumbs";
7import { PageChrome, PageEmojiIcon } from "@/client/components/ui/page-chrome";
8import { PageCover } from "@/client/components/ui/page-cover";
9import { PageErrorState } from "@/client/components/ui/page-error-state";
10import { PageLoadingSkeleton } from "@/client/components/ui/page-loading-skeleton";
11import { Skeleton } from "@/client/components/ui/skeleton";
12import { useDocumentTitle } from "@/client/hooks/use-document-title";
13import { useOnline } from "@/client/hooks/use-online";
14import {
15 useActivePageActions,
16 useActivePageState,
17 useActivePageSync,
18} from "@/client/components/active-page/use-active-page";
19import { useSharedPagePresentation } from "@/client/components/share/use-share-view";
20import { deriveSharePageAffordance } from "@/client/lib/affordance/share-page";
21 
22function SharedBreadcrumbSkeleton() {
23 return (
24 <div className="text-xs" aria-hidden="true">
25 <div className="hidden items-center gap-2 md:flex">
26 <Skeleton className="h-3.5 w-20 rounded-sm" />
27 <ChevronRight className="h-3 w-3 shrink-0 text-zinc-700" />
28 <Skeleton className="h-3.5 w-24 rounded-sm" />
29 <ChevronRight className="h-3 w-3 shrink-0 text-zinc-700" />
30 <Skeleton className="h-3.5 w-28 rounded-sm" />
31 </div>
32 <div className="flex items-center gap-2 md:hidden">
33 <Skeleton className="h-3.5 w-24 rounded-sm" />
34 <ChevronRight className="h-3 w-3 shrink-0 text-zinc-700" />
35 <Skeleton className="h-3.5 w-3 rounded-sm" />
36 </div>
37 </div>
38 );
39}
40 
41export function SharePageView() {
42 const activePageState = useActivePageState();
43 const { patchPage } = useActivePageActions();
44 const { setSyncProvider } = useActivePageSync();
45 const presentation = useSharedPagePresentation();
46 const online = useOnline();
47 const handleTitleChange = useCallback(
48 (titleOverride: string) => {
49 patchPage({ title: titleOverride });
50 },
51 [patchPage],
52 );
53 
54 useDocumentTitle(presentation.displayTitle || DEFAULT_PAGE_TITLE);
55 
56 if (activePageState.kind === "unavailable") {
57 return (
58 <PageErrorState
59 message={presentation.unavailableMessage ?? activePageState.message}
60 className="h-full"
61 action={{
62 label: "Back to shared page",
63 onClick: () => presentation.navigate(presentation.rootPageId),
64 }}
65 />
66 );
67 }
68 
69 if (presentation.isPageLoading || !presentation.page) {
70 return (
71 <PageLoadingSkeleton
72 canvasLayout="stage"
73 kind={presentation.isRootActive ? presentation.rootPage.kind : "unknown"}
74 documentLayout="rail"
75 />
76 );
77 }
78 
79 const page = presentation.page;
80 const showBreadcrumbSlot = presentation.isAncestorTrailLoading || presentation.ancestors.length > 0;
81 const pageAffordance = deriveSharePageAffordance({
82 pageKind: page.kind,
83 pageAccess: presentation.isViewOnly ? "view" : "edit",
84 workspaceId: presentation.workspaceId,
85 online,
86 });
87 
88 const sharedChrome = <SharedPageChrome presentation={presentation} showBreadcrumbSlot={showBreadcrumbSlot} />;
89 
90 if (pageAffordance.kind === "doc") {
91 return (
92 <DocumentPage
93 key={`${page.id}:${presentation.token}`}
94 pageId={page.id}
95 initialTitle={page.title}
96 onTitleChange={handleTitleChange}
97 onProvider={setSyncProvider}
98 shareToken={presentation.token}
99 workspaceId={presentation.workspaceId}
100 affordance={pageAffordance.editor}
101 outlineMode="rail"
102 chrome={sharedChrome}
103 />
104 );
105 }
106 
107 return (
108 <CanvasPage
109 pageId={page.id}
110 initialTitle={page.title}
111 onTitleChange={handleTitleChange}
112 onProvider={setSyncProvider}
113 shareToken={presentation.token}
114 workspaceId={presentation.workspaceId}
115 affordance={pageAffordance.canvas}
116 layout="stage"
117 chrome={sharedChrome}
118 />
119 );
120}
121 
122interface SharedPageChromeProps {
123 presentation: ReturnType<typeof useSharedPagePresentation>;
124 showBreadcrumbSlot: boolean;
125}
126 
127function SharedPageChrome({ presentation, showBreadcrumbSlot }: SharedPageChromeProps) {
128 const breadcrumb = showBreadcrumbSlot ? (
129 presentation.isAncestorTrailLoading ? (
130 <SharedBreadcrumbSkeleton />
131 ) : (
132 <PageBreadcrumbs
133 mode="shared"
134 ancestors={presentation.ancestors}
135 currentTitle={presentation.displayTitle}
136 currentIcon={presentation.displayIcon}
137 onNavigate={presentation.navigate}
138 />
139 )
140 ) : null;
141 
142 return (
143 <PageChrome
144 cover={
145 presentation.displayCoverUrl ? (
146 <PageCover coverUrl={presentation.displayCoverUrl} shareToken={presentation.token} />
147 ) : null
148 }
149 breadcrumb={breadcrumb}
150 icon={presentation.displayIcon ? <PageEmojiIcon emoji={presentation.displayIcon} /> : null}
151 />
152 );
153}