a tool for shared writing and social publishing
1"use client";
2import { Popover } from "components/Popover";
3
4import { Color } from "react-aria-components";
5
6import {
7 LeafletBackgroundPicker,
8 PageThemePickers,
9} from "./Pickers/PageThemePickers";
10import { PageWidthSetter } from "./Pickers/PageWidthSetter";
11import { useMemo, useState } from "react";
12import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache";
13import { Replicache } from "replicache";
14import { FilterAttributes } from "src/replicache/attributes";
15import { colorToString } from "components/ThemeManager/useColorAttribute";
16import { useEntitySetContext } from "components/EntitySetProvider";
17import { ActionButton } from "components/ActionBar/ActionButton";
18import { CheckboxChecked } from "components/Icons/CheckboxChecked";
19import { CheckboxEmpty } from "components/Icons/CheckboxEmpty";
20import { PaintSmall } from "components/Icons/PaintSmall";
21import { AccentPickers } from "./Pickers/AccentPickers";
22import { useLeafletPublicationData } from "components/PageSWRDataProvider";
23import { useIsMobile } from "src/hooks/isMobile";
24import { Toggle } from "components/Toggle";
25import { getFontConfig, getFontFamilyValue } from "src/fonts";
26
27export type pickers =
28 | "null"
29 | "leaflet"
30 | "page"
31 | "accent-1"
32 | "accent-2"
33 | "text"
34 | "highlight-1"
35 | "highlight-2"
36 | "highlight-3"
37 | "page-background-image"
38 | "page-width";
39
40export function setColorAttribute(
41 rep: Replicache<ReplicacheMutators> | null,
42 entity: string,
43) {
44 return (attribute: keyof FilterAttributes<{ type: "color" }>) =>
45 (color: Color) =>
46 rep?.mutate.assertFact({
47 entity,
48 attribute,
49 data: { type: "color", value: colorToString(color, "hsba") },
50 });
51}
52export const ThemePopover = (props: { entityID: string; home?: boolean }) => {
53 let { rep } = useReplicache();
54 let { data: pub } = useLeafletPublicationData();
55 let isMobile = useIsMobile();
56
57 // I need to get these variables from replicache and then write them to the DB. I also need to parse them into a state that can be used here.
58 let permission = useEntitySetContext().permissions.write;
59 let leafletBGImage = useEntity(props.entityID, "theme/background-image");
60 let leafletBGRepeat = useEntity(
61 props.entityID,
62 "theme/background-image-repeat",
63 );
64
65 let [openPicker, setOpenPicker] = useState<pickers>(
66 props.home === true ? "leaflet" : "null",
67 );
68 let set = useMemo(() => {
69 return setColorAttribute(rep, props.entityID);
70 }, [rep, props.entityID]);
71
72 if (!permission) return null;
73 if (pub?.publications) return null;
74
75 return (
76 <>
77 <Popover
78 className="w-80 bg-white py-3!"
79 arrowFill="#FFFFFF"
80 asChild
81 side={isMobile ? "top" : "right"}
82 align={isMobile ? "center" : "start"}
83 trigger={<ActionButton icon={<PaintSmall />} label="Theme" />}
84 >
85 <ThemeSetterContent {...props} />
86 </Popover>
87 </>
88 );
89};
90
91export const ThemeSetterContent = (props: {
92 entityID: string;
93 home?: boolean;
94}) => {
95 let { rep } = useReplicache();
96 let { data: pub } = useLeafletPublicationData();
97
98 // I need to get these variables from replicache and then write them to the DB. I also need to parse them into a state that can be used here.
99 let permission = useEntitySetContext().permissions.write;
100 let leafletBGImage = useEntity(props.entityID, "theme/background-image");
101 let leafletBGRepeat = useEntity(
102 props.entityID,
103 "theme/background-image-repeat",
104 );
105
106 let [openPicker, setOpenPicker] = useState<pickers>(
107 props.home === true ? "leaflet" : "null",
108 );
109 let set = useMemo(() => {
110 return setColorAttribute(rep, props.entityID);
111 }, [rep, props.entityID]);
112
113 if (!permission) return null;
114 if (pub?.publications) return null;
115 return (
116 <div className="themeSetterContent flex flex-col w-full overflow-y-scroll no-scrollbar">
117 {!props.home && (
118 <PageWidthSetter
119 entityID={props.entityID}
120 thisPicker={"page-width"}
121 openPicker={openPicker}
122 setOpenPicker={setOpenPicker}
123 closePicker={() => setOpenPicker("null")}
124 />
125 )}
126 <div className="themeBGLeaflet flex">
127 <div className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `}>
128 <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md">
129 <LeafletBackgroundPicker
130 entityID={props.entityID}
131 openPicker={openPicker}
132 setOpenPicker={setOpenPicker}
133 />
134 </div>
135
136 <SectionArrow fill="white" stroke="#CCCCCC" className="ml-2 -mt-px" />
137 </div>
138 </div>
139
140 <div
141 onClick={(e) => {
142 e.currentTarget === e.target && setOpenPicker("leaflet");
143 }}
144 style={{
145 backgroundImage: leafletBGImage
146 ? `url(${leafletBGImage.data.src})`
147 : undefined,
148 backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat",
149 backgroundPosition: "center",
150 backgroundSize: !leafletBGRepeat
151 ? "cover"
152 : `calc(${leafletBGRepeat.data.value}px / 2 )`,
153 }}
154 className={`bg-bg-leaflet px-3 pt-4 pb-0 mb-2 flex flex-col gap-4 rounded-md border border-border`}
155 >
156 <PageThemePickers
157 entityID={props.entityID}
158 openPicker={openPicker}
159 setOpenPicker={(pickers) => setOpenPicker(pickers)}
160 home={props.home}
161 />
162 <div className="flex flex-col -gap-[6px]">
163 <div className={`flex flex-col z-10 -mb-[6px] `}>
164 <AccentPickers
165 entityID={props.entityID}
166 openPicker={openPicker}
167 setOpenPicker={(pickers) => setOpenPicker(pickers)}
168 />
169 <SectionArrow
170 fill="rgb(var(--accent-2))"
171 stroke="rgb(var(--accent-1))"
172 className="ml-2"
173 />
174 </div>
175
176 <SampleButton
177 entityID={props.entityID}
178 setOpenPicker={setOpenPicker}
179 />
180 </div>
181
182 <SamplePage
183 setOpenPicker={setOpenPicker}
184 home={props.home}
185 entityID={props.entityID}
186 />
187 </div>
188 {!props.home && <WatermarkSetter entityID={props.entityID} />}
189 </div>
190 );
191};
192
193function WatermarkSetter(props: { entityID: string }) {
194 let { rep } = useReplicache();
195 let checked = useEntity(props.entityID, "theme/page-leaflet-watermark");
196
197 function handleToggle() {
198 rep?.mutate.assertFact({
199 entity: props.entityID,
200 attribute: "theme/page-leaflet-watermark",
201 data: { type: "boolean", value: !checked?.data.value },
202 });
203 }
204 return (
205 <div className="flex gap-2 items-start mt-0.5">
206 <Toggle
207 toggle={!!checked?.data.value}
208 onToggle={() => {
209 handleToggle();
210 }}
211 disabledColor1="#8C8C8C"
212 disabledColor2="#DBDBDB"
213 >
214 <div className="flex flex-col gap-0 items-start ">
215 <div className="font-bold">Show Leaflet Watermark</div>
216 <div className="text-sm text-[#969696]">Help us spread the word!</div>
217 </div>
218 </Toggle>
219 </div>
220 );
221}
222
223const SampleButton = (props: {
224 entityID: string;
225 setOpenPicker: (thisPicker: pickers) => void;
226}) => {
227 return (
228 <div
229 onClick={(e) => {
230 e.target === e.currentTarget && props.setOpenPicker("accent-1");
231 }}
232 className="pointer-cursor font-bold relative text-center text-lg py-2 rounded-md bg-accent-1 text-accent-2 shadow-md flex items-center justify-center"
233 >
234 <div
235 className="cursor-pointer w-fit"
236 onClick={() => {
237 props.setOpenPicker("accent-2");
238 }}
239 >
240 Example Button
241 </div>
242 </div>
243 );
244};
245const SamplePage = (props: {
246 entityID: string;
247 home: boolean | undefined;
248 setOpenPicker: (picker: "page" | "text") => void;
249}) => {
250 let pageBGImage = useEntity(props.entityID, "theme/card-background-image");
251 let pageBGRepeat = useEntity(
252 props.entityID,
253 "theme/card-background-image-repeat",
254 );
255 let pageBGOpacity = useEntity(
256 props.entityID,
257 "theme/card-background-image-opacity",
258 );
259 let pageBorderHidden = useEntity(props.entityID, "theme/card-border-hidden")
260 ?.data.value;
261
262 // Read font values directly since the popover is portalled outside .leafletWrapper
263 let headingFontId = useEntity(props.entityID, "theme/heading-font")?.data.value;
264 let bodyFontId = useEntity(props.entityID, "theme/body-font")?.data.value;
265 let bodyFontFamily = getFontFamilyValue(getFontConfig(bodyFontId));
266 let headingFontFamily = getFontFamilyValue(getFontConfig(headingFontId));
267
268 return (
269 <div
270 onClick={(e) => {
271 e.currentTarget === e.target && props.setOpenPicker("page");
272 }}
273 className={`
274 text-primary relative
275 ${
276 pageBorderHidden
277 ? "py-2 px-0 border border-transparent"
278 : `cursor-pointer p-2 border border-border border-b-transparent shadow-md
279 ${props.home ? "rounded-md " : "rounded-t-lg "}`
280 }`}
281 style={
282 pageBorderHidden
283 ? undefined
284 : {
285 backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))",
286 }
287 }
288 >
289 <div
290 className="background absolute top-0 right-0 bottom-0 left-0 z-0 rounded-t-lg"
291 style={
292 pageBorderHidden
293 ? undefined
294 : {
295 backgroundImage: pageBGImage
296 ? `url(${pageBGImage.data.src})`
297 : undefined,
298
299 backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat",
300 opacity: pageBGOpacity?.data.value || 1,
301 backgroundSize: !pageBGRepeat
302 ? "cover"
303 : `calc(${pageBGRepeat.data.value}px / 2 )`,
304 }
305 }
306 />
307 <div className="z-10 relative" style={{ fontFamily: bodyFontFamily }}>
308 <p
309 onClick={() => {
310 props.setOpenPicker("text");
311 }}
312 className="cursor-pointer font-bold w-fit"
313 style={{ fontFamily: headingFontFamily }}
314 >
315 Hello!
316 </p>
317 <small onClick={() => props.setOpenPicker("text")}>
318 Welcome to{" "}
319 <span className="font-bold text-accent-contrast">Leaflet</span> — a
320 fun and easy way to make, share, and collab on little bits of paper ✨
321 </small>
322 </div>
323 </div>
324 );
325};
326
327export const SectionArrow = (props: {
328 fill: string;
329 stroke: string;
330 className: string;
331}) => {
332 return (
333 <svg
334 width="24"
335 height="12"
336 viewBox="0 0 24 12"
337 fill="none"
338 xmlns="http://www.w3.org/2000/svg"
339 className={props.className}
340 >
341 <path d="M11.9999 12L24 0H0L11.9999 12Z" fill={props.fill} />
342 <path
343 fillRule="evenodd"
344 clipRule="evenodd"
345 d="M1.33552 0L12 10.6645L22.6645 0H24L12 12L0 0H1.33552Z"
346 fill={props.stroke}
347 />
348 </svg>
349 );
350};