a tool for shared writing and social publishing
1import { usePublicationData } from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider";
2import { useState } from "react";
3import { pickers, SectionArrow } from "./ThemeSetter";
4import { Color } from "react-aria-components";
5import {
6 PubLeafletPublication,
7 PubLeafletThemeBackgroundImage,
8} from "lexicons/api";
9import { AtUri } from "@atproto/syntax";
10import { useLocalPubTheme } from "./PublicationThemeProvider";
11import { BaseThemeProvider } from "./ThemeProvider";
12import { blobRefToSrc } from "src/utils/blobRefToSrc";
13import { updatePublicationTheme } from "app/lish/createPub/updatePublication";
14import { PagePickers } from "./PubPickers/PubTextPickers";
15import { BackgroundPicker } from "./PubPickers/PubBackgroundPickers";
16import { PubAccentPickers } from "./PubPickers/PubAcccentPickers";
17import { Separator } from "components/Layout";
18import { PubSettingsHeader } from "app/lish/[did]/[publication]/dashboard/PublicationSettings";
19
20export type ImageState = {
21 src: string;
22 file?: File;
23 repeat: number | null;
24};
25export const PubThemeSetter = (props: {
26 backToMenu: () => void;
27 loading: boolean;
28 setLoading: (l: boolean) => void;
29}) => {
30 let [sample, setSample] = useState<"pub" | "post">("pub");
31 let [openPicker, setOpenPicker] = useState<pickers>("null");
32 let { data, mutate } = usePublicationData();
33 let { publication: pub } = data || {};
34 let record = pub?.record as PubLeafletPublication.Record | undefined;
35 let [showPageBackground, setShowPageBackground] = useState(
36 !!record?.theme?.showPageBackground,
37 );
38 let {
39 theme: localPubTheme,
40 setTheme,
41 changes,
42 } = useLocalPubTheme(record, showPageBackground);
43 let [image, setImage] = useState<ImageState | null>(
44 PubLeafletThemeBackgroundImage.isMain(record?.theme?.backgroundImage)
45 ? {
46 src: blobRefToSrc(
47 record.theme.backgroundImage.image.ref,
48 pub?.identity_did!,
49 ),
50 repeat: record.theme.backgroundImage.repeat
51 ? record.theme.backgroundImage.width || 500
52 : null,
53 }
54 : null,
55 );
56
57 let pubBGImage = image?.src || null;
58 let leafletBGRepeat = image?.repeat || null;
59
60 return (
61 <BaseThemeProvider local {...localPubTheme}>
62 <form
63 onSubmit={async (e) => {
64 e.preventDefault();
65 if (!pub) return;
66 props.setLoading(true);
67 let result = await updatePublicationTheme({
68 uri: pub.uri,
69 theme: {
70 pageBackground: ColorToRGBA(localPubTheme.bgPage),
71 showPageBackground: showPageBackground,
72 backgroundColor: image
73 ? ColorToRGBA(localPubTheme.bgLeaflet)
74 : ColorToRGB(localPubTheme.bgLeaflet),
75 backgroundRepeat: image?.repeat,
76 backgroundImage: image ? image.file : null,
77 primary: ColorToRGB(localPubTheme.primary),
78 accentBackground: ColorToRGB(localPubTheme.accent1),
79 accentText: ColorToRGB(localPubTheme.accent2),
80 },
81 });
82 mutate((pub) => {
83 if (result?.publication && pub?.publication)
84 return {
85 ...pub,
86 publication: { ...pub.publication, ...result.publication },
87 };
88 return pub;
89 }, false);
90 props.setLoading(false);
91 }}
92 >
93 <PubSettingsHeader
94 loading={props.loading}
95 setLoadingAction={props.setLoading}
96 backToMenuAction={props.backToMenu}
97 state={"theme"}
98 />
99 </form>
100
101 <div className="themeSetterContent flex flex-col w-full overflow-y-scroll -mb-2 ">
102 <div className="themeBGLeaflet flex">
103 <div
104 className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `}
105 >
106 <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md text-[#595959] bg-white">
107 <BackgroundPicker
108 bgImage={image}
109 setBgImage={setImage}
110 backgroundColor={localPubTheme.bgLeaflet}
111 pageBackground={localPubTheme.bgPage}
112 setPageBackground={(color) => {
113 setTheme((t) => ({ ...t, bgPage: color }));
114 }}
115 setBackgroundColor={(color) => {
116 setTheme((t) => ({ ...t, bgLeaflet: color }));
117 }}
118 openPicker={openPicker}
119 setOpenPicker={setOpenPicker}
120 hasPageBackground={!!showPageBackground}
121 setHasPageBackground={setShowPageBackground}
122 />
123 </div>
124
125 <SectionArrow
126 fill="white"
127 stroke="#CCCCCC"
128 className="ml-2 -mt-[1px]"
129 />
130 </div>
131 </div>
132
133 <div
134 style={{
135 backgroundImage: pubBGImage ? `url(${pubBGImage})` : undefined,
136 backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat",
137 backgroundPosition: "center",
138 backgroundSize: !leafletBGRepeat
139 ? "cover"
140 : `calc(${leafletBGRepeat}px / 2 )`,
141 }}
142 className={` relative bg-bg-leaflet px-3 py-4 flex flex-col rounded-md border border-border `}
143 >
144 <div className={`flex flex-col gap-3 z-10`}>
145 <PagePickers
146 pageBackground={localPubTheme.bgPage}
147 primary={localPubTheme.primary}
148 setPageBackground={(color) => {
149 setTheme((t) => ({ ...t, bgPage: color }));
150 }}
151 setPrimary={(color) => {
152 setTheme((t) => ({ ...t, primary: color }));
153 }}
154 openPicker={openPicker}
155 setOpenPicker={(pickers) => setOpenPicker(pickers)}
156 hasPageBackground={showPageBackground}
157 />
158 <PubAccentPickers
159 accent1={localPubTheme.accent1}
160 setAccent1={(color) => {
161 setTheme((t) => ({ ...t, accent1: color }));
162 }}
163 accent2={localPubTheme.accent2}
164 setAccent2={(color) => {
165 setTheme((t) => ({ ...t, accent2: color }));
166 }}
167 openPicker={openPicker}
168 setOpenPicker={(pickers) => setOpenPicker(pickers)}
169 />
170 </div>
171 </div>
172 <div className="flex flex-col mt-4 ">
173 <div className="flex gap-2 items-center text-sm text-[#8C8C8C]">
174 <div className="text-sm">Preview</div>
175 <Separator classname="h-4!" />{" "}
176 <button
177 className={`${sample === "pub" ? "font-bold text-[#595959]" : ""}`}
178 onClick={() => setSample("pub")}
179 >
180 Pub
181 </button>
182 <button
183 className={`${sample === "post" ? "font-bold text-[#595959]" : ""}`}
184 onClick={() => setSample("post")}
185 >
186 Post
187 </button>
188 </div>
189 {sample === "pub" ? (
190 <SamplePub
191 pubBGImage={pubBGImage}
192 pubBGRepeat={leafletBGRepeat}
193 showPageBackground={showPageBackground}
194 />
195 ) : (
196 <SamplePost
197 pubBGImage={pubBGImage}
198 pubBGRepeat={leafletBGRepeat}
199 showPageBackground={showPageBackground}
200 />
201 )}
202 </div>
203 </div>
204 </BaseThemeProvider>
205 );
206};
207
208const SamplePub = (props: {
209 pubBGImage: string | null;
210 pubBGRepeat: number | null;
211 showPageBackground: boolean;
212}) => {
213 let { data } = usePublicationData();
214 let { publication } = data || {};
215 let record = publication?.record as PubLeafletPublication.Record | null;
216
217 return (
218 <div
219 style={{
220 backgroundImage: props.pubBGImage
221 ? `url(${props.pubBGImage})`
222 : undefined,
223 backgroundRepeat: props.pubBGRepeat ? "repeat" : "no-repeat",
224 backgroundPosition: "center",
225 backgroundSize: !props.pubBGRepeat
226 ? "cover"
227 : `calc(${props.pubBGRepeat}px / 2 )`,
228 }}
229 className={`bg-bg-leaflet p-3 pb-0 flex flex-col gap-3 rounded-t-md border border-border border-b-0 h-[148px] overflow-hidden `}
230 >
231 <div
232 className="sampleContent rounded-t-md border-border pb-4 px-[10px] flex flex-col gap-[14px] w-[250px] mx-auto"
233 style={{
234 background: props.showPageBackground
235 ? "rgba(var(--bg-page), var(--bg-page-alpha))"
236 : undefined,
237 }}
238 >
239 <div className="flex flex-col justify-center text-center pt-2">
240 {record?.icon && publication?.uri && (
241 <div
242 style={{
243 backgroundRepeat: "no-repeat",
244 backgroundPosition: "center",
245 backgroundSize: "cover",
246 backgroundImage: `url(/api/atproto_images?did=${new AtUri(publication.uri).host}&cid=${(record.icon?.ref as unknown as { $link: string })["$link"]})`,
247 }}
248 className="w-4 h-4 rounded-full place-self-center"
249 />
250 )}
251
252 <div className="text-[11px] font-bold pt-[5px] text-accent-contrast">
253 {record?.name}
254 </div>
255 <div className="text-[7px] font-normal text-tertiary">
256 {record?.description}
257 </div>
258 <div className=" flex gap-1 items-center mt-[6px] bg-accent-1 text-accent-2 py-px px-[4px] text-[7px] w-fit font-bold rounded-[2px] mx-auto">
259 <div className="h-[7px] w-[7px] rounded-full bg-accent-2" />
260 Subscribe with Bluesky
261 </div>
262 </div>
263
264 <div className="flex flex-col text-[8px] rounded-md ">
265 <div className="font-bold">A Sample Post</div>
266 <div className="text-secondary italic text-[6px]">
267 This is a sample description about the sample post
268 </div>
269 <div className="text-tertiary text-[5px] pt-[2px]">Jan 1, 20XX </div>
270 </div>
271 </div>
272 </div>
273 );
274};
275
276const SamplePost = (props: {
277 pubBGImage: string | null;
278 pubBGRepeat: number | null;
279 showPageBackground: boolean;
280}) => {
281 let { data } = usePublicationData();
282 let { publication } = data || {};
283 let record = publication?.record as PubLeafletPublication.Record | null;
284 return (
285 <div
286 style={{
287 backgroundImage: props.pubBGImage
288 ? `url(${props.pubBGImage})`
289 : undefined,
290 backgroundRepeat: props.pubBGRepeat ? "repeat" : "no-repeat",
291 backgroundPosition: "center",
292 backgroundSize: !props.pubBGRepeat
293 ? "cover"
294 : `calc(${props.pubBGRepeat}px / 2 )`,
295 }}
296 className={`bg-bg-leaflet p-3 max-w-full flex flex-col gap-3 rounded-t-md border border-border border-b-0 pb-0 h-[148px] overflow-hidden`}
297 >
298 <div
299 className="sampleContent rounded-t-md border-border pb-0 px-[6px] flex flex-col w-[250px] mx-auto"
300 style={{
301 background: props.showPageBackground
302 ? "rgba(var(--bg-page), var(--bg-page-alpha))"
303 : undefined,
304 }}
305 >
306 <div className="flex flex-col ">
307 <div className="text-[6px] font-bold pt-[6px] text-accent-contrast">
308 {record?.name}
309 </div>
310 <div className="text-[11px] font-bold text-primary">
311 A Sample Post
312 </div>
313 <div className="text-[7px] font-normal text-secondary italic">
314 A short sample description about the sample post
315 </div>
316 <div className="text-tertiary text-[5px] pt-[2px]">Jan 1, 20XX </div>
317 </div>
318 <div className="text-[6px] pt-[8px] flex flex-col gap-[6px]">
319 <div>
320 Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque
321 faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi
322 pretium tellus duis convallis. Tempus leo eu aenean sed diam urna
323 tempor.
324 </div>
325
326 <div>
327 Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis
328 massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit
329 semper vel class aptent taciti sociosqu. Ad litora torquent per
330 conubia nostra inceptos himenaeos.
331 </div>
332 <div>
333 Sed et nisi semper, egestas purus a, egestas nulla. Nulla ultricies,
334 purus non dapibus tincidunt, nunc sem rhoncus sem, vel malesuada
335 tellus enim sit amet magna. Donec ac justo a ipsum fermentum
336 vulputate. Etiam sit amet viverra leo. Aenean accumsan consectetur
337 velit. Vivamus at justo a nisl imperdiet dictum. Donec scelerisque
338 ex eget turpis scelerisque tincidunt. Proin non convallis nibh, eget
339 aliquet ex. Curabitur ornare a ipsum in ultrices.
340 </div>
341 </div>
342 </div>
343 </div>
344 );
345};
346
347export function ColorToRGBA(color: Color) {
348 if (!color)
349 return {
350 $type: "pub.leaflet.theme.color#rgba" as const,
351 r: 0,
352 g: 0,
353 b: 0,
354 a: 1,
355 };
356 let c = color.toFormat("rgba");
357 const r = c.getChannelValue("red");
358 const g = c.getChannelValue("green");
359 const b = c.getChannelValue("blue");
360 const a = c.getChannelValue("alpha");
361 return {
362 $type: "pub.leaflet.theme.color#rgba" as const,
363 r: Math.round(r),
364 g: Math.round(g),
365 b: Math.round(b),
366 a: Math.round(a * 100),
367 };
368}
369function ColorToRGB(color: Color) {
370 if (!color)
371 return {
372 $type: "pub.leaflet.theme.color#rgb" as const,
373 r: 0,
374 g: 0,
375 b: 0,
376 };
377 let c = color.toFormat("rgb");
378 const r = c.getChannelValue("red");
379 const g = c.getChannelValue("green");
380 const b = c.getChannelValue("blue");
381 return {
382 $type: "pub.leaflet.theme.color#rgb" as const,
383 r: Math.round(r),
384 g: Math.round(g),
385 b: Math.round(b),
386 };
387}