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