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