a tool for shared writing and social publishing
at feature/email 342 lines 11 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 { PubLeafletThemeBackgroundImage } from "lexicons/api"; 8import { AtUri } from "@atproto/syntax"; 9import { useLocalPubTheme } from "./PublicationThemeProvider"; 10import { BaseThemeProvider, CardBorderHiddenContext } from "./ThemeProvider"; 11import { blobRefToSrc } from "src/utils/blobRefToSrc"; 12import { updatePublicationTheme } from "app/lish/createPub/updatePublication"; 13import { PagePickers } from "./PubPickers/PubTextPickers"; 14import { BackgroundPicker } from "./PubPickers/PubBackgroundPickers"; 15import { PubAccentPickers } from "./PubPickers/PubAcccentPickers"; 16import { Separator } from "components/Layout"; 17 18import { ColorToRGB, ColorToRGBA } from "./colorToLexicons"; 19import { useToaster } from "components/Toast"; 20import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 21import { PubPageWidthSetter } from "./PubPickers/PubPageWidthSetter"; 22import { FontPicker } from "./Pickers/TextPickers"; 23import { GoToArrow } from "components/Icons/GoToArrow"; 24import { ButtonPrimary } from "components/Buttons"; 25import { PresetThemePicker } from "./PubPickers/PubPresetPicker"; 26 27export type ImageState = { 28 src: string; 29 file?: File; 30 repeat: number | null; 31}; 32 33export function usePubThemeEditorState() { 34 let [openPicker, setOpenPicker] = useState<pickers>("null"); 35 let { data, mutate } = usePublicationData(); 36 let { publication: pub } = data || {}; 37 let record = useNormalizedPublicationRecord(); 38 let [showPageBackground, setShowPageBackground] = useState( 39 !!record?.theme?.showPageBackground, 40 ); 41 let { 42 theme: localPubTheme, 43 setTheme, 44 changes, 45 resetChanges, 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 [headingFont, setHeadingFont] = useState<string | undefined>( 64 record?.theme?.headingFont, 65 ); 66 let [bodyFont, setBodyFont] = useState<string | undefined>( 67 record?.theme?.bodyFont, 68 ); 69 let pubBGImage = image?.src || null; 70 let leafletBGRepeat = image?.repeat || null; 71 let toaster = useToaster(); 72 73 let submitTheme = async (setLoading: (l: boolean) => void) => { 74 if (!pub) return; 75 setLoading(true); 76 let result = await updatePublicationTheme({ 77 uri: pub.uri, 78 theme: { 79 pageBackground: ColorToRGBA(localPubTheme.bgPage), 80 showPageBackground: showPageBackground, 81 backgroundColor: image 82 ? ColorToRGBA(localPubTheme.bgLeaflet) 83 : ColorToRGB(localPubTheme.bgLeaflet), 84 backgroundRepeat: image?.repeat, 85 backgroundImage: image ? image.file : null, 86 pageWidth: pageWidth, 87 primary: ColorToRGB(localPubTheme.primary), 88 accentBackground: ColorToRGB(localPubTheme.accent1), 89 accentText: ColorToRGB(localPubTheme.accent2), 90 headingFont: headingFont, 91 bodyFont: bodyFont, 92 }, 93 }); 94 95 if (!result.success) { 96 setLoading(false); 97 if (result.error && isOAuthSessionError(result.error)) { 98 toaster({ 99 content: <OAuthErrorMessage error={result.error} />, 100 type: "error", 101 }); 102 } else { 103 toaster({ 104 content: "Failed to update theme", 105 type: "error", 106 }); 107 } 108 return result; 109 } 110 111 mutate((pub) => { 112 if (result.publication && pub?.publication) 113 return { 114 ...pub, 115 publication: { ...pub.publication, ...result.publication }, 116 }; 117 return pub; 118 }, false); 119 resetChanges(); 120 setLoading(false); 121 return result; 122 }; 123 124 return { 125 openPicker, 126 setOpenPicker, 127 pub, 128 record, 129 mutate, 130 showPageBackground, 131 setShowPageBackground, 132 localPubTheme, 133 setTheme, 134 changes, 135 resetChanges, 136 image, 137 setImage, 138 pageWidth, 139 setPageWidth, 140 headingFont, 141 setHeadingFont, 142 bodyFont, 143 setBodyFont, 144 pubBGImage, 145 leafletBGRepeat, 146 toaster, 147 submitTheme, 148 }; 149} 150 151export type PubThemeEditorState = ReturnType<typeof usePubThemeEditorState>; 152 153export function PubThemePickerPanel(props: { state: PubThemeEditorState }) { 154 let { 155 openPicker, 156 setOpenPicker, 157 showPageBackground, 158 setShowPageBackground, 159 localPubTheme, 160 setTheme, 161 image, 162 setImage, 163 pageWidth, 164 setPageWidth, 165 headingFont, 166 setHeadingFont, 167 bodyFont, 168 setBodyFont, 169 pubBGImage, 170 leafletBGRepeat, 171 } = props.state; 172 173 return ( 174 <div className="themeSetterContent flex flex-col w-full"> 175 <div className="themeBGLeaflet flex flex-col"> 176 <div 177 className={`themeBgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `} 178 > 179 <PresetThemePicker state={props.state} /> 180 <div className="bgPickerBody w-full flex flex-col gap-2 p-2 border border-[#CCCCCC] rounded-md text-[#595959] bg-white"> 181 <PubPageWidthSetter 182 pageWidth={pageWidth} 183 setPageWidth={setPageWidth} 184 thisPicker="page-width" 185 openPicker={openPicker} 186 setOpenPicker={setOpenPicker} 187 /> 188 <hr className="border-[#CCCCCC] my-0.5" /> 189 <BackgroundPicker 190 bgImage={image} 191 setBgImage={setImage} 192 backgroundColor={localPubTheme.bgLeaflet} 193 pageBackground={localPubTheme.bgPage} 194 setPageBackground={(color) => { 195 setTheme((t) => ({ ...t, bgPage: color })); 196 }} 197 setBackgroundColor={(color) => { 198 setTheme((t) => ({ ...t, bgLeaflet: color })); 199 }} 200 openPicker={openPicker} 201 setOpenPicker={setOpenPicker} 202 hasPageBackground={!!showPageBackground} 203 setHasPageBackground={setShowPageBackground} 204 /> 205 </div> 206 207 <SectionArrow 208 fill="white" 209 stroke="#CCCCCC" 210 className="ml-2 -mt-[1px]" 211 /> 212 </div> 213 </div> 214 215 <div 216 style={{ 217 backgroundImage: pubBGImage ? `url(${pubBGImage})` : undefined, 218 backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 219 backgroundPosition: "center", 220 backgroundSize: !leafletBGRepeat 221 ? "cover" 222 : `calc(${leafletBGRepeat}px / 2 )`, 223 }} 224 className={` relative bg-bg-leaflet px-3 py-4 flex flex-col rounded-md border border-border `} 225 > 226 <div className={`flex flex-col gap-3 z-10`}> 227 <PagePickers 228 pageBackground={localPubTheme.bgPage} 229 primary={localPubTheme.primary} 230 setPageBackground={(color) => { 231 setTheme((t) => ({ ...t, bgPage: color })); 232 }} 233 setPrimary={(color) => { 234 setTheme((t) => ({ ...t, primary: color })); 235 }} 236 openPicker={openPicker} 237 setOpenPicker={(pickers) => setOpenPicker(pickers)} 238 hasPageBackground={showPageBackground} 239 /> 240 <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"> 241 <FontPicker 242 label="Heading" 243 value={headingFont} 244 onChange={setHeadingFont} 245 /> 246 <FontPicker label="Body" value={bodyFont} onChange={setBodyFont} /> 247 </div> 248 <PubAccentPickers 249 accent1={localPubTheme.accent1} 250 setAccent1={(color) => { 251 setTheme((t) => ({ ...t, accent1: color })); 252 }} 253 accent2={localPubTheme.accent2} 254 setAccent2={(color) => { 255 setTheme((t) => ({ ...t, accent2: color })); 256 }} 257 openPicker={openPicker} 258 setOpenPicker={(pickers) => setOpenPicker(pickers)} 259 /> 260 </div> 261 </div> 262 </div> 263 ); 264} 265 266export const PubThemeSetter = (props: { 267 backToMenu: () => void; 268 loading: boolean; 269 setLoading: (l: boolean) => void; 270}) => { 271 let [sample, setSample] = useState<"pub" | "post">("pub"); 272 let state = usePubThemeEditorState(); 273 let { 274 localPubTheme, 275 headingFont, 276 bodyFont, 277 image, 278 pageWidth, 279 pubBGImage, 280 leafletBGRepeat, 281 pub, 282 record, 283 showPageBackground, 284 submitTheme, 285 } = state; 286 287 return ( 288 <CardBorderHiddenContext.Provider value={!showPageBackground}> 289 <BaseThemeProvider 290 local 291 {...localPubTheme} 292 headingFontId={headingFont} 293 bodyFontId={bodyFont} 294 hasBackgroundImage={!!image} 295 className="min-h-0!" 296 > 297 <div className="min-h-0 flex-1 flex flex-col pb-0.5"> 298 <div className="flex-shrink-0"> 299 <button type="button" onClick={props.backToMenu}> 300 <GoToArrow /> 301 </button> 302 </div> 303 304 <div className="themeSetterContent flex flex-col w-full overflow-y-scroll min-h-0 -mb-2 pt-2 "> 305 <PubThemePickerPanel state={state} /> 306 <div className="flex flex-col mt-4 "> 307 <div className="flex gap-2 items-center text-sm text-[#8C8C8C]"> 308 <div className="text-sm">Preview</div> 309 <Separator classname="h-4!" />{" "} 310 <button 311 type="button" 312 className={`${sample === "pub" ? "font-bold text-[#595959]" : ""}`} 313 onClick={() => setSample("pub")} 314 > 315 Pub 316 </button> 317 <button 318 type="button" 319 className={`${sample === "post" ? "font-bold text-[#595959]" : ""}`} 320 onClick={() => setSample("post")} 321 > 322 Post 323 </button> 324 </div> 325 </div> 326 <div className="pt-2"> 327 <ButtonPrimary 328 fullWidth 329 disabled={props.loading} 330 onClick={async () => { 331 await submitTheme(props.setLoading); 332 }} 333 > 334 {props.loading ? "Saving..." : "Save Theme"} 335 </ButtonPrimary> 336 </div> 337 </div> 338 </div> 339 </BaseThemeProvider> 340 </CardBorderHiddenContext.Provider> 341 ); 342};