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