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