a tool for shared writing and social publishing
at update/reader 214 lines 7.1 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"; 20 21export function Page(props: { 22 entityID: string; 23 first?: boolean; 24 fullPageScroll: boolean; 25}) { 26 let { rep } = useReplicache(); 27 28 let isFocused = useUIState((s) => { 29 let focusedElement = s.focusedEntity; 30 let focusedPageID = 31 focusedElement?.entityType === "page" 32 ? focusedElement.entityID 33 : focusedElement?.parent; 34 return focusedPageID === props.entityID; 35 }); 36 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 37 38 let drawerOpen = useDrawerOpen(props.entityID); 39 return ( 40 <CardThemeProvider entityID={props.entityID}> 41 <PageWrapper 42 onClickAction={(e) => { 43 if (e.defaultPrevented) return; 44 if (rep) { 45 if (isFocused) return; 46 focusPage(props.entityID, rep); 47 } 48 }} 49 id={elementId.page(props.entityID).container} 50 drawerOpen={!!drawerOpen} 51 isFocused={isFocused} 52 fullPageScroll={props.fullPageScroll} 53 pageType={pageType} 54 pageOptions={ 55 <PageOptions 56 entityID={props.entityID} 57 first={props.first} 58 isFocused={isFocused} 59 /> 60 } 61 > 62 {props.first && pageType === "doc" && ( 63 <> 64 <PublicationMetadata /> 65 </> 66 )} 67 <PageContent entityID={props.entityID} first={props.first} /> 68 </PageWrapper> 69 <DesktopPageFooter pageID={props.entityID} /> 70 </CardThemeProvider> 71 ); 72} 73 74export const PageWrapper = (props: { 75 id: string; 76 children: React.ReactNode; 77 pageOptions?: React.ReactNode; 78 fullPageScroll: boolean; 79 isFocused?: boolean; 80 onClickAction?: (e: React.MouseEvent) => void; 81 pageType: "canvas" | "doc"; 82 drawerOpen: boolean | undefined; 83 fixedWidth?: boolean; 84}) => { 85 const cardBorderHidden = useCardBorderHidden(); 86 let { ref } = usePreserveScroll<HTMLDivElement>(props.id); 87 return ( 88 // this div wraps the contents AND the page options. 89 // it needs to be its own div because this container does NOT scroll, and therefore doesn't clip the absolutely positioned pageOptions 90 <div 91 className={`pageWrapper relative shrink-0 ${props.fullPageScroll ? "w-full" : "w-max"}`} 92 > 93 {/* 94 this div is the scrolling container that wraps only the contents div. 95 96 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 97 */} 98 <div 99 ref={ref} 100 onClick={props.onClickAction} 101 id={props.id} 102 className={` 103 pageScrollWrapper 104 grow 105 shrink-0 snap-center 106 overflow-y-scroll 107 ${ 108 !cardBorderHidden && 109 `h-full border 110 bg-[rgba(var(--bg-page),var(--bg-page-alpha))] 111 ${props.drawerOpen ? "rounded-l-lg " : "rounded-lg"} 112 ${props.isFocused ? "shadow-md border-border" : "border-border-light"}` 113 } 114 ${cardBorderHidden && "sm:h-[calc(100%+48px)] h-[calc(100%+20px)] sm:-my-6 -my-3 sm:pt-6 pt-3"} 115 ${props.fullPageScroll && "max-w-full "} 116 ${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)]") : ""} 117 ${ 118 props.pageType === "canvas" && 119 !props.fullPageScroll && 120 "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))]" 121 } 122 123`} 124 > 125 <div 126 className={`postPageContent 127 ${props.fullPageScroll ? "sm:max-w-[var(--page-width-units)] mx-auto" : "w-full h-full"} 128 `} 129 > 130 {props.children} 131 {props.pageType === "doc" && <div className="h-4 sm:h-6 w-full" />} 132 </div> 133 </div> 134 {props.pageOptions} 135 </div> 136 ); 137}; 138 139const PageContent = (props: { entityID: string; first?: boolean }) => { 140 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 141 if (pageType === "doc") return <DocContent entityID={props.entityID} />; 142 return <Canvas entityID={props.entityID} first={props.first} />; 143}; 144 145const DocContent = (props: { entityID: string }) => { 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 return ( 186 <> 187 {!cardBorderHidden ? ( 188 <div 189 className={`pageBackground 190 absolute top-0 left-0 right-0 bottom-0 191 pointer-events-none 192 rounded-lg 193 `} 194 style={{ 195 backgroundImage: backgroundImage 196 ? `url(${backgroundImage.data.src}), url(${backgroundImage.data.fallback})` 197 : undefined, 198 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 199 backgroundPosition: "center", 200 backgroundSize: !backgroundImageRepeat 201 ? "cover" 202 : backgroundImageRepeat, 203 opacity: backgroundImage?.data.src ? backgroundImageOpacity : 1, 204 }} 205 /> 206 ) : null} 207 <Blocks entityID={props.entityID} /> 208 <div className="h-4 sm:h-6 w-full" /> 209 {/* we handle page bg in this sepate div so that 210 we can apply an opacity the background image 211 without affecting the opacity of the rest of the page */} 212 </> 213 ); 214};