a tool for shared writing and social publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

move theme setter to a new page, adjusted post and pub pages so that i can extract them and use dummy data to fill them in

celine (Apr 3, 2026, 2:56 AM EDT) 94c367d4 5baab1bb

+965 -368
+32
components/ToggleGroup.tsx
··· 1 + import { type ReactNode } from "react"; 2 + 3 + export function ToggleGroup<T extends string>(props: { 4 + value: T; 5 + onChange: (value: T) => void; 6 + options: { value: T; label: ReactNode }[]; 7 + className?: string; 8 + optionClassName?: string; 9 + }) { 10 + return ( 11 + <div 12 + className={`flex gap-1 p-1 bg-accent-contrast rounded-lg text-sm ${props.className || ""}`} 13 + > 14 + {props.options.map((option) => ( 15 + <button 16 + key={option.value} 17 + type="button" 18 + className={`px-1 rounded-md ${ 19 + props.value === option.value 20 + ? "bg-accent-2 font-bold text-accent-contrast" 21 + : "bg-transparent text-bg-page" 22 + } 23 + ${props.optionClassName} 24 + `} 25 + onClick={() => props.onChange(option.value)} 26 + > 27 + {option.label} 28 + </button> 29 + ))} 30 + </div> 31 + ); 32 + }
+214 -155
components/ThemeManager/PubThemeSetter.tsx
··· 29 29 file?: File; 30 30 repeat: number | null; 31 31 }; 32 - export const PubThemeSetter = (props: { 33 - backToMenu: () => void; 34 - loading: boolean; 35 - setLoading: (l: boolean) => void; 36 - }) => { 37 - let [sample, setSample] = useState<"pub" | "post">("pub"); 32 + 33 + export function usePubThemeEditorState() { 38 34 let [openPicker, setOpenPicker] = useState<pickers>("null"); 39 35 let { data, mutate } = usePublicationData(); 40 36 let { publication: pub } = data || {}; ··· 73 69 let leafletBGRepeat = image?.repeat || null; 74 70 let toaster = useToaster(); 75 71 72 + let submitTheme = async (setLoading: (l: boolean) => void) => { 73 + if (!pub) return; 74 + setLoading(true); 75 + let result = await updatePublicationTheme({ 76 + uri: pub.uri, 77 + theme: { 78 + pageBackground: ColorToRGBA(localPubTheme.bgPage), 79 + showPageBackground: showPageBackground, 80 + backgroundColor: image 81 + ? ColorToRGBA(localPubTheme.bgLeaflet) 82 + : ColorToRGB(localPubTheme.bgLeaflet), 83 + backgroundRepeat: image?.repeat, 84 + backgroundImage: image ? image.file : null, 85 + pageWidth: pageWidth, 86 + primary: ColorToRGB(localPubTheme.primary), 87 + accentBackground: ColorToRGB(localPubTheme.accent1), 88 + accentText: ColorToRGB(localPubTheme.accent2), 89 + headingFont: headingFont, 90 + bodyFont: bodyFont, 91 + }, 92 + }); 93 + 94 + if (!result.success) { 95 + setLoading(false); 96 + if (result.error && isOAuthSessionError(result.error)) { 97 + toaster({ 98 + content: <OAuthErrorMessage error={result.error} />, 99 + type: "error", 100 + }); 101 + } else { 102 + toaster({ 103 + content: "Failed to update theme", 104 + type: "error", 105 + }); 106 + } 107 + return result; 108 + } 109 + 110 + mutate((pub) => { 111 + if (result.publication && pub?.publication) 112 + return { 113 + ...pub, 114 + publication: { ...pub.publication, ...result.publication }, 115 + }; 116 + return pub; 117 + }, false); 118 + setLoading(false); 119 + return result; 120 + }; 121 + 122 + return { 123 + openPicker, 124 + setOpenPicker, 125 + pub, 126 + record, 127 + mutate, 128 + showPageBackground, 129 + setShowPageBackground, 130 + localPubTheme, 131 + setTheme, 132 + changes, 133 + image, 134 + setImage, 135 + pageWidth, 136 + setPageWidth, 137 + headingFont, 138 + setHeadingFont, 139 + bodyFont, 140 + setBodyFont, 141 + pubBGImage, 142 + leafletBGRepeat, 143 + toaster, 144 + submitTheme, 145 + }; 146 + } 147 + 148 + export type PubThemeEditorState = ReturnType<typeof usePubThemeEditorState>; 149 + 150 + export function PubThemePickerPanel(props: { state: PubThemeEditorState }) { 151 + let { 152 + openPicker, 153 + setOpenPicker, 154 + showPageBackground, 155 + setShowPageBackground, 156 + localPubTheme, 157 + setTheme, 158 + image, 159 + setImage, 160 + pageWidth, 161 + setPageWidth, 162 + headingFont, 163 + setHeadingFont, 164 + bodyFont, 165 + setBodyFont, 166 + pubBGImage, 167 + leafletBGRepeat, 168 + } = props.state; 169 + 170 + return ( 171 + <div className="themeSetterContent flex flex-col w-full"> 172 + <PubPageWidthSetter 173 + pageWidth={pageWidth} 174 + setPageWidth={setPageWidth} 175 + thisPicker="page-width" 176 + openPicker={openPicker} 177 + setOpenPicker={setOpenPicker} 178 + /> 179 + <div className="themeBGLeaflet flex flex-col"> 180 + <div 181 + className={`themeBgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `} 182 + > 183 + <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md text-[#595959] bg-white"> 184 + <BackgroundPicker 185 + bgImage={image} 186 + setBgImage={setImage} 187 + backgroundColor={localPubTheme.bgLeaflet} 188 + pageBackground={localPubTheme.bgPage} 189 + setPageBackground={(color) => { 190 + setTheme((t) => ({ ...t, bgPage: color })); 191 + }} 192 + setBackgroundColor={(color) => { 193 + setTheme((t) => ({ ...t, bgLeaflet: color })); 194 + }} 195 + openPicker={openPicker} 196 + setOpenPicker={setOpenPicker} 197 + hasPageBackground={!!showPageBackground} 198 + setHasPageBackground={setShowPageBackground} 199 + /> 200 + </div> 201 + 202 + <SectionArrow 203 + fill="white" 204 + stroke="#CCCCCC" 205 + className="ml-2 -mt-[1px]" 206 + /> 207 + </div> 208 + </div> 209 + 210 + <div 211 + style={{ 212 + backgroundImage: pubBGImage ? `url(${pubBGImage})` : undefined, 213 + backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 214 + backgroundPosition: "center", 215 + backgroundSize: !leafletBGRepeat 216 + ? "cover" 217 + : `calc(${leafletBGRepeat}px / 2 )`, 218 + }} 219 + className={` relative bg-bg-leaflet px-3 py-4 flex flex-col rounded-md border border-border `} 220 + > 221 + <div className={`flex flex-col gap-3 z-10`}> 222 + <PagePickers 223 + pageBackground={localPubTheme.bgPage} 224 + primary={localPubTheme.primary} 225 + setPageBackground={(color) => { 226 + setTheme((t) => ({ ...t, bgPage: color })); 227 + }} 228 + setPrimary={(color) => { 229 + setTheme((t) => ({ ...t, primary: color })); 230 + }} 231 + openPicker={openPicker} 232 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 233 + hasPageBackground={showPageBackground} 234 + /> 235 + <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"> 236 + <FontPicker 237 + label="Heading" 238 + value={headingFont} 239 + onChange={setHeadingFont} 240 + /> 241 + <FontPicker label="Body" value={bodyFont} onChange={setBodyFont} /> 242 + </div> 243 + <PubAccentPickers 244 + accent1={localPubTheme.accent1} 245 + setAccent1={(color) => { 246 + setTheme((t) => ({ ...t, accent1: color })); 247 + }} 248 + accent2={localPubTheme.accent2} 249 + setAccent2={(color) => { 250 + setTheme((t) => ({ ...t, accent2: color })); 251 + }} 252 + openPicker={openPicker} 253 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 254 + /> 255 + </div> 256 + </div> 257 + </div> 258 + ); 259 + } 260 + 261 + export const PubThemeSetter = (props: { 262 + backToMenu: () => void; 263 + loading: boolean; 264 + setLoading: (l: boolean) => void; 265 + }) => { 266 + let [sample, setSample] = useState<"pub" | "post">("pub"); 267 + let state = usePubThemeEditorState(); 268 + let { 269 + localPubTheme, 270 + headingFont, 271 + bodyFont, 272 + image, 273 + pageWidth, 274 + pubBGImage, 275 + leafletBGRepeat, 276 + pub, 277 + record, 278 + showPageBackground, 279 + submitTheme, 280 + } = state; 281 + 76 282 return ( 77 283 <BaseThemeProvider 78 284 local ··· 87 293 className="flex-shrink-0" 88 294 onSubmit={async (e) => { 89 295 e.preventDefault(); 90 - if (!pub) return; 91 - props.setLoading(true); 92 - let result = await updatePublicationTheme({ 93 - uri: pub.uri, 94 - theme: { 95 - pageBackground: ColorToRGBA(localPubTheme.bgPage), 96 - showPageBackground: showPageBackground, 97 - backgroundColor: image 98 - ? ColorToRGBA(localPubTheme.bgLeaflet) 99 - : ColorToRGB(localPubTheme.bgLeaflet), 100 - backgroundRepeat: image?.repeat, 101 - backgroundImage: image ? image.file : null, 102 - pageWidth: pageWidth, 103 - primary: ColorToRGB(localPubTheme.primary), 104 - accentBackground: ColorToRGB(localPubTheme.accent1), 105 - accentText: ColorToRGB(localPubTheme.accent2), 106 - headingFont: headingFont, 107 - bodyFont: bodyFont, 108 - }, 109 - }); 110 - 111 - if (!result.success) { 112 - props.setLoading(false); 113 - if (result.error && isOAuthSessionError(result.error)) { 114 - toaster({ 115 - content: <OAuthErrorMessage error={result.error} />, 116 - type: "error", 117 - }); 118 - } else { 119 - toaster({ 120 - content: "Failed to update theme", 121 - type: "error", 122 - }); 123 - } 124 - return; 125 - } 126 - 127 - mutate((pub) => { 128 - if (result.publication && pub?.publication) 129 - return { 130 - ...pub, 131 - publication: { ...pub.publication, ...result.publication }, 132 - }; 133 - return pub; 134 - }, false); 135 - props.setLoading(false); 296 + await submitTheme(props.setLoading); 136 297 }} 137 298 > 138 299 <button onClick={props.backToMenu}> 139 300 <GoToArrow /> 140 301 </button> 141 - <ButtonPrimary onClick={props.setLoading}>update</ButtonPrimary> 142 302 </form> 143 303 144 304 <div className="themeSetterContent flex flex-col w-full overflow-y-scroll min-h-0 -mb-2 pt-2 "> 145 - <PubPageWidthSetter 146 - pageWidth={pageWidth} 147 - setPageWidth={setPageWidth} 148 - thisPicker="page-width" 149 - openPicker={openPicker} 150 - setOpenPicker={setOpenPicker} 151 - /> 152 - <div className="themeBGLeaflet flex flex-col"> 153 - <div 154 - className={`themeBgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `} 155 - > 156 - <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md text-[#595959] bg-white"> 157 - <BackgroundPicker 158 - bgImage={image} 159 - setBgImage={setImage} 160 - backgroundColor={localPubTheme.bgLeaflet} 161 - pageBackground={localPubTheme.bgPage} 162 - setPageBackground={(color) => { 163 - setTheme((t) => ({ ...t, bgPage: color })); 164 - }} 165 - setBackgroundColor={(color) => { 166 - setTheme((t) => ({ ...t, bgLeaflet: color })); 167 - }} 168 - openPicker={openPicker} 169 - setOpenPicker={setOpenPicker} 170 - hasPageBackground={!!showPageBackground} 171 - setHasPageBackground={setShowPageBackground} 172 - /> 173 - </div> 174 - 175 - <SectionArrow 176 - fill="white" 177 - stroke="#CCCCCC" 178 - className="ml-2 -mt-[1px]" 179 - /> 180 - </div> 181 - </div> 182 - 183 - <div 184 - style={{ 185 - backgroundImage: pubBGImage ? `url(${pubBGImage})` : undefined, 186 - backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 187 - backgroundPosition: "center", 188 - backgroundSize: !leafletBGRepeat 189 - ? "cover" 190 - : `calc(${leafletBGRepeat}px / 2 )`, 191 - }} 192 - className={` relative bg-bg-leaflet px-3 py-4 flex flex-col rounded-md border border-border `} 193 - > 194 - <div className={`flex flex-col gap-3 z-10`}> 195 - <PagePickers 196 - pageBackground={localPubTheme.bgPage} 197 - primary={localPubTheme.primary} 198 - setPageBackground={(color) => { 199 - setTheme((t) => ({ ...t, bgPage: color })); 200 - }} 201 - setPrimary={(color) => { 202 - setTheme((t) => ({ ...t, primary: color })); 203 - }} 204 - openPicker={openPicker} 205 - setOpenPicker={(pickers) => setOpenPicker(pickers)} 206 - hasPageBackground={showPageBackground} 207 - /> 208 - <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"> 209 - <FontPicker 210 - label="Heading" 211 - value={headingFont} 212 - onChange={setHeadingFont} 213 - /> 214 - <FontPicker 215 - label="Body" 216 - value={bodyFont} 217 - onChange={setBodyFont} 218 - /> 219 - </div> 220 - <PubAccentPickers 221 - accent1={localPubTheme.accent1} 222 - setAccent1={(color) => { 223 - setTheme((t) => ({ ...t, accent1: color })); 224 - }} 225 - accent2={localPubTheme.accent2} 226 - setAccent2={(color) => { 227 - setTheme((t) => ({ ...t, accent2: color })); 228 - }} 229 - openPicker={openPicker} 230 - setOpenPicker={(pickers) => setOpenPicker(pickers)} 231 - /> 232 - </div> 233 - </div> 305 + <PubThemePickerPanel state={state} /> 234 306 <div className="flex flex-col mt-4 "> 235 307 <div className="flex gap-2 items-center text-sm text-[#8C8C8C]"> 236 308 <div className="text-sm">Preview</div> ··· 248 320 Post 249 321 </button> 250 322 </div> 251 - {sample === "pub" ? ( 252 - <SamplePub 253 - pubBGImage={pubBGImage} 254 - pubBGRepeat={leafletBGRepeat} 255 - showPageBackground={showPageBackground} 256 - /> 257 - ) : ( 258 - <SamplePost 259 - pubBGImage={pubBGImage} 260 - pubBGRepeat={leafletBGRepeat} 261 - showPageBackground={showPageBackground} 262 - /> 263 - )} 264 323 </div> 265 324 </div> 266 325 </div>
+185
app/lish/[did]/[publication]/PublicationContent.tsx
··· 1 + import React from "react"; 2 + import { AtUri } from "@atproto/syntax"; 3 + import { 4 + getPublicationURL, 5 + getDocumentURL, 6 + } from "app/lish/createPub/getPublicationURL"; 7 + import { SubscribeWithBluesky } from "app/lish/Subscribe"; 8 + import { 9 + PublicationBackgroundProvider, 10 + PublicationThemeProvider, 11 + } from "components/ThemeManager/PublicationThemeProvider"; 12 + import { InteractionPreview } from "components/InteractionsPreview"; 13 + import { LocalizedDate } from "./LocalizedDate"; 14 + import { PublicationHomeLayout } from "./PublicationHomeLayout"; 15 + import { PublicationAuthor } from "./PublicationAuthor"; 16 + import { 17 + normalizePublicationRecord, 18 + normalizeDocumentRecord, 19 + } from "src/utils/normalizeRecords"; 20 + import { getFirstParagraph } from "src/utils/getFirstParagraph"; 21 + import { FontLoader } from "components/FontLoader"; 22 + import { 23 + PublicationHeader, 24 + PublicationPostItem, 25 + } from "./PublicationPageContent"; 26 + 27 + export const PublicationContent = ({ 28 + record, 29 + publication, 30 + did, 31 + profile, 32 + showPageBackground, 33 + }: { 34 + record: ReturnType<typeof normalizePublicationRecord>; 35 + publication: { 36 + uri: string; 37 + name: string; 38 + identity_did: string; 39 + record: unknown; 40 + publication_subscriptions: { identity: string }[]; 41 + documents_in_publications: { 42 + documents: { 43 + uri: string; 44 + data: unknown; 45 + comments_on_documents: { count: number }[]; 46 + document_mentions_in_bsky: { count: number }[]; 47 + recommends_on_documents: { count: number }[]; 48 + } | null; 49 + }[]; 50 + }; 51 + did: string; 52 + profile: { did: string; displayName?: string; handle: string } | undefined; 53 + showPageBackground: boolean | undefined; 54 + }) => { 55 + return ( 56 + <> 57 + <FontLoader 58 + headingFontId={record?.theme?.headingFont} 59 + bodyFontId={record?.theme?.bodyFont} 60 + /> 61 + <PublicationThemeProvider 62 + theme={record?.theme} 63 + pub_creator={publication.identity_did} 64 + > 65 + <PublicationBackgroundProvider 66 + theme={record?.theme} 67 + pub_creator={publication.identity_did} 68 + > 69 + <PublicationHomeLayout 70 + uri={publication.uri} 71 + showPageBackground={!!showPageBackground} 72 + > 73 + <PublicationHeader 74 + iconUrl={ 75 + record?.icon 76 + ? `/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]}` 77 + : undefined 78 + } 79 + publicationName={publication.name} 80 + description={record?.description} 81 + author={ 82 + profile ? ( 83 + <PublicationAuthor 84 + did={profile.did} 85 + displayName={profile.displayName} 86 + handle={profile.handle} 87 + /> 88 + ) : undefined 89 + } 90 + subscribeButton={ 91 + <SubscribeWithBluesky 92 + base_url={getPublicationURL(publication)} 93 + pubName={publication.name} 94 + pub_uri={publication.uri} 95 + subscribers={publication.publication_subscriptions} 96 + /> 97 + } 98 + /> 99 + <div className="publicationPostList w-full flex flex-col gap-4"> 100 + {publication.documents_in_publications 101 + .filter((d) => !!d?.documents) 102 + .sort((a, b) => { 103 + const aRecord = normalizeDocumentRecord(a.documents?.data); 104 + const bRecord = normalizeDocumentRecord(b.documents?.data); 105 + const aDate = aRecord?.publishedAt 106 + ? new Date(aRecord.publishedAt) 107 + : new Date(0); 108 + const bDate = bRecord?.publishedAt 109 + ? new Date(bRecord.publishedAt) 110 + : new Date(0); 111 + return bDate.getTime() - aDate.getTime(); // Sort by most recent first 112 + }) 113 + .map((doc) => { 114 + if (!doc.documents) return null; 115 + const doc_record = normalizeDocumentRecord( 116 + doc.documents.data, 117 + ); 118 + if (!doc_record) return null; 119 + let uri = new AtUri(doc.documents.uri); 120 + let quotes = 121 + doc.documents.document_mentions_in_bsky[0].count || 0; 122 + let comments = 123 + record?.preferences?.showComments === false 124 + ? 0 125 + : doc.documents.comments_on_documents[0].count || 0; 126 + let recommends = 127 + doc.documents.recommends_on_documents?.[0]?.count || 0; 128 + let tags = doc_record.tags || []; 129 + 130 + const docUrl = getDocumentURL( 131 + doc_record, 132 + doc.documents.uri, 133 + publication, 134 + ); 135 + return ( 136 + <React.Fragment key={doc.documents?.uri}> 137 + <PublicationPostItem 138 + href={docUrl} 139 + title={doc_record.title} 140 + description={ 141 + doc_record.description || 142 + getFirstParagraph(doc_record) 143 + } 144 + date={ 145 + doc_record.publishedAt ? ( 146 + <LocalizedDate 147 + dateString={doc_record.publishedAt} 148 + options={{ 149 + year: "numeric", 150 + month: "long", 151 + day: "2-digit", 152 + }} 153 + /> 154 + ) : undefined 155 + } 156 + interactions={ 157 + <InteractionPreview 158 + quotesCount={quotes} 159 + commentsCount={comments} 160 + recommendsCount={recommends} 161 + documentUri={doc.documents.uri} 162 + tags={tags} 163 + postUrl={docUrl} 164 + showComments={ 165 + record?.preferences?.showComments !== false 166 + } 167 + showMentions={ 168 + record?.preferences?.showMentions !== false 169 + } 170 + showRecommends={ 171 + record?.preferences?.showRecommends !== false 172 + } 173 + /> 174 + } 175 + /> 176 + </React.Fragment> 177 + ); 178 + })} 179 + </div> 180 + </PublicationHomeLayout> 181 + </PublicationBackgroundProvider> 182 + </PublicationThemeProvider> 183 + </> 184 + ); 185 + };
+1 -1
app/lish/[did]/[publication]/PublicationHomeLayout.tsx
··· 11 11 return ( 12 12 <div 13 13 ref={props.showPageBackground ? null : ref} 14 - className={`pubWrapper flex flex-col sm:py-6 h-full ${props.showPageBackground ? "max-w-(--page-width-units) mx-auto sm:px-0 px-[6px] py-2" : "w-full overflow-y-scroll"}`} 14 + className={`pubWrapper flex flex-col sm:py-6 min-h-full ${props.showPageBackground ? "max-w-(--page-width-units) mx-auto sm:px-0 px-[6px] py-2" : "w-full overflow-y-scroll"}`} 15 15 > 16 16 <div 17 17 ref={!props.showPageBackground ? null : ref}
+78
app/lish/[did]/[publication]/PublicationPageContent.tsx
··· 1 + import React from "react"; 2 + import { SpeedyLink } from "components/SpeedyLink"; 3 + 4 + export function PublicationHeader(props: { 5 + iconUrl?: string; 6 + publicationName: string; 7 + description?: string; 8 + author?: React.ReactNode; 9 + subscribeButton?: React.ReactNode; 10 + }) { 11 + return ( 12 + <div className="pubHeader flex flex-col pb-8 w-full text-center justify-center "> 13 + {props.iconUrl && ( 14 + <div 15 + className="shrink-0 w-10 h-10 rounded-full mx-auto" 16 + style={{ 17 + backgroundImage: `url(${props.iconUrl})`, 18 + backgroundRepeat: "no-repeat", 19 + backgroundPosition: "center", 20 + backgroundSize: "cover", 21 + }} 22 + /> 23 + )} 24 + <h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 "> 25 + {props.publicationName} 26 + </h2> 27 + <p className="sm:text-lg text-secondary"> 28 + {props.description}{" "} 29 + </p> 30 + {props.author} 31 + <div className="sm:pt-4 pt-4">{props.subscribeButton}</div> 32 + </div> 33 + ); 34 + } 35 + 36 + export function PublicationPostItem(props: { 37 + href?: string; 38 + title?: string; 39 + description?: string; 40 + date?: React.ReactNode; 41 + interactions?: React.ReactNode; 42 + }) { 43 + const content = ( 44 + <> 45 + {props.title && <h3 className="text-primary">{props.title}</h3>} 46 + <p className="italic text-secondary line-clamp-3"> 47 + {props.description} 48 + </p> 49 + </> 50 + ); 51 + 52 + return ( 53 + <> 54 + <div className="flex w-full grow flex-col "> 55 + {props.href ? ( 56 + <SpeedyLink 57 + href={props.href} 58 + className="publishedPost no-underline! flex flex-col" 59 + > 60 + {content} 61 + </SpeedyLink> 62 + ) : ( 63 + <div className="publishedPost no-underline! flex flex-col"> 64 + {content} 65 + </div> 66 + )} 67 + 68 + <div className="justify-between w-full text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center"> 69 + <p className="text-sm text-tertiary "> 70 + {props.date}{" "} 71 + </p> 72 + {props.interactions} 73 + </div> 74 + </div> 75 + <hr className="last:hidden border-border-light" /> 76 + </> 77 + ); 78 + }
+10 -14
app/lish/[did]/[publication]/UpgradeModal.tsx
··· 3 3 import { useState } from "react"; 4 4 import { createCheckoutSession } from "actions/createCheckoutSession"; 5 5 import { DotLoader } from "components/utils/DotLoader"; 6 + import { ToggleGroup } from "components/ToggleGroup"; 6 7 7 8 export const UpgradeContent = () => { 8 9 let [cadence, setCadence] = useState<"year" | "month">("year"); ··· 37 38 </div> 38 39 <div className="sm:w-64 w-full accent-container flex justify-center items-center"> 39 40 <div className="flex flex-col justify-center text-center py-6"> 40 - <div className="flex gap-2 mb-3 p-1 bg-accent-contrast rounded-lg text-sm"> 41 - <button 42 - className={`px-1 rounded-md ${cadence === "year" ? "bg-bg-page font-bold text-accent-contrast" : "bg-transparent text-bg-page"}`} 43 - onClick={() => setCadence("year")} 44 - > 45 - Yearly 46 - </button> 47 - <button 48 - className={`px-1 rounded-md ${cadence === "month" ? "bg-bg-page font-bold text-accent-contrast" : "bg-transparent text-bg-page"}`} 49 - onClick={() => setCadence("month")} 50 - > 51 - Monthly 52 - </button> 53 - </div> 41 + <ToggleGroup 42 + className="mb-3" 43 + value={cadence} 44 + onChange={setCadence} 45 + options={[ 46 + { value: "year", label: "Yearly" }, 47 + { value: "month", label: "Monthly" }, 48 + ]} 49 + /> 54 50 <div className="flex gap-1 items-baseline justify-center"> 55 51 <div className="text-2xl font-bold leading-tight"> 56 52 {cadence === "year" ? "$120" : "$12"}
+14 -168
app/lish/[did]/[publication]/page.tsx
··· 1 1 import { supabaseServerClient } from "supabase/serverClient"; 2 - import { AtUri } from "@atproto/syntax"; 3 2 import { 4 3 getPublicationURL, 5 4 getDocumentURL, 6 5 } from "app/lish/createPub/getPublicationURL"; 7 6 import { BskyAgent } from "@atproto/api"; 8 7 import { publicationNameOrUriFilter } from "src/utils/uriHelpers"; 9 - import { SubscribeWithBluesky } from "app/lish/Subscribe"; 10 8 import React from "react"; 11 - import { 12 - PublicationBackgroundProvider, 13 - PublicationThemeProvider, 14 - } from "components/ThemeManager/PublicationThemeProvider"; 15 9 import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 16 - import { SpeedyLink } from "components/SpeedyLink"; 17 - import { InteractionPreview } from "components/InteractionsPreview"; 18 - import { LocalizedDate } from "./LocalizedDate"; 19 - import { PublicationHomeLayout } from "./PublicationHomeLayout"; 20 - import { PublicationAuthor } from "./PublicationAuthor"; 21 - import { Separator } from "components/Layout"; 22 - import { 23 - normalizePublicationRecord, 24 - normalizeDocumentRecord, 25 - } from "src/utils/normalizeRecords"; 26 - import { getFirstParagraph } from "src/utils/getFirstParagraph"; 27 - import { FontLoader } from "components/FontLoader"; 10 + import { normalizePublicationRecord } from "src/utils/normalizeRecords"; 11 + import { PublicationContent } from "./PublicationContent"; 28 12 29 13 export default async function Publication(props: { 30 14 params: Promise<{ publication: string; did: string }>; 31 15 }) { 32 16 let params = await props.params; 33 - let did = decodeURIComponent(params.did); 17 + const did = decodeURIComponent(params.did); 34 18 if (!did) return <PubNotFound />; 35 - let agent = new BskyAgent({ service: "https://public.api.bsky.app" }); 36 - let publication_name = decodeURIComponent(params.publication); 37 - let [{ data: publications }, { data: profile }] = await Promise.all([ 19 + const agent = new BskyAgent({ service: "https://public.api.bsky.app" }); 20 + const publication_name = decodeURIComponent(params.publication); 21 + const [{ data: publications }, { data: profile }] = await Promise.all([ 38 22 supabaseServerClient 39 23 .from("publications") 40 24 .select( ··· 54 38 .limit(1), 55 39 agent.getProfile({ actor: did }), 56 40 ]); 57 - let publication = publications?.[0]; 41 + const publication = publications?.[0]; 58 42 59 43 const record = normalizePublicationRecord(publication?.record); 60 44 61 - let showPageBackground = record?.theme?.showPageBackground; 45 + const showPageBackground = record?.theme?.showPageBackground; 62 46 63 47 if (!publication) return <PubNotFound />; 64 48 try { 65 49 return ( 66 50 <> 67 - <FontLoader 68 - headingFontId={record?.theme?.headingFont} 69 - bodyFontId={record?.theme?.bodyFont} 51 + <PublicationContent 52 + record={record} 53 + publication={publication} 54 + did={did} 55 + profile={profile} 56 + showPageBackground={showPageBackground} 70 57 /> 71 - <PublicationThemeProvider 72 - theme={record?.theme} 73 - pub_creator={publication.identity_did} 74 - > 75 - <PublicationBackgroundProvider 76 - theme={record?.theme} 77 - pub_creator={publication.identity_did} 78 - > 79 - <PublicationHomeLayout 80 - uri={publication.uri} 81 - showPageBackground={!!showPageBackground} 82 - > 83 - <div className="pubHeader flex flex-col pb-8 w-full text-center justify-center "> 84 - {record?.icon && ( 85 - <div 86 - className="shrink-0 w-10 h-10 rounded-full mx-auto" 87 - style={{ 88 - backgroundImage: `url(/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`, 89 - backgroundRepeat: "no-repeat", 90 - backgroundPosition: "center", 91 - backgroundSize: "cover", 92 - }} 93 - /> 94 - )} 95 - <h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 "> 96 - {publication.name} 97 - </h2> 98 - <p className="sm:text-lg text-secondary"> 99 - {record?.description}{" "} 100 - </p> 101 - {profile && ( 102 - <PublicationAuthor 103 - did={profile.did} 104 - displayName={profile.displayName} 105 - handle={profile.handle} 106 - /> 107 - )} 108 - <div className="sm:pt-4 pt-4"> 109 - <SubscribeWithBluesky 110 - base_url={getPublicationURL(publication)} 111 - pubName={publication.name} 112 - pub_uri={publication.uri} 113 - subscribers={publication.publication_subscriptions} 114 - /> 115 - </div> 116 - </div> 117 - <div className="publicationPostList w-full flex flex-col gap-4"> 118 - {publication.documents_in_publications 119 - .filter((d) => !!d?.documents) 120 - .sort((a, b) => { 121 - const aRecord = normalizeDocumentRecord(a.documents?.data); 122 - const bRecord = normalizeDocumentRecord(b.documents?.data); 123 - const aDate = aRecord?.publishedAt 124 - ? new Date(aRecord.publishedAt) 125 - : new Date(0); 126 - const bDate = bRecord?.publishedAt 127 - ? new Date(bRecord.publishedAt) 128 - : new Date(0); 129 - return bDate.getTime() - aDate.getTime(); // Sort by most recent first 130 - }) 131 - .map((doc) => { 132 - if (!doc.documents) return null; 133 - const doc_record = normalizeDocumentRecord( 134 - doc.documents.data, 135 - ); 136 - if (!doc_record) return null; 137 - let uri = new AtUri(doc.documents.uri); 138 - let quotes = 139 - doc.documents.document_mentions_in_bsky[0].count || 0; 140 - let comments = 141 - record?.preferences?.showComments === false 142 - ? 0 143 - : doc.documents.comments_on_documents[0].count || 0; 144 - let recommends = 145 - doc.documents.recommends_on_documents?.[0]?.count || 0; 146 - let tags = doc_record.tags || []; 147 - 148 - const docUrl = getDocumentURL( 149 - doc_record, 150 - doc.documents.uri, 151 - publication, 152 - ); 153 - return ( 154 - <React.Fragment key={doc.documents?.uri}> 155 - <div className="flex w-full grow flex-col "> 156 - <SpeedyLink 157 - href={docUrl} 158 - className="publishedPost no-underline! flex flex-col" 159 - > 160 - {doc_record.title && ( 161 - <h3 className="text-primary"> 162 - {doc_record.title} 163 - </h3> 164 - )} 165 - <p className="italic text-secondary line-clamp-3"> 166 - {doc_record.description || 167 - getFirstParagraph(doc_record)} 168 - </p> 169 - </SpeedyLink> 170 - 171 - <div className="justify-between w-full text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center"> 172 - <p className="text-sm text-tertiary "> 173 - {doc_record.publishedAt && ( 174 - <LocalizedDate 175 - dateString={doc_record.publishedAt} 176 - options={{ 177 - year: "numeric", 178 - month: "long", 179 - day: "2-digit", 180 - }} 181 - /> 182 - )}{" "} 183 - </p> 184 - 185 - <InteractionPreview 186 - quotesCount={quotes} 187 - commentsCount={comments} 188 - recommendsCount={recommends} 189 - documentUri={doc.documents.uri} 190 - tags={tags} 191 - postUrl={docUrl} 192 - showComments={ 193 - record?.preferences?.showComments !== false 194 - } 195 - showMentions={ 196 - record?.preferences?.showMentions !== false 197 - } 198 - showRecommends={ 199 - record?.preferences?.showRecommends !== false 200 - } 201 - /> 202 - </div> 203 - </div> 204 - <hr className="last:hidden border-border-light" /> 205 - </React.Fragment> 206 - ); 207 - })} 208 - </div> 209 - </PublicationHomeLayout> 210 - </PublicationBackgroundProvider> 211 - </PublicationThemeProvider> 212 58 </> 213 59 ); 214 60 } catch (e) {
+4 -3
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 41 41 did, 42 42 profile, 43 43 preferences, 44 - pubRecord, 45 - theme, 46 44 prerenderedCodeBlocks, 47 45 bskyPostData, 48 46 pollData, ··· 73 71 pageOptions={pageOptions} 74 72 footnoteSideColumn={ 75 73 !props.hasContentToRight ? ( 76 - <PublishedFootnoteSideColumn footnotes={footnotes} fullPageScroll={fullPageScroll} /> 74 + <PublishedFootnoteSideColumn 75 + footnotes={footnotes} 76 + fullPageScroll={fullPageScroll} 77 + /> 77 78 ) : undefined 78 79 } 79 80 >
+162
app/lish/[did]/[publication]/theme-settings/PostPreview.tsx
··· 1 + "use client"; 2 + 3 + import { LinearDocumentPage } from "app/lish/[did]/[publication]/[rkey]/LinearDocumentPage"; 4 + import { LeafletContentProvider } from "contexts/LeafletContentContext"; 5 + import { DocumentProvider } from "contexts/DocumentContext"; 6 + import { useIdentityData } from "components/IdentityProvider"; 7 + import type { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 8 + import type { PubLeafletPagesLinearDocument } from "lexicons/api"; 9 + import type { PostPageData } from "app/lish/[did]/[publication]/[rkey]/getPostPageData"; 10 + import type { DocumentContextValue } from "contexts/DocumentContext"; 11 + import { 12 + usePublicationData, 13 + useNormalizedPublicationRecord, 14 + } from "../dashboard/PublicationSWRProvider"; 15 + 16 + const FAKE_DID = "did:plc:fake-preview-user"; 17 + const FAKE_DOC_URI = 18 + "at://did:plc:fake-preview-user/site.standard.document/preview"; 19 + 20 + const fakeBlocks: PubLeafletPagesLinearDocument.Block[] = [ 21 + { 22 + $type: "pub.leaflet.pages.linearDocument#block", 23 + block: { 24 + $type: "pub.leaflet.blocks.header", 25 + level: 2, 26 + plaintext: "A Heading", 27 + }, 28 + }, 29 + { 30 + $type: "pub.leaflet.pages.linearDocument#block", 31 + block: { 32 + $type: "pub.leaflet.blocks.text", 33 + plaintext: 34 + "This is a preview of what your posts will look like with the current theme settings. You can adjust colors, fonts, and layout options to match your style.", 35 + }, 36 + }, 37 + { 38 + $type: "pub.leaflet.pages.linearDocument#block", 39 + block: { 40 + $type: "pub.leaflet.blocks.text", 41 + plaintext: 42 + "Try changing the heading font, body font, accent color, and background to see how they affect the look and feel of your publication.", 43 + }, 44 + }, 45 + { 46 + $type: "pub.leaflet.pages.linearDocument#block", 47 + block: { 48 + $type: "pub.leaflet.blocks.horizontalRule", 49 + }, 50 + }, 51 + { 52 + $type: "pub.leaflet.pages.linearDocument#block", 53 + block: { 54 + $type: "pub.leaflet.blocks.text", 55 + plaintext: 56 + "Your readers will see posts rendered just like this. Make sure you're happy with the typography and spacing before publishing!", 57 + }, 58 + }, 59 + ]; 60 + 61 + const fakePage: PubLeafletPagesLinearDocument.Main = { 62 + $type: "pub.leaflet.pages.linearDocument", 63 + id: "preview-page", 64 + blocks: fakeBlocks, 65 + }; 66 + 67 + const fakeNormalizedDocument = { 68 + $type: "site.standard.document" as const, 69 + title: "Preview Post Title", 70 + description: "A short description of this preview post.", 71 + publishedAt: new Date().toISOString(), 72 + site: "at://did:plc:fake-preview-user/site.standard.publication/preview", 73 + tags: ["preview", "theme"], 74 + }; 75 + 76 + const fakeDocument: NonNullable<PostPageData> = { 77 + data: {}, 78 + uri: FAKE_DOC_URI, 79 + normalizedDocument: fakeNormalizedDocument, 80 + normalizedPublication: null, 81 + quotesAndMentions: [], 82 + theme: null, 83 + prevNext: undefined, 84 + publication: null, 85 + comments: [], 86 + comments_on_documents: [], 87 + mentions: [], 88 + document_mentions_in_bsky: [], 89 + leaflets_in_publications: [], 90 + leafletId: null, 91 + recommendsCount: 0, 92 + documents_in_publications: [], 93 + recommends_on_documents: [], 94 + } as unknown as NonNullable<PostPageData>; 95 + 96 + const fakeDocumentContextValue: DocumentContextValue = { 97 + uri: FAKE_DOC_URI, 98 + normalizedDocument: fakeNormalizedDocument, 99 + normalizedPublication: null, 100 + theme: undefined, 101 + prevNext: undefined, 102 + quotesAndMentions: [], 103 + publication: null, 104 + comments: [], 105 + mentions: [], 106 + leafletId: null, 107 + recommendsCount: 0, 108 + }; 109 + 110 + export function PostPreview(props: { 111 + showPageBackground: boolean; 112 + pageWidth: number; 113 + }) { 114 + let { identity } = useIdentityData(); 115 + let { data } = usePublicationData(); 116 + let { publication } = data || {}; 117 + let record = useNormalizedPublicationRecord(); 118 + let preferences = record?.preferences; 119 + let profileRecord = identity?.bsky_profiles 120 + ?.record as unknown as ProfileViewDetailed; 121 + 122 + let profile = profileRecord ?? { 123 + did: FAKE_DID, 124 + handle: "preview.bsky.social", 125 + displayName: "Preview Author", 126 + }; 127 + 128 + return ( 129 + <DocumentProvider value={fakeDocumentContextValue}> 130 + <LeafletContentProvider 131 + value={{ 132 + pages: [ 133 + fakePage as PubLeafletPagesLinearDocument.Main & { 134 + $type: string; 135 + }, 136 + ], 137 + }} 138 + > 139 + <div className="w-fit pointer-events-none"> 140 + <LinearDocumentPage 141 + document={fakeDocument} 142 + did={profile.did || FAKE_DID} 143 + profile={profile as ProfileViewDetailed} 144 + preferences={{ 145 + showComments: preferences?.showComments, 146 + showMentions: preferences?.showMentions, 147 + showRecommends: preferences?.showRecommends, 148 + showPrevNext: preferences?.showPrevNext, 149 + }} 150 + prerenderedCodeBlocks={new Map()} 151 + bskyPostData={[]} 152 + pollData={[]} 153 + document_uri={FAKE_DOC_URI} 154 + fullPageScroll={!props.showPageBackground} 155 + hasPageBackground={props.showPageBackground} 156 + blocks={fakeBlocks} 157 + /> 158 + </div> 159 + </LeafletContentProvider> 160 + </DocumentProvider> 161 + ); 162 + }
+44
app/lish/[did]/[publication]/theme-settings/PubPreview.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + usePublicationData, 5 + useNormalizedPublicationRecord, 6 + } from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider"; 7 + import { useIdentityData } from "components/IdentityProvider"; 8 + import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 9 + import { PublicationContent } from "../PublicationContent"; 10 + 11 + export function PubPreview(props: { 12 + showPageBackground: boolean; 13 + pageWidth: number; 14 + }) { 15 + let { data } = usePublicationData(); 16 + let { publication } = data || {}; 17 + let { identity } = useIdentityData(); 18 + let record = useNormalizedPublicationRecord(); 19 + 20 + let profileRecord = identity?.bsky_profiles 21 + ?.record as unknown as ProfileViewDetailed; 22 + 23 + let did = publication?.identity_did || ""; 24 + 25 + let profile = profileRecord 26 + ? { 27 + did: profileRecord.did, 28 + displayName: profileRecord.displayName, 29 + handle: profileRecord.handle, 30 + } 31 + : undefined; 32 + 33 + if (!publication) return null; 34 + 35 + return ( 36 + <PublicationContent 37 + record={record} 38 + publication={publication} 39 + did={did} 40 + profile={profile} 41 + showPageBackground={props.showPageBackground} 42 + /> 43 + ); 44 + }
+138
app/lish/[did]/[publication]/theme-settings/ThemeSettingsContent.tsx
··· 1 + "use client"; 2 + 3 + import { useState } from "react"; 4 + import { BaseThemeProvider } from "components/ThemeManager/ThemeProvider"; 5 + import { ButtonPrimary } from "components/Buttons"; 6 + import { DotLoader } from "components/utils/DotLoader"; 7 + import { ToggleGroup } from "components/ToggleGroup"; 8 + import { PaintSmall } from "components/Icons/PaintSmall"; 9 + import { Popover } from "components/Popover"; 10 + import { 11 + usePubThemeEditorState, 12 + PubThemePickerPanel, 13 + } from "components/ThemeManager/PubThemeSetter"; 14 + import { PubPreview } from "./PubPreview"; 15 + import { PostPreview } from "./PostPreview"; 16 + 17 + export function ThemeSettingsContent() { 18 + let [previewMode, setPreviewMode] = useState<"post" | "pub">("post"); 19 + let [loading, setLoading] = useState(false); 20 + 21 + let state = usePubThemeEditorState(); 22 + let { 23 + localPubTheme, 24 + headingFont, 25 + bodyFont, 26 + image, 27 + pageWidth, 28 + pubBGImage, 29 + leafletBGRepeat, 30 + showPageBackground, 31 + submitTheme, 32 + toaster, 33 + } = state; 34 + 35 + return ( 36 + <BaseThemeProvider 37 + local 38 + {...localPubTheme} 39 + headingFontId={headingFont} 40 + bodyFontId={bodyFont} 41 + hasBackgroundImage={!!image} 42 + pageWidth={pageWidth} 43 + > 44 + <div className="w-full h-full flex relative overflow-hidden"> 45 + {/* Theme Setter Panel */} 46 + <div className="absolute top-6 left-6 z-20"> 47 + <Popover 48 + align="start" 49 + side="bottom" 50 + className="sm:w-sm w-full rounded-lg! max-h-full overflow-y-auto !p-0 bg-white!" 51 + trigger={ 52 + <button 53 + type="button" 54 + className="w-12 h-12 rounded-full bg-accent-1 text-accent-2 data-[state=open]:bg-accent-2 data-[state=open]:text-accent-1 flex items-center justify-center transition-colors cursor-pointer" 55 + > 56 + <PaintSmall /> 57 + </button> 58 + } 59 + asChild 60 + > 61 + <form 62 + className="flex flex-col" 63 + onSubmit={async (e) => { 64 + e.preventDefault(); 65 + let result = await submitTheme(setLoading); 66 + if (result?.success) { 67 + toaster({ content: "Theme updated!", type: "success" }); 68 + } 69 + }} 70 + > 71 + {/* Toggle + Save Header */} 72 + <div className="flex items-center justify-between px-3 pt-3 pb-2"> 73 + <div className="font-bold text-[#272727]">Preview Mode</div> 74 + <ToggleGroup 75 + value={previewMode} 76 + optionClassName="text-base!" 77 + onChange={setPreviewMode} 78 + options={[ 79 + { value: "post", label: "Post" }, 80 + { value: "pub", label: "Pub" }, 81 + ]} 82 + /> 83 + </div> 84 + 85 + <div className="px-3 pb-3 gap-2 flex flex-col"> 86 + <PubThemePickerPanel state={state} /> 87 + </div> 88 + <div className="p-3 pt-0!"> 89 + <ButtonPrimary fullWidth type="submit" disabled={loading}> 90 + {loading ? <DotLoader /> : "Update"} 91 + </ButtonPrimary> 92 + </div> 93 + </form> 94 + </Popover> 95 + </div> 96 + 97 + {/* Full-page Preview */} 98 + <div 99 + className="w-full h-full overflow-y-auto" 100 + style={{ 101 + backgroundImage: pubBGImage ? `url(${pubBGImage})` : undefined, 102 + backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 103 + backgroundPosition: "center", 104 + backgroundSize: !leafletBGRepeat 105 + ? "cover" 106 + : `calc(${leafletBGRepeat}px / 2)`, 107 + }} 108 + > 109 + <div className="w-full h-full bg-bg-leaflet"> 110 + <div 111 + className="w-full min-h-full" 112 + style={{ 113 + backgroundImage: pubBGImage ? `url(${pubBGImage})` : undefined, 114 + backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 115 + backgroundPosition: "center", 116 + backgroundSize: !leafletBGRepeat 117 + ? "cover" 118 + : `calc(${leafletBGRepeat}px / 2)`, 119 + }} 120 + > 121 + {previewMode === "pub" ? ( 122 + <PubPreview 123 + showPageBackground={showPageBackground} 124 + pageWidth={pageWidth} 125 + /> 126 + ) : ( 127 + <PostPreview 128 + showPageBackground={showPageBackground} 129 + pageWidth={pageWidth} 130 + /> 131 + )} 132 + </div> 133 + </div> 134 + </div> 135 + </div> 136 + </BaseThemeProvider> 137 + ); 138 + }
+69
app/lish/[did]/[publication]/theme-settings/page.tsx
··· 1 + import { supabaseServerClient } from "supabase/serverClient"; 2 + import { getIdentityData } from "actions/getIdentityData"; 3 + import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; 4 + import { PublicationSWRDataProvider } from "../dashboard/PublicationSWRProvider"; 5 + import { PublicationThemeProviderDashboard } from "components/ThemeManager/PublicationThemeProvider"; 6 + import { AtUri } from "@atproto/syntax"; 7 + import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 8 + import { normalizePublicationRecord } from "src/utils/normalizeRecords"; 9 + import { ThemeSettingsContent } from "./ThemeSettingsContent"; 10 + 11 + export default async function ThemeSettingsPage(props: { 12 + params: Promise<{ publication: string; did: string }>; 13 + }) { 14 + let params = await props.params; 15 + let identity = await getIdentityData(); 16 + if (!identity || !identity.atp_did) 17 + return ( 18 + <NotFoundLayout> 19 + <p>Looks like you&apos;re not logged in.</p> 20 + <p> 21 + If the issue persists please{" "} 22 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 23 + </p> 24 + </NotFoundLayout> 25 + ); 26 + let did = decodeURIComponent(params.did); 27 + if (!did) return <ThemeNotFound />; 28 + let { result: publication_data } = await get_publication_data.handler( 29 + { 30 + did, 31 + publication_name: decodeURIComponent(params.publication), 32 + }, 33 + { supabase: supabaseServerClient }, 34 + ); 35 + let { publication } = publication_data; 36 + const record = normalizePublicationRecord(publication?.record); 37 + 38 + if (!publication || identity.atp_did !== publication.identity_did || !record) 39 + return <ThemeNotFound />; 40 + let uri = new AtUri(publication.uri); 41 + 42 + try { 43 + return ( 44 + <PublicationSWRDataProvider 45 + publication_did={did} 46 + publication_rkey={uri.rkey} 47 + publication_data={publication_data} 48 + > 49 + <PublicationThemeProviderDashboard> 50 + <ThemeSettingsContent /> 51 + </PublicationThemeProviderDashboard> 52 + </PublicationSWRDataProvider> 53 + ); 54 + } catch (e) { 55 + return <pre>{JSON.stringify(e, undefined, 2)}</pre>; 56 + } 57 + } 58 + 59 + const ThemeNotFound = () => { 60 + return ( 61 + <NotFoundLayout> 62 + <p className="font-bold">Sorry, we can&apos;t find this publication!</p> 63 + <p> 64 + This may be a glitch on our end. If the issue persists please{" "} 65 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 66 + </p> 67 + </NotFoundLayout> 68 + ); 69 + };
+1 -20
app/lish/[did]/[publication]/dashboard/settings/SettingsContent.tsx
··· 23 23 type SettingsView = "all" | "theme"; 24 24 25 25 export function SettingsContent(props: { showPageBackground: boolean }) { 26 - let [view, setView] = useState<SettingsView>("all"); 27 - let [themeLoading, setThemeLoading] = useState(false); 28 - 29 - if (view === "theme") { 30 - return ( 31 - <div className="flex flex-col gap-0 w-full pb-8"> 32 - <PubThemeSetter 33 - backToMenu={() => setView("all")} 34 - loading={themeLoading} 35 - setLoading={setThemeLoading} 36 - /> 37 - </div> 38 - ); 39 - } 40 - 41 - return <SettingsForm onOpenTheme={() => setView("theme")} />; 42 - } 43 - 44 - function SettingsForm(props: { onOpenTheme: () => void }) { 45 26 let { data } = usePublicationData(); 46 27 let { publication: pubData } = data || {}; 47 28 let isPro = useIsPro(); ··· 153 134 {cardBorderHidden && <hr className="border-border-light" />} 154 135 155 136 <DashboardContainer> 156 - <ThemeSettings onOpenTheme={props.onOpenTheme} /> 137 + <ThemeSettings /> 157 138 </DashboardContainer> 158 139 159 140 {cardBorderHidden && <hr className="border-border-light" />}
+13 -7
app/lish/[did]/[publication]/dashboard/settings/ThemeSettings.tsx
··· 1 - import { GoToArrow } from "components/Icons/GoToArrow"; 1 + "use client"; 2 2 3 - export function ThemeSettings(props: { onOpenTheme: () => void }) { 3 + import { GoToArrow } from "components/Icons/GoToArrow"; 4 + import { SpeedyLink } from "components/SpeedyLink"; 5 + import { useParams } from "next/navigation"; 6 + 7 + export function ThemeSettings() { 8 + let params = useParams<{ did: string; publication: string }>(); 9 + let href = `/lish/${params.did}/${params.publication}/theme-settings`; 10 + 4 11 return ( 5 12 <> 6 13 <h3 className="font-bold text-primary">Theme and Layout</h3> 7 - <button 8 - type="button" 9 - className="text-left flex gap-2 items-center text-accent-contrast font-bold w-fit" 10 - onClick={props.onOpenTheme} 14 + <SpeedyLink 15 + className="text-left flex gap-2 items-center text-accent-contrast font-bold no-underline! w-fit" 16 + href={href} 11 17 > 12 18 Customize Theme <GoToArrow /> 13 - </button> 19 + </SpeedyLink> 14 20 </> 15 21 ); 16 22 }