a tool for shared writing and social publishing
at update/thread-viewer 297 lines 10 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( 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}