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