a tool for shared writing and social publishing
at main 9.9 kB view raw
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}