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}) => {
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};