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