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";
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 && pageType === "doc" && (
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};