a tool for shared writing and social publishing
1"use client";
2import { BlockProps, ListMarker, Block, BlockLayout } from "./Block";
3import { focusBlock } from "src/utils/focusBlock";
4
5import { focusPage } from "src/utils/focusPage";
6import { useEntity, useReplicache } from "src/replicache";
7import { useUIState } from "src/useUIState";
8import { RenderedTextBlock } from "components/Blocks/TextBlock";
9import { usePageMetadata } from "src/hooks/queries/usePageMetadata";
10import { CSSProperties, useEffect, useRef, useState } from "react";
11import { useBlocks } from "src/hooks/queries/useBlocks";
12import { Canvas, CanvasBackground, CanvasContent } from "components/Canvas";
13import { CardThemeProvider } from "components/ThemeManager/ThemeProvider";
14import { useCardBorderHidden } from "components/Pages/useCardBorderHidden";
15
16export function PageLinkBlock(
17 props: BlockProps & {
18 preview?: boolean;
19 areYouSure?: boolean;
20 setAreYouSure?: (value: boolean) => void;
21 },
22) {
23 let page = useEntity(props.entityID, "block/card");
24 let type =
25 useEntity(page?.data.value || null, "page/type")?.data.value || "doc";
26 let { rep } = useReplicache();
27
28 let isSelected = useUIState((s) =>
29 s.selectedBlocks.find((b) => b.value === props.entityID),
30 );
31
32 let isOpen = useUIState((s) => s.openPages).includes(page?.data.value || "");
33 if (!page)
34 return <div>An error occured, there should be a page linked here!</div>;
35
36 return (
37 <CardThemeProvider entityID={page?.data.value}>
38 <BlockLayout
39 hasBackground="page"
40 isSelected={!!isSelected}
41 areYouSure={props.areYouSure}
42 setAreYouSure={props.setAreYouSure}
43 className={`cursor-pointer
44 pageLinkBlockWrapper relative group/pageLinkBlock
45 flex overflow-clip p-0!
46 ${isOpen && "border-accent-contrast! outline-accent-contrast!"}
47 `}
48 >
49 <div
50 className="w-full h-full"
51 onClick={(e) => {
52 if (!page) return;
53 if (e.isDefaultPrevented()) return;
54 if (e.shiftKey) return;
55 e.preventDefault();
56 e.stopPropagation();
57 useUIState.getState().openPage(props.parent, page.data.value);
58 if (rep) focusPage(page.data.value, rep);
59 }}
60 >
61 {type === "canvas" && page ? (
62 <CanvasLinkBlock entityID={page?.data.value} />
63 ) : (
64 <DocLinkBlock {...props} />
65 )}
66 </div>
67 </BlockLayout>
68 </CardThemeProvider>
69 );
70}
71export function DocLinkBlock(props: BlockProps & { preview?: boolean }) {
72 let { rep } = useReplicache();
73 let page = useEntity(props.entityID, "block/card");
74 let pageEntity = page ? page.data.value : props.entityID;
75 let leafletMetadata = usePageMetadata(pageEntity);
76
77 return (
78 <div
79 style={{ "--list-marker-width": "20px" } as CSSProperties}
80 className={`
81 w-full h-[104px]
82 `}
83 >
84 <>
85 <div
86 className="pageLinkBlockContent w-full flex overflow-clip cursor-pointer h-full"
87 onClick={(e) => {
88 if (e.isDefaultPrevented()) return;
89 if (e.shiftKey) return;
90 e.preventDefault();
91 e.stopPropagation();
92 useUIState.getState().openPage(props.parent, pageEntity);
93 if (rep) focusPage(pageEntity, rep);
94 }}
95 >
96 <div className="my-2 ml-3 grow min-w-0 text-sm bg-transparent overflow-clip ">
97 {leafletMetadata[0] && (
98 <div
99 className={`pageBlockOne outline-hidden resize-none align-top flex gap-2 ${leafletMetadata[0].type === "heading" ? "font-bold text-base" : ""}`}
100 >
101 {leafletMetadata[0].listData && (
102 <ListMarker
103 {...leafletMetadata[0]}
104 className={
105 leafletMetadata[0].type === "heading"
106 ? "pt-[12px]!"
107 : "pt-[8px]!"
108 }
109 />
110 )}
111 <RenderedTextBlock
112 entityID={leafletMetadata[0].value}
113 type="text"
114 />
115 </div>
116 )}
117 {leafletMetadata[1] && (
118 <div
119 className={`pageBlockLineTwo outline-hidden resize-none align-top flex gap-2 ${leafletMetadata[1].type === "heading" ? "font-bold" : ""}`}
120 >
121 {leafletMetadata[1].listData && (
122 <ListMarker {...leafletMetadata[1]} className="pt-[8px]!" />
123 )}
124 <RenderedTextBlock
125 entityID={leafletMetadata[1].value}
126 type="text"
127 />
128 </div>
129 )}
130 {leafletMetadata[2] && (
131 <div
132 className={`pageBlockLineThree outline-hidden resize-none align-top flex gap-2 ${leafletMetadata[2].type === "heading" ? "font-bold" : ""}`}
133 >
134 {leafletMetadata[2].listData && (
135 <ListMarker {...leafletMetadata[2]} className="pt-[8px]!" />
136 )}
137 <RenderedTextBlock
138 entityID={leafletMetadata[2].value}
139 type="text"
140 />
141 </div>
142 )}
143 </div>
144 {!props.preview && <PagePreview entityID={pageEntity} />}
145 </div>
146 </>
147 </div>
148 );
149}
150
151export function PagePreview(props: { entityID: string }) {
152 let blocks = useBlocks(props.entityID);
153 let previewRef = useRef<HTMLDivElement | null>(null);
154 let { rootEntity } = useReplicache();
155
156 let cardBorderHidden = useCardBorderHidden(props.entityID);
157 let rootBackgroundImage = useEntity(
158 rootEntity,
159 "theme/card-background-image",
160 );
161 let rootBackgroundRepeat = useEntity(
162 rootEntity,
163 "theme/card-background-image-repeat",
164 );
165 let rootBackgroundOpacity = useEntity(
166 rootEntity,
167 "theme/card-background-image-opacity",
168 );
169
170 let cardBackgroundImage = useEntity(
171 props.entityID,
172 "theme/card-background-image",
173 );
174
175 let cardBackgroundImageRepeat = useEntity(
176 props.entityID,
177 "theme/card-background-image-repeat",
178 );
179
180 let cardBackgroundImageOpacity = useEntity(
181 props.entityID,
182 "theme/card-background-image-opacity",
183 );
184
185 let backgroundImage = cardBackgroundImage || rootBackgroundImage;
186 let backgroundImageRepeat = cardBackgroundImage
187 ? cardBackgroundImageRepeat?.data?.value
188 : rootBackgroundRepeat?.data.value;
189 let backgroundImageOpacity = cardBackgroundImage
190 ? cardBackgroundImageOpacity?.data.value
191 : rootBackgroundOpacity?.data.value || 1;
192
193 let pageWidth = `var(--page-width-unitless)`;
194 return (
195 <div
196 ref={previewRef}
197 inert
198 className={`pageLinkBlockPreview w-[120px] overflow-clip mx-3 mt-3 -mb-2 border rounded-md shrink-0 border-border-light flex flex-col gap-0.5 rotate-[4deg] origin-center ${cardBorderHidden ? "" : "bg-bg-page"}`}
199 >
200 <div
201 className="absolute top-0 left-0 origin-top-left pointer-events-none "
202 style={{
203 width: `calc(1px * ${pageWidth})`,
204 height: `calc(100vh - 64px)`,
205 transform: `scale(calc((120 / ${pageWidth} )))`,
206 backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))",
207 }}
208 >
209 {!cardBorderHidden && (
210 <div
211 className={`pageLinkBlockBackground
212 absolute top-0 left-0 right-0 bottom-0
213 pointer-events-none
214 `}
215 style={{
216 backgroundImage: backgroundImage
217 ? `url(${backgroundImage.data.src}), url(${backgroundImage.data.fallback})`
218 : undefined,
219 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat",
220 backgroundPosition: "center",
221 backgroundSize: !backgroundImageRepeat
222 ? "cover"
223 : backgroundImageRepeat,
224 opacity: backgroundImage?.data.src ? backgroundImageOpacity : 1,
225 }}
226 />
227 )}
228 {blocks.slice(0, 20).map((b, index, arr) => {
229 return (
230 <BlockPreview
231 pageType="doc"
232 entityID={b.value}
233 previousBlock={arr[index - 1] || null}
234 nextBlock={arr[index + 1] || null}
235 nextPosition={""}
236 previewRef={previewRef}
237 {...b}
238 key={b.factID}
239 />
240 );
241 })}
242 </div>
243 </div>
244 );
245}
246
247const CanvasLinkBlock = (props: { entityID: string; preview?: boolean }) => {
248 let pageWidth = `var(--page-width-unitless)`;
249 return (
250 <div
251 style={{ contain: "size layout paint" }}
252 className={`pageLinkBlockPreview shrink-0 h-[200px] w-full overflow-clip relative`}
253 >
254 <div
255 className={`absolute top-0 left-0 origin-top-left pointer-events-none w-full`}
256 style={{
257 width: `calc(1px * ${pageWidth})`,
258 height: "calc(1150px * 2)",
259 transform: `scale(calc(((${pageWidth} - 36) / 1272 )))`,
260 }}
261 >
262 {props.preview ? (
263 <CanvasBackground entityID={props.entityID} />
264 ) : (
265 <CanvasContent entityID={props.entityID} preview />
266 )}
267 </div>
268 </div>
269 );
270};
271
272export function BlockPreview(
273 b: BlockProps & {
274 previewRef: React.RefObject<HTMLDivElement | null>;
275 },
276) {
277 let ref = useRef<HTMLDivElement | null>(null);
278 let [isVisible, setIsVisible] = useState(true);
279 useEffect(() => {
280 if (!ref.current) return;
281 let observer = new IntersectionObserver(
282 (entries) => {
283 entries.forEach((entry) => {
284 if (entry.isIntersecting) {
285 setIsVisible(true);
286 } else {
287 setIsVisible(false);
288 }
289 });
290 },
291 { threshold: 0.01, root: b.previewRef.current },
292 );
293 observer.observe(ref.current);
294 return () => observer.disconnect();
295 }, [b.previewRef]);
296 return <div ref={ref}>{isVisible && <Block {...b} preview />}</div>;
297}