a tool for shared writing and social publishing
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 "."; 16import { PageOptions } from "./PageOptions"; 17import { CardThemeProvider } from "components/ThemeManager/ThemeProvider"; 18import { useDrawerOpen } from "app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer"; 19 20export function Page(props: { 21 entityID: string; 22 first?: boolean; 23 fullPageScroll: boolean; 24}) { 25 let { rep } = useReplicache(); 26 27 let isFocused = useUIState((s) => { 28 let focusedElement = s.focusedEntity; 29 let focusedPageID = 30 focusedElement?.entityType === "page" 31 ? focusedElement.entityID 32 : focusedElement?.parent; 33 return focusedPageID === props.entityID; 34 }); 35 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 36 let cardBorderHidden = useCardBorderHidden(props.entityID); 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 cardBorderHidden={!!cardBorderHidden} 52 isFocused={isFocused} 53 fullPageScroll={props.fullPageScroll} 54 pageType={pageType} 55 pageOptions={ 56 <PageOptions 57 entityID={props.entityID} 58 first={props.first} 59 isFocused={isFocused} 60 /> 61 } 62 > 63 {props.first && ( 64 <> 65 <PublicationMetadata /> 66 </> 67 )} 68 <PageContent entityID={props.entityID} first={props.first} /> 69 </PageWrapper> 70 <DesktopPageFooter pageID={props.entityID} /> 71 </CardThemeProvider> 72 ); 73} 74 75export const PageWrapper = (props: { 76 id: string; 77 children: React.ReactNode; 78 pageOptions?: React.ReactNode; 79 cardBorderHidden: boolean; 80 fullPageScroll: boolean; 81 isFocused?: boolean; 82 onClickAction?: (e: React.MouseEvent) => void; 83 pageType: "canvas" | "doc"; 84 drawerOpen: boolean | undefined; 85}) => { 86 return ( 87 // this div wraps the contents AND the page options. 88 // it needs to be its own div because this container does NOT scroll, and therefore doesn't clip the absolutely positioned pageOptions 89 <div 90 className={`pageWrapper relative shrink-0 ${props.fullPageScroll ? "w-full" : "w-max"}`} 91 > 92 {/* 93 this div is the scrolling container that wraps only the contents div. 94 95 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 96 */} 97 <div 98 onClick={props.onClickAction} 99 id={props.id} 100 className={` 101 pageScrollWrapper 102 grow 103 shrink-0 snap-center 104 overflow-y-scroll 105 ${ 106 !props.cardBorderHidden && 107 `h-full border 108 bg-[rgba(var(--bg-page),var(--bg-page-alpha))] 109 ${props.drawerOpen ? "rounded-l-lg " : "rounded-lg"} 110 ${props.isFocused ? "shadow-md border-border" : "border-border-light"}` 111 } 112 ${props.cardBorderHidden && "sm:h-[calc(100%+48px)] h-[calc(100%+20px)] sm:-my-6 -my-3 sm:pt-6 pt-3"} 113 ${props.fullPageScroll && "max-w-full "} 114 ${props.pageType === "doc" && !props.fullPageScroll && "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]"} 115 ${ 116 props.pageType === "canvas" && 117 !props.fullPageScroll && 118 "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))]" 119 } 120 121`} 122 > 123 <div 124 className={`postPageContent 125 ${props.fullPageScroll ? "sm:max-w-[var(--page-width-units)] mx-auto" : "w-full h-full"} 126 `} 127 > 128 {props.children} 129 {props.pageType === "doc" && <div className="h-4 sm:h-6 w-full" />} 130 </div> 131 </div> 132 {props.pageOptions} 133 </div> 134 ); 135}; 136 137const PageContent = (props: { entityID: string; first?: boolean }) => { 138 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 139 if (pageType === "doc") return <DocContent entityID={props.entityID} />; 140 return <Canvas entityID={props.entityID} first={props.first} />; 141}; 142 143const DocContent = (props: { entityID: string }) => { 144 let { rootEntity } = useReplicache(); 145 146 let cardBorderHidden = useCardBorderHidden(props.entityID); 147 let rootBackgroundImage = useEntity( 148 rootEntity, 149 "theme/card-background-image", 150 ); 151 let rootBackgroundRepeat = useEntity( 152 rootEntity, 153 "theme/card-background-image-repeat", 154 ); 155 let rootBackgroundOpacity = useEntity( 156 rootEntity, 157 "theme/card-background-image-opacity", 158 ); 159 160 let cardBackgroundImage = useEntity( 161 props.entityID, 162 "theme/card-background-image", 163 ); 164 165 let cardBackgroundImageRepeat = useEntity( 166 props.entityID, 167 "theme/card-background-image-repeat", 168 ); 169 170 let cardBackgroundImageOpacity = useEntity( 171 props.entityID, 172 "theme/card-background-image-opacity", 173 ); 174 175 let backgroundImage = cardBackgroundImage || rootBackgroundImage; 176 let backgroundImageRepeat = cardBackgroundImage 177 ? cardBackgroundImageRepeat?.data?.value 178 : rootBackgroundRepeat?.data.value; 179 let backgroundImageOpacity = cardBackgroundImage 180 ? cardBackgroundImageOpacity?.data.value 181 : rootBackgroundOpacity?.data.value || 1; 182 183 return ( 184 <> 185 {!cardBorderHidden ? ( 186 <div 187 className={`pageBackground 188 absolute top-0 left-0 right-0 bottom-0 189 pointer-events-none 190 rounded-lg 191 `} 192 style={{ 193 backgroundImage: backgroundImage 194 ? `url(${backgroundImage.data.src}), url(${backgroundImage.data.fallback})` 195 : undefined, 196 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 197 backgroundPosition: "center", 198 backgroundSize: !backgroundImageRepeat 199 ? "cover" 200 : backgroundImageRepeat, 201 opacity: backgroundImage?.data.src ? backgroundImageOpacity : 1, 202 }} 203 /> 204 ) : null} 205 <Blocks entityID={props.entityID} /> 206 <div className="h-4 sm:h-6 w-full" /> 207 {/* we handle page bg in this sepate div so that 208 we can apply an opacity the background image 209 without affecting the opacity of the rest of the page */} 210 </> 211 ); 212};