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