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