a tool for shared writing and social publishing
at feature/post-options 213 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 "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}) => { 84 const cardBorderHidden = useCardBorderHidden(); 85 let { ref } = usePreserveScroll<HTMLDivElement>(props.id); 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 ref={ref} 99 onClick={props.onClickAction} 100 id={props.id} 101 className={` 102 pageScrollWrapper 103 grow 104 shrink-0 snap-center 105 overflow-y-scroll 106 ${ 107 !cardBorderHidden && 108 `h-full border 109 bg-[rgba(var(--bg-page),var(--bg-page-alpha))] 110 ${props.drawerOpen ? "rounded-l-lg " : "rounded-lg"} 111 ${props.isFocused ? "shadow-md border-border" : "border-border-light"}` 112 } 113 ${cardBorderHidden && "sm:h-[calc(100%+48px)] h-[calc(100%+20px)] sm:-my-6 -my-3 sm:pt-6 pt-3"} 114 ${props.fullPageScroll && "max-w-full "} 115 ${props.pageType === "doc" && !props.fullPageScroll && "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]"} 116 ${ 117 props.pageType === "canvas" && 118 !props.fullPageScroll && 119 "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))]" 120 } 121 122`} 123 > 124 <div 125 className={`postPageContent 126 ${props.fullPageScroll ? "sm:max-w-[var(--page-width-units)] mx-auto" : "w-full h-full"} 127 `} 128 > 129 {props.children} 130 {props.pageType === "doc" && <div className="h-4 sm:h-6 w-full" />} 131 </div> 132 </div> 133 {props.pageOptions} 134 </div> 135 ); 136}; 137 138const PageContent = (props: { entityID: string; first?: boolean }) => { 139 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 140 if (pageType === "doc") return <DocContent entityID={props.entityID} />; 141 return <Canvas entityID={props.entityID} first={props.first} />; 142}; 143 144const DocContent = (props: { entityID: string }) => { 145 let { rootEntity } = useReplicache(); 146 147 let cardBorderHidden = useCardBorderHidden(props.entityID); 148 let rootBackgroundImage = useEntity( 149 rootEntity, 150 "theme/card-background-image", 151 ); 152 let rootBackgroundRepeat = useEntity( 153 rootEntity, 154 "theme/card-background-image-repeat", 155 ); 156 let rootBackgroundOpacity = useEntity( 157 rootEntity, 158 "theme/card-background-image-opacity", 159 ); 160 161 let cardBackgroundImage = useEntity( 162 props.entityID, 163 "theme/card-background-image", 164 ); 165 166 let cardBackgroundImageRepeat = useEntity( 167 props.entityID, 168 "theme/card-background-image-repeat", 169 ); 170 171 let cardBackgroundImageOpacity = useEntity( 172 props.entityID, 173 "theme/card-background-image-opacity", 174 ); 175 176 let backgroundImage = cardBackgroundImage || rootBackgroundImage; 177 let backgroundImageRepeat = cardBackgroundImage 178 ? cardBackgroundImageRepeat?.data?.value 179 : rootBackgroundRepeat?.data.value; 180 let backgroundImageOpacity = cardBackgroundImage 181 ? cardBackgroundImageOpacity?.data.value 182 : rootBackgroundOpacity?.data.value || 1; 183 184 return ( 185 <> 186 {!cardBorderHidden ? ( 187 <div 188 className={`pageBackground 189 absolute top-0 left-0 right-0 bottom-0 190 pointer-events-none 191 rounded-lg 192 `} 193 style={{ 194 backgroundImage: backgroundImage 195 ? `url(${backgroundImage.data.src}), url(${backgroundImage.data.fallback})` 196 : undefined, 197 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 198 backgroundPosition: "center", 199 backgroundSize: !backgroundImageRepeat 200 ? "cover" 201 : backgroundImageRepeat, 202 opacity: backgroundImage?.data.src ? backgroundImageOpacity : 1, 203 }} 204 /> 205 ) : null} 206 <Blocks entityID={props.entityID} /> 207 <div className="h-4 sm:h-6 w-full" /> 208 {/* we handle page bg in this sepate div so that 209 we can apply an opacity the background image 210 without affecting the opacity of the rest of the page */} 211 </> 212 ); 213};