Skip to content
File

Blob: src/client/components/workspace/use-canonical-page-actions.ts

typescript98 lines
1import { useCallback, useRef } from "react";
2import type YProvider from "y-partyserver/provider";
3import { api } from "@/client/lib/api";
4import { reportClientError } from "@/client/lib/report-client-error";
5import { replicaCommands } from "@/client/stores/db/workspace-replica";
6import type { Workspace } from "@/shared/types";
7import type { ActivePagePatch, ActivePageSnapshot } from "@/client/lib/active-page-model";
8 
9interface UseCanonicalPageActionsInput {
10 workspace: Workspace | null;
11 page: ActivePageSnapshot | null;
12 syncProvider: YProvider | null;
13 patchPage: (updates: ActivePagePatch) => void;
14}
15 
16export function useCanonicalPageActions({ workspace, page, syncProvider, patchPage }: UseCanonicalPageActionsInput) {
17 const iconVersionRef = useRef(0);
18 const coverVersionRef = useRef(0);
19 
20 const patchRuntimeAndSnapshot = useCallback(
21 (updates: ActivePagePatch) => {
22 if (!workspace || !page) return;
23 patchPage(updates);
24 void replicaCommands.patchPage(workspace.id, page.id, {
25 ...(updates.title !== undefined ? { title: updates.title } : {}),
26 ...(updates.icon !== undefined ? { icon: updates.icon } : {}),
27 ...(updates.coverUrl !== undefined ? { cover_url: updates.coverUrl } : {}),
28 });
29 },
30 [workspace, page, patchPage],
31 );
32 
33 const handleTitleChange = useCallback(
34 (title: string) => {
35 patchRuntimeAndSnapshot({ title });
36 },
37 [patchRuntimeAndSnapshot],
38 );
39 
40 const handleIconChange = useCallback(
41 async (icon: string | null) => {
42 if (!workspace || !page) return;
43 const version = ++iconVersionRef.current;
44 patchRuntimeAndSnapshot({ icon });
45 try {
46 await api.pages.update(workspace.id, page.id, { icon });
47 syncProvider?.sendMessage(JSON.stringify({ type: "page-metadata-refresh" }));
48 } catch (error) {
49 if (iconVersionRef.current === version) {
50 patchRuntimeAndSnapshot({ icon: page.icon });
51 }
52 reportClientError({
53 source: "page.icon-update",
54 error,
55 context: {
56 workspaceId: workspace.id,
57 pageId: page.id,
58 icon,
59 },
60 });
61 }
62 },
63 [workspace, page, patchRuntimeAndSnapshot, syncProvider],
64 );
65 
66 const handleCoverChange = useCallback(
67 async (coverUrl: string | null) => {
68 if (!workspace || !page) return;
69 const version = ++coverVersionRef.current;
70 patchRuntimeAndSnapshot({ coverUrl });
71 try {
72 await api.pages.update(workspace.id, page.id, { cover_url: coverUrl });
73 syncProvider?.sendMessage(JSON.stringify({ type: "page-metadata-refresh" }));
74 } catch (error) {
75 if (coverVersionRef.current === version) {
76 patchRuntimeAndSnapshot({ coverUrl: page.coverUrl });
77 }
78 reportClientError({
79 source: "page.cover-update",
80 error,
81 context: {
82 workspaceId: workspace.id,
83 pageId: page.id,
84 hasCover: !!coverUrl,
85 },
86 });
87 }
88 },
89 [workspace, page, patchRuntimeAndSnapshot, syncProvider],
90 );
91 
92 return {
93 handleTitleChange,
94 handleIconChange,
95 handleCoverChange,
96 };
97}