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