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