a tool for shared writing and social publishing
at main 14 kB view raw
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};