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