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}