a tool for shared writing and social publishing
at main 240 lines 8.3 kB view raw
1"use client"; 2 3import React from "react"; 4import { useUIState } from "src/useUIState"; 5 6import { elementId } from "src/utils/elementId"; 7 8import { useEntity, useReferenceToEntity, useReplicache } from "src/replicache"; 9 10import { DesktopPageFooter } from "../DesktopFooter"; 11import { Canvas } from "../Canvas"; 12import { Blocks } from "components/Blocks"; 13import { PublicationMetadata } from "./PublicationMetadata"; 14import { useCardBorderHidden } from "./useCardBorderHidden"; 15import { focusPage } from "src/utils/focusPage"; 16import { PageOptions } from "./PageOptions"; 17import { CardThemeProvider } from "components/ThemeManager/ThemeProvider"; 18import { useDrawerOpen } from "app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer"; 19import { usePreserveScroll } from "src/hooks/usePreserveScroll"; 20import { usePageFootnotes } from "components/Footnotes/usePageFootnotes"; 21import { FootnoteContext } from "components/Footnotes/FootnoteContext"; 22import { FootnoteSection } from "components/Footnotes/FootnoteSection"; 23import { FootnoteSideColumn } from "components/Footnotes/FootnoteSideColumn"; 24import { FootnotePopover } from "components/Footnotes/FootnotePopover"; 25 26export function Page(props: { 27 entityID: string; 28 first?: boolean; 29 fullPageScroll: boolean; 30}) { 31 let { rep } = useReplicache(); 32 33 let isFocused = useUIState((s) => { 34 let focusedElement = s.focusedEntity; 35 let focusedPageID = 36 focusedElement?.entityType === "page" 37 ? focusedElement.entityID 38 : focusedElement?.parent; 39 return focusedPageID === props.entityID; 40 }); 41 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 42 43 let drawerOpen = useDrawerOpen(props.entityID); 44 let footnoteData = usePageFootnotes(props.entityID); 45 let isRightmostPage = useUIState((s) => { 46 let pages = s.openPages; 47 if (pages.length === 0) return true; 48 return pages[pages.length - 1] === props.entityID; 49 }); 50 let sideColumnVisible = pageType === "doc" && !drawerOpen && isRightmostPage; 51 52 return ( 53 <CardThemeProvider entityID={props.entityID}> 54 <FootnoteContext.Provider value={footnoteData}> 55 <PageWrapper 56 onClickAction={(e) => { 57 if (e.defaultPrevented) return; 58 if (rep) { 59 if (isFocused) return; 60 focusPage(props.entityID, rep); 61 } 62 }} 63 id={elementId.page(props.entityID).container} 64 drawerOpen={!!drawerOpen} 65 isFocused={isFocused} 66 fullPageScroll={props.fullPageScroll} 67 pageType={pageType} 68 pageOptions={ 69 <PageOptions 70 entityID={props.entityID} 71 first={props.first} 72 isFocused={isFocused} 73 /> 74 } 75 footnoteSideColumn={ 76 <FootnoteSideColumn 77 pageEntityID={props.entityID} 78 visible={sideColumnVisible} 79 fullPageScroll={props.fullPageScroll} 80 /> 81 } 82 > 83 {props.first && pageType === "doc" && ( 84 <> 85 <PublicationMetadata /> 86 </> 87 )} 88 <PageContent entityID={props.entityID} first={props.first} /> 89 </PageWrapper> 90 <DesktopPageFooter pageID={props.entityID} /> 91 <FootnotePopover /> 92 </FootnoteContext.Provider> 93 </CardThemeProvider> 94 ); 95} 96 97export const PageWrapper = (props: { 98 id: string; 99 children: React.ReactNode; 100 pageOptions?: React.ReactNode; 101 footnoteSideColumn?: React.ReactNode; 102 fullPageScroll: boolean; 103 isFocused?: boolean; 104 onClickAction?: (e: React.MouseEvent) => void; 105 pageType: "canvas" | "doc"; 106 drawerOpen: boolean | undefined; 107 fixedWidth?: boolean; 108}) => { 109 const cardBorderHidden = useCardBorderHidden(); 110 let { ref } = usePreserveScroll<HTMLDivElement>(props.id); 111 return ( 112 // this div wraps the contents AND the page options. 113 // it needs to be its own div because this container does NOT scroll, and therefore doesn't clip the absolutely positioned pageOptions 114 <div 115 className={`pageWrapper relative shrink-0 ${props.fullPageScroll ? "w-full" : "w-max"}`} 116 > 117 {/* 118 this div is the scrolling container that wraps only the contents div. 119 120 it needs to be a separate div so that the user can scroll from anywhere on the page if there isn't a card border 121 */} 122 <div 123 ref={ref} 124 onClick={props.onClickAction} 125 id={props.id} 126 className={` 127 pageScrollWrapper 128 grow 129 shrink-0 snap-center 130 overflow-y-scroll 131 ${ 132 !cardBorderHidden && 133 `h-full border 134 bg-[rgba(var(--bg-page),var(--bg-page-alpha))] 135 ${props.drawerOpen ? "rounded-l-lg " : "rounded-lg"} 136 ${props.isFocused ? "shadow-md border-border" : "border-border-light"}` 137 } 138 ${cardBorderHidden && "sm:h-[calc(100%+48px)] h-[calc(100%+20px)] sm:-my-6 -my-3 sm:pt-6 pt-3"} 139 ${props.fullPageScroll && "max-w-full "} 140 ${props.pageType === "doc" && !props.fullPageScroll ? (props.fixedWidth ? "w-[10000px] sm:max-w-prose max-w-[var(--page-width-units)]" : "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]") : ""} 141 ${ 142 props.pageType === "canvas" && 143 !props.fullPageScroll && 144 "max-w-[var(--page-width-units)] sm:max-w-[calc(100vw-128px)] lg:max-w-fit lg:w-[calc(var(--page-width-units)*2 + 24px))]" 145 } 146 147`} 148 > 149 <div 150 className={`postPageContent footnote-scope 151 ${props.fullPageScroll ? "sm:max-w-[var(--page-width-units)] mx-auto" : "w-full h-full"} 152 `} 153 > 154 {props.children} 155 {props.pageType === "doc" && <div className="h-4 sm:h-6 w-full" />} 156 </div> 157 </div> 158 {props.pageOptions} 159 {props.footnoteSideColumn} 160 </div> 161 ); 162}; 163 164const PageContent = (props: { entityID: string; first?: boolean }) => { 165 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 166 if (pageType === "doc") return <DocContent entityID={props.entityID} />; 167 return <Canvas entityID={props.entityID} first={props.first} />; 168}; 169 170const DocContent = (props: { entityID: string }) => { 171 let { rootEntity } = useReplicache(); 172 173 let cardBorderHidden = useCardBorderHidden(props.entityID); 174 let rootBackgroundImage = useEntity( 175 rootEntity, 176 "theme/card-background-image", 177 ); 178 let rootBackgroundRepeat = useEntity( 179 rootEntity, 180 "theme/card-background-image-repeat", 181 ); 182 let rootBackgroundOpacity = useEntity( 183 rootEntity, 184 "theme/card-background-image-opacity", 185 ); 186 187 let cardBackgroundImage = useEntity( 188 props.entityID, 189 "theme/card-background-image", 190 ); 191 192 let cardBackgroundImageRepeat = useEntity( 193 props.entityID, 194 "theme/card-background-image-repeat", 195 ); 196 197 let cardBackgroundImageOpacity = useEntity( 198 props.entityID, 199 "theme/card-background-image-opacity", 200 ); 201 202 let backgroundImage = cardBackgroundImage || rootBackgroundImage; 203 let backgroundImageRepeat = cardBackgroundImage 204 ? cardBackgroundImageRepeat?.data?.value 205 : rootBackgroundRepeat?.data.value; 206 let backgroundImageOpacity = cardBackgroundImage 207 ? cardBackgroundImageOpacity?.data.value 208 : rootBackgroundOpacity?.data.value || 1; 209 210 return ( 211 <> 212 {!cardBorderHidden ? ( 213 <div 214 className={`pageBackground 215 absolute top-0 left-0 right-0 bottom-0 216 pointer-events-none 217 rounded-lg 218 `} 219 style={{ 220 backgroundImage: backgroundImage 221 ? `url(${backgroundImage.data.src}), url(${backgroundImage.data.fallback})` 222 : undefined, 223 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 224 backgroundPosition: "center", 225 backgroundSize: !backgroundImageRepeat 226 ? "cover" 227 : backgroundImageRepeat, 228 opacity: backgroundImage?.data.src ? backgroundImageOpacity : 1, 229 }} 230 /> 231 ) : null} 232 <Blocks entityID={props.entityID} /> 233 <FootnoteSection /> 234 <div className="h-4 sm:h-6 w-full" /> 235 {/* we handle page bg in this sepate div so that 236 we can apply an opacity the background image 237 without affecting the opacity of the rest of the page */} 238 </> 239 ); 240};