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