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