a tool for shared writing and social publishing

clean up leaflet options and fix links

+289 -344
-19
app/(home-pages)/home/HomeLayout.tsx
··· 235 235 <LeafletListItem 236 236 title={props?.titles?.[leaflet.root_entity]} 237 237 archived={archived} 238 - token={leaflet} 239 - draftInPublication={ 240 - leaflet.leaflets_in_publications?.[0]?.publication 241 - } 242 - published={ 243 - !!leaflet.leaflets_in_publications?.find((l) => l.doc) || 244 - !!leaflet.leaflets_to_documents?.find((l) => !!l.documents) 245 - } 246 - publishedAt={ 247 - leaflet.leaflets_in_publications?.find((l) => l.doc)?.documents 248 - ?.indexed_at || 249 - leaflet.leaflets_to_documents?.find((l) => !!l.documents) 250 - ?.documents?.indexed_at 251 - } 252 - document_uri={ 253 - leaflet.leaflets_in_publications?.find((l) => l.doc)?.documents 254 - ?.uri 255 - } 256 - leaflet_id={leaflet.root_entity} 257 238 loggedIn={!!identity} 258 239 display={display} 259 240 added_at={added_at}
+13 -24
app/(home-pages)/home/LeafletList/LeafletInfo.tsx
··· 1 1 "use client"; 2 - import { PermissionToken, useEntity } from "src/replicache"; 2 + import { useEntity } from "src/replicache"; 3 3 import { LeafletOptions } from "./LeafletOptions"; 4 - import { useState } from "react"; 5 4 import { timeAgo } from "src/utils/timeAgo"; 6 5 import { usePageTitle } from "components/utils/UpdateLeafletTitle"; 6 + import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 7 7 8 8 export const LeafletInfo = (props: { 9 9 title?: string; 10 - draftInPublication?: string; 11 - published?: boolean; 12 - token: PermissionToken; 13 - leaflet_id: string; 14 - loggedIn: boolean; 15 10 className?: string; 16 11 display: "grid" | "list"; 17 12 added_at: string; 18 - publishedAt?: string; 19 - document_uri?: string; 20 13 archived?: boolean | null; 14 + loggedIn: boolean; 21 15 }) => { 22 - let [prefetch, setPrefetch] = useState(false); 16 + const pubStatus = useLeafletPublicationStatus(); 23 17 let prettyCreatedAt = props.added_at ? timeAgo(props.added_at) : ""; 24 - let prettyPublishedAt = props.publishedAt ? timeAgo(props.publishedAt) : ""; 18 + let prettyPublishedAt = pubStatus?.publishedAt 19 + ? timeAgo(pubStatus.publishedAt) 20 + : ""; 25 21 26 22 // Look up root page first, like UpdateLeafletTitle does 27 - let firstPage = useEntity(props.leaflet_id, "root/page")[0]; 28 - let entityID = firstPage?.data.value || props.leaflet_id; 23 + let firstPage = useEntity(pubStatus?.leafletId ?? "", "root/page")[0]; 24 + let entityID = firstPage?.data.value || pubStatus?.leafletId || ""; 29 25 let titleFromDb = usePageTitle(entityID); 30 26 31 27 let title = props.title ?? titleFromDb ?? "Untitled"; ··· 39 35 {title} 40 36 </h3> 41 37 <div className="flex gap-1 shrink-0"> 42 - <LeafletOptions 43 - leaflet={props.token} 44 - draftInPublication={props.draftInPublication} 45 - document_uri={props.document_uri} 46 - shareLink={`${props.token.id}`} 47 - archived={props.archived} 48 - loggedIn={props.loggedIn} 49 - /> 38 + <LeafletOptions archived={props.archived} loggedIn={props.loggedIn} /> 50 39 </div> 51 40 </div> 52 41 <div className="flex gap-2 items-center"> 53 42 {props.archived ? ( 54 43 <div className="text-xs text-tertiary truncate">Archived</div> 55 - ) : props.draftInPublication || props.published ? ( 44 + ) : pubStatus?.draftInPublication || pubStatus?.isPublished ? ( 56 45 <div 57 - className={`text-xs w-max grow truncate ${props.published ? "font-bold text-tertiary" : "text-tertiary"}`} 46 + className={`text-xs w-max grow truncate ${pubStatus?.isPublished ? "font-bold text-tertiary" : "text-tertiary"}`} 58 47 > 59 - {props.published 48 + {pubStatus?.isPublished 60 49 ? `Published ${prettyPublishedAt}` 61 50 : `Draft ${prettyCreatedAt}`} 62 51 </div>
+20 -15
app/(home-pages)/home/LeafletList/LeafletListItem.tsx
··· 1 1 "use client"; 2 - import { PermissionToken } from "src/replicache"; 3 2 import { LeafletListPreview, LeafletGridPreview } from "./LeafletPreview"; 4 3 import { LeafletInfo } from "./LeafletInfo"; 5 4 import { useState, useRef, useEffect } from "react"; 6 5 import { SpeedyLink } from "components/SpeedyLink"; 6 + import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 7 7 8 8 export const LeafletListItem = (props: { 9 - token: PermissionToken; 10 9 archived?: boolean | null; 11 - leaflet_id: string; 12 10 loggedIn: boolean; 13 11 display: "list" | "grid"; 14 12 cardBorderHidden: boolean; 15 13 added_at: string; 16 14 title?: string; 17 - draftInPublication?: string; 18 - published?: boolean; 19 - publishedAt?: string; 20 - document_uri?: string; 21 15 index: number; 22 16 isHidden: boolean; 23 17 showPreview?: boolean; 24 18 }) => { 19 + const pubStatus = useLeafletPublicationStatus(); 25 20 let [isOnScreen, setIsOnScreen] = useState(props.index < 16 ? true : false); 26 21 let previewRef = useRef<HTMLDivElement | null>(null); 27 22 ··· 43 38 return () => observer.disconnect(); 44 39 }, [previewRef]); 45 40 41 + const tokenId = pubStatus?.shareLink ?? ""; 42 + 46 43 if (props.display === "list") 47 44 return ( 48 45 <> ··· 58 55 }} 59 56 > 60 57 <SpeedyLink 61 - href={`/${props.token.id}`} 58 + href={`/${tokenId}`} 62 59 className={`absolute w-full h-full top-0 left-0 no-underline hover:no-underline! text-primary`} 63 60 /> 64 - {props.showPreview && ( 65 - <LeafletListPreview isVisible={isOnScreen} {...props} /> 66 - )} 67 - <LeafletInfo {...props} /> 61 + {props.showPreview && <LeafletListPreview isVisible={isOnScreen} />} 62 + <LeafletInfo 63 + title={props.title} 64 + display={props.display} 65 + added_at={props.added_at} 66 + archived={props.archived} 67 + loggedIn={props.loggedIn} 68 + /> 68 69 </div> 69 70 {props.cardBorderHidden && ( 70 71 <hr ··· 92 93 }} 93 94 > 94 95 <SpeedyLink 95 - href={`/${props.token.id}`} 96 + href={`/${tokenId}`} 96 97 className={`absolute w-full h-full top-0 left-0 no-underline hover:no-underline! text-primary`} 97 98 /> 98 99 <div className="grow"> 99 - <LeafletGridPreview {...props} isVisible={isOnScreen} /> 100 + <LeafletGridPreview isVisible={isOnScreen} /> 100 101 </div> 101 102 <LeafletInfo 102 103 className="px-1 pb-0.5 shrink-0" 103 - {...props} 104 + title={props.title} 105 + display={props.display} 106 + added_at={props.added_at} 107 + archived={props.archived} 108 + loggedIn={props.loggedIn} 104 109 /> 105 110 </div> 106 111 );
+136 -173
app/(home-pages)/home/LeafletList/LeafletOptions.tsx
··· 21 21 import { HideSmall } from "components/Icons/HideSmall"; 22 22 import { hideDoc } from "../storage"; 23 23 24 - import { PermissionToken } from "src/replicache"; 25 24 import { 26 25 useIdentityData, 27 26 mutateIdentityData, ··· 31 30 mutatePublicationData, 32 31 } from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider"; 33 32 import { ShareButton } from "app/[leaflet_id]/actions/ShareOptions"; 33 + import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 34 34 35 35 export const LeafletOptions = (props: { 36 - leaflet: PermissionToken; 37 - draftInPublication?: string; 38 - document_uri?: string; 39 - shareLink: string; 40 36 archived?: boolean | null; 41 37 loggedIn?: boolean; 42 38 }) => { 39 + const pubStatus = useLeafletPublicationStatus(); 43 40 let [state, setState] = useState<"normal" | "areYouSure">("normal"); 44 41 let [open, setOpen] = useState(false); 45 42 let { identity } = useIdentityData(); 46 43 let isPublicationOwner = 47 - !!identity?.atp_did && !!props.document_uri?.includes(identity.atp_did); 44 + !!identity?.atp_did && !!pubStatus?.documentUri?.includes(identity.atp_did); 48 45 return ( 49 46 <> 50 47 <Menu ··· 68 65 > 69 66 {state === "normal" ? ( 70 67 !props.loggedIn ? ( 71 - <LoggedOutOptions 72 - leaflet={props.leaflet} 73 - setState={setState} 74 - shareLink={props.shareLink} 75 - /> 76 - ) : props.document_uri && isPublicationOwner ? ( 77 - <PublishedPostOptions 78 - setState={setState} 79 - document_uri={props.document_uri} 80 - {...props} 81 - /> 68 + <LoggedOutOptions setState={setState} /> 69 + ) : pubStatus?.documentUri && isPublicationOwner ? ( 70 + <PublishedPostOptions setState={setState} /> 82 71 ) : ( 83 - <DefaultOptions setState={setState} {...props} /> 72 + <DefaultOptions setState={setState} archived={props.archived} /> 84 73 ) 85 74 ) : state === "areYouSure" ? ( 86 - <DeleteAreYouSureForm 87 - backToMenu={() => setState("normal")} 88 - leaflet={props.leaflet} 89 - document_uri={props.document_uri} 90 - draft={!!props.draftInPublication} 91 - /> 75 + <DeleteAreYouSureForm backToMenu={() => setState("normal")} /> 92 76 ) : null} 93 77 </Menu> 94 78 </> ··· 97 81 98 82 const DefaultOptions = (props: { 99 83 setState: (s: "areYouSure") => void; 100 - draftInPublication?: string; 101 - leaflet: PermissionToken; 102 - shareLink: string; 103 84 archived?: boolean | null; 104 85 }) => { 105 - let toaster = useToaster(); 106 - let { mutate: mutatePub } = usePublicationData(); 107 - let { mutate: mutateIdentity } = useIdentityData(); 86 + const pubStatus = useLeafletPublicationStatus(); 87 + const toaster = useToaster(); 88 + const { setArchived } = useArchiveMutations(); 89 + const tokenId = pubStatus?.token.id; 90 + const itemType = pubStatus?.draftInPublication ? "Draft" : "Leaflet"; 91 + 108 92 return ( 109 93 <> 110 - <ShareButton 111 - text={ 112 - <div className="flex gap-2"> 113 - <ShareSmall /> 114 - Copy Edit Link 115 - </div> 116 - } 117 - subtext="" 118 - smokerText="Link copied!" 119 - id="get-link" 120 - link={props.shareLink} 121 - /> 94 + <EditLinkShareButton link={pubStatus?.shareLink ?? ""} /> 122 95 <hr className="border-border-light" /> 123 96 <MenuItem 124 97 onSelect={async () => { 98 + if (!tokenId) return; 99 + setArchived(tokenId, !props.archived); 100 + 125 101 if (!props.archived) { 126 - mutateIdentityData(mutateIdentity, (data) => { 127 - let item = data.permission_token_on_homepage.find( 128 - (p) => p.permission_tokens?.id === props.leaflet.id, 129 - ); 130 - if (item) item.archived = true; 131 - }); 132 - mutatePublicationData(mutatePub, (data) => { 133 - let item = data.publication?.leaflets_in_publications.find( 134 - (l) => l.permission_tokens?.id === props.leaflet.id, 135 - ); 136 - if (item) item.archived = true; 137 - }); 138 - await archivePost(props.leaflet.id); 102 + await archivePost(tokenId); 139 103 toaster({ 140 104 content: ( 141 105 <div className="font-bold flex gap-2"> 142 - Archived{props.draftInPublication ? " Draft" : " Leaflet"}! 106 + Archived {itemType}! 143 107 <ButtonTertiary 144 108 className="underline text-accent-2!" 145 109 onClick={async () => { 146 - mutateIdentityData(mutateIdentity, (data) => { 147 - let item = data.permission_token_on_homepage.find( 148 - (p) => p.permission_tokens?.id === props.leaflet.id, 149 - ); 150 - if (item) item.archived = false; 151 - }); 152 - mutatePublicationData(mutatePub, (data) => { 153 - let item = 154 - data.publication?.leaflets_in_publications.find( 155 - (l) => l.permission_tokens?.id === props.leaflet.id, 156 - ); 157 - if (item) item.archived = false; 158 - }); 159 - await unarchivePost(props.leaflet.id); 110 + setArchived(tokenId, false); 111 + await unarchivePost(tokenId); 160 112 toaster({ 161 - content: ( 162 - <div className="font-bold flex gap-2"> 163 - Unarchived! 164 - </div> 165 - ), 113 + content: <div className="font-bold">Unarchived!</div>, 166 114 type: "success", 167 115 }); 168 116 }} ··· 174 122 type: "success", 175 123 }); 176 124 } else { 177 - mutateIdentityData(mutateIdentity, (data) => { 178 - let item = data.permission_token_on_homepage.find( 179 - (p) => p.permission_tokens?.id === props.leaflet.id, 180 - ); 181 - if (item) item.archived = false; 182 - }); 183 - mutatePublicationData(mutatePub, (data) => { 184 - let item = data.publication?.leaflets_in_publications.find( 185 - (l) => l.permission_tokens?.id === props.leaflet.id, 186 - ); 187 - if (item) item.archived = false; 188 - }); 189 - await unarchivePost(props.leaflet.id); 125 + await unarchivePost(tokenId); 190 126 toaster({ 191 127 content: <div className="font-bold">Unarchived!</div>, 192 128 type: "success", ··· 195 131 }} 196 132 > 197 133 <ArchiveSmall /> 198 - {!props.archived ? " Archive" : "Unarchive"} 199 - {props.draftInPublication ? " Draft" : " Leaflet"} 134 + {!props.archived ? " Archive" : "Unarchive"} {itemType} 200 135 </MenuItem> 201 - <MenuItem 136 + <DeleteForeverMenuItem 202 137 onSelect={(e) => { 203 138 e.preventDefault(); 204 139 props.setState("areYouSure"); 205 140 }} 206 - > 207 - <DeleteSmall /> 208 - Delete Forever 209 - </MenuItem> 141 + /> 210 142 </> 211 143 ); 212 144 }; 213 145 214 - const LoggedOutOptions = (props: { 215 - leaflet: PermissionToken; 216 - setState: (s: "areYouSure") => void; 217 - shareLink: string; 218 - }) => { 219 - let toaster = useToaster(); 146 + const LoggedOutOptions = (props: { setState: (s: "areYouSure") => void }) => { 147 + const pubStatus = useLeafletPublicationStatus(); 148 + const toaster = useToaster(); 149 + 220 150 return ( 221 151 <> 222 - <ShareButton 223 - text={ 224 - <div className="flex gap-2"> 225 - <ShareSmall /> 226 - Copy Edit Link 227 - </div> 228 - } 229 - subtext="" 230 - smokerText="Link copied!" 231 - id="get-link" 232 - link={`/${props.shareLink}`} 233 - /> 152 + <EditLinkShareButton link={`/${pubStatus?.shareLink ?? ""}`} /> 234 153 <hr className="border-border-light" /> 235 154 <MenuItem 236 155 onSelect={() => { 237 - hideDoc(props.leaflet); 156 + if (pubStatus?.token) hideDoc(pubStatus.token); 238 157 toaster({ 239 158 content: <div className="font-bold">Removed from Home!</div>, 240 159 type: "success", ··· 244 163 <HideSmall /> 245 164 Remove from Home 246 165 </MenuItem> 247 - <MenuItem 166 + <DeleteForeverMenuItem 248 167 onSelect={(e) => { 249 168 e.preventDefault(); 250 169 props.setState("areYouSure"); 251 170 }} 252 - > 253 - <DeleteSmall /> 254 - Delete Forever 255 - </MenuItem> 171 + /> 256 172 </> 257 173 ); 258 174 }; 259 175 260 176 const PublishedPostOptions = (props: { 261 177 setState: (s: "areYouSure") => void; 262 - document_uri: string; 263 - leaflet: PermissionToken; 264 - shareLink: string; 265 178 }) => { 266 - let toaster = useToaster(); 179 + const pubStatus = useLeafletPublicationStatus(); 180 + const toaster = useToaster(); 181 + const postLink = pubStatus?.postShareLink ?? ""; 182 + const isFullUrl = postLink.includes("http"); 183 + 267 184 return ( 268 185 <> 269 186 <ShareButton ··· 275 192 } 276 193 smokerText="Link copied!" 277 194 id="get-link" 278 - link="" 279 - fullLink={props.shareLink} 195 + link={postLink} 196 + fullLink={isFullUrl ? postLink : undefined} 280 197 /> 281 - 282 198 <hr className="border-border-light" /> 283 199 <MenuItem 284 200 onSelect={async () => { 285 - if (props.document_uri) { 286 - await unpublishPost(props.document_uri); 201 + if (pubStatus?.documentUri) { 202 + await unpublishPost(pubStatus.documentUri); 287 203 } 288 204 toaster({ 289 205 content: <div className="font-bold">Unpublished Post!</div>, ··· 299 215 </div> 300 216 </div> 301 217 </MenuItem> 302 - <MenuItem 218 + <DeleteForeverMenuItem 303 219 onSelect={(e) => { 304 220 e.preventDefault(); 305 221 props.setState("areYouSure"); 306 222 }} 307 - > 308 - <DeleteSmall /> 309 - <div className="flex flex-col"> 310 - Delete Post 311 - <div className="text-tertiary text-sm font-normal!"> 312 - Unpublish AND delete 313 - </div> 314 - </div> 315 - </MenuItem> 223 + subtext="Post" 224 + /> 316 225 </> 317 226 ); 318 227 }; 319 228 320 - const DeleteAreYouSureForm = (props: { 321 - backToMenu: () => void; 322 - document_uri?: string; 323 - leaflet: PermissionToken; 324 - draft?: boolean; 325 - }) => { 326 - let toaster = useToaster(); 327 - let { mutate: mutatePub } = usePublicationData(); 328 - let { mutate: mutateIdentity } = useIdentityData(); 229 + const DeleteAreYouSureForm = (props: { backToMenu: () => void }) => { 230 + const pubStatus = useLeafletPublicationStatus(); 231 + const toaster = useToaster(); 232 + const { removeFromLists } = useArchiveMutations(); 233 + const tokenId = pubStatus?.token.id; 234 + 235 + const itemType = pubStatus?.documentUri 236 + ? "Post" 237 + : pubStatus?.draftInPublication 238 + ? "Draft" 239 + : "Leaflet"; 329 240 330 241 return ( 331 242 <div className="flex flex-col justify-center p-2 text-center"> ··· 339 250 </ButtonTertiary> 340 251 <ButtonPrimary 341 252 onClick={async () => { 342 - mutateIdentityData(mutateIdentity, (data) => { 343 - data.permission_token_on_homepage = 344 - data.permission_token_on_homepage.filter( 345 - (p) => p.permission_tokens?.id !== props.leaflet.id, 346 - ); 347 - }); 348 - mutatePublicationData(mutatePub, (data) => { 349 - if (!data.publication) return; 350 - data.publication.leaflets_in_publications = 351 - data.publication.leaflets_in_publications.filter( 352 - (l) => l.permission_tokens?.id !== props.leaflet.id, 353 - ); 354 - }); 355 - if (props.document_uri) { 356 - await deletePost(props.document_uri); 253 + if (tokenId) removeFromLists(tokenId); 254 + if (pubStatus?.documentUri) { 255 + await deletePost(pubStatus.documentUri); 357 256 } 358 - deleteLeaflet(props.leaflet); 257 + if (pubStatus?.token) deleteLeaflet(pubStatus.token); 359 258 360 259 toaster({ 361 - content: ( 362 - <div className="font-bold"> 363 - Deleted{" "} 364 - {props.document_uri 365 - ? "Post!" 366 - : props.draft 367 - ? "Draft" 368 - : "Leaflet!"} 369 - </div> 370 - ), 260 + content: <div className="font-bold">Deleted {itemType}!</div>, 371 261 type: "success", 372 262 }); 373 263 }} ··· 378 268 </div> 379 269 ); 380 270 }; 271 + 272 + // Shared menu items 273 + const EditLinkShareButton = (props: { link: string }) => ( 274 + <ShareButton 275 + text={ 276 + <div className="flex gap-2"> 277 + <ShareSmall /> 278 + Copy Edit Link 279 + </div> 280 + } 281 + subtext="" 282 + smokerText="Link copied!" 283 + id="get-link" 284 + link={props.link} 285 + /> 286 + ); 287 + 288 + const DeleteForeverMenuItem = (props: { 289 + onSelect: (e: Event) => void; 290 + subtext?: string; 291 + }) => ( 292 + <MenuItem onSelect={props.onSelect}> 293 + <DeleteSmall /> 294 + {props.subtext ? ( 295 + <div className="flex flex-col"> 296 + Delete {props.subtext} 297 + <div className="text-tertiary text-sm font-normal!"> 298 + Unpublish AND delete 299 + </div> 300 + </div> 301 + ) : ( 302 + "Delete Forever" 303 + )} 304 + </MenuItem> 305 + ); 306 + 307 + // Helper to update archived state in both identity and publication data 308 + function useArchiveMutations() { 309 + const { mutate: mutatePub } = usePublicationData(); 310 + const { mutate: mutateIdentity } = useIdentityData(); 311 + 312 + return { 313 + setArchived: (tokenId: string, archived: boolean) => { 314 + mutateIdentityData(mutateIdentity, (data) => { 315 + const item = data.permission_token_on_homepage.find( 316 + (p) => p.permission_tokens?.id === tokenId, 317 + ); 318 + if (item) item.archived = archived; 319 + }); 320 + mutatePublicationData(mutatePub, (data) => { 321 + const item = data.publication?.leaflets_in_publications.find( 322 + (l) => l.permission_tokens?.id === tokenId, 323 + ); 324 + if (item) item.archived = archived; 325 + }); 326 + }, 327 + removeFromLists: (tokenId: string) => { 328 + mutateIdentityData(mutateIdentity, (data) => { 329 + data.permission_token_on_homepage = 330 + data.permission_token_on_homepage.filter( 331 + (p) => p.permission_tokens?.id !== tokenId, 332 + ); 333 + }); 334 + mutatePublicationData(mutatePub, (data) => { 335 + if (!data.publication) return; 336 + data.publication.leaflets_in_publications = 337 + data.publication.leaflets_in_publications.filter( 338 + (l) => l.permission_tokens?.id !== tokenId, 339 + ); 340 + }); 341 + }, 342 + }; 343 + }
+46 -95
app/(home-pages)/home/LeafletList/LeafletPreview.tsx
··· 3 3 ThemeBackgroundProvider, 4 4 ThemeProvider, 5 5 } from "components/ThemeManager/ThemeProvider"; 6 - import { 7 - PermissionToken, 8 - useEntity, 9 - useReferenceToEntity, 10 - } from "src/replicache"; 6 + import { useEntity, useReferenceToEntity } from "src/replicache"; 11 7 import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 12 8 import { LeafletContent } from "./LeafletContent"; 13 9 import { Tooltip } from "components/Tooltip"; 10 + import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 11 + import { CSSProperties } from "react"; 14 12 15 - export const LeafletListPreview = (props: { 16 - draft?: boolean; 17 - published?: boolean; 18 - isVisible: boolean; 19 - token: PermissionToken; 20 - leaflet_id: string; 21 - loggedIn: boolean; 22 - }) => { 23 - let root = 24 - useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity || 25 - props.leaflet_id; 26 - let firstPage = useEntity(root, "root/page")[0]; 27 - let page = firstPage?.data.value || root; 13 + function useLeafletPreviewData() { 14 + const pubStatus = useLeafletPublicationStatus(); 15 + const leafletId = pubStatus?.leafletId ?? ""; 16 + const root = 17 + useReferenceToEntity("root/page", leafletId)[0]?.entity || leafletId; 18 + const firstPage = useEntity(root, "root/page")[0]; 19 + const page = firstPage?.data.value || root; 28 20 29 - let cardBorderHidden = useCardBorderHidden(root); 30 - let rootBackgroundImage = useEntity(root, "theme/card-background-image"); 31 - let rootBackgroundRepeat = useEntity( 21 + const cardBorderHidden = useCardBorderHidden(root); 22 + const rootBackgroundImage = useEntity(root, "theme/card-background-image"); 23 + const rootBackgroundRepeat = useEntity( 32 24 root, 33 25 "theme/card-background-image-repeat", 34 26 ); 35 - let rootBackgroundOpacity = useEntity( 27 + const rootBackgroundOpacity = useEntity( 36 28 root, 37 29 "theme/card-background-image-opacity", 38 30 ); 39 31 32 + const contentWrapperStyle: CSSProperties = cardBorderHidden 33 + ? {} 34 + : { 35 + backgroundImage: rootBackgroundImage 36 + ? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})` 37 + : undefined, 38 + backgroundRepeat: rootBackgroundRepeat ? "repeat" : "no-repeat", 39 + backgroundPosition: "center", 40 + backgroundSize: !rootBackgroundRepeat 41 + ? "cover" 42 + : rootBackgroundRepeat?.data.value / 3, 43 + opacity: 44 + rootBackgroundImage?.data.src && rootBackgroundOpacity 45 + ? rootBackgroundOpacity.data.value 46 + : 1, 47 + backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 48 + }; 49 + 50 + const contentWrapperClass = `leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`; 51 + 52 + return { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass }; 53 + } 54 + 55 + export const LeafletListPreview = (props: { isVisible: boolean }) => { 56 + const { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass } = 57 + useLeafletPreviewData(); 58 + 40 59 return ( 41 60 <Tooltip 42 61 open={true} ··· 73 92 <ThemeProvider local entityID={root} className="rounded-sm"> 74 93 <ThemeBackgroundProvider entityID={root}> 75 94 <div className="leafletPreview grow shrink-0 h-44 w-64 px-2 pt-2 sm:px-3 sm:pt-3 flex items-end pointer-events-none rounded-[2px] "> 76 - <div 77 - className={`leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`} 78 - style={ 79 - cardBorderHidden 80 - ? {} 81 - : { 82 - backgroundImage: rootBackgroundImage 83 - ? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})` 84 - : undefined, 85 - backgroundRepeat: rootBackgroundRepeat 86 - ? "repeat" 87 - : "no-repeat", 88 - backgroundPosition: "center", 89 - backgroundSize: !rootBackgroundRepeat 90 - ? "cover" 91 - : rootBackgroundRepeat?.data.value / 3, 92 - opacity: 93 - rootBackgroundImage?.data.src && rootBackgroundOpacity 94 - ? rootBackgroundOpacity.data.value 95 - : 1, 96 - backgroundColor: 97 - "rgba(var(--bg-page), var(--bg-page-alpha))", 98 - } 99 - } 100 - > 95 + <div className={contentWrapperClass} style={contentWrapperStyle}> 101 96 <LeafletContent entityID={page} isOnScreen={props.isVisible} /> 102 97 </div> 103 98 </div> ··· 107 102 ); 108 103 }; 109 104 110 - export const LeafletGridPreview = (props: { 111 - draft?: boolean; 112 - published?: boolean; 113 - token: PermissionToken; 114 - leaflet_id: string; 115 - loggedIn: boolean; 116 - isVisible: boolean; 117 - }) => { 118 - let root = 119 - useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity || 120 - props.leaflet_id; 121 - let firstPage = useEntity(root, "root/page")[0]; 122 - let page = firstPage?.data.value || root; 105 + export const LeafletGridPreview = (props: { isVisible: boolean }) => { 106 + const { root, page, contentWrapperStyle, contentWrapperClass } = 107 + useLeafletPreviewData(); 123 108 124 - let cardBorderHidden = useCardBorderHidden(root); 125 - let rootBackgroundImage = useEntity(root, "theme/card-background-image"); 126 - let rootBackgroundRepeat = useEntity( 127 - root, 128 - "theme/card-background-image-repeat", 129 - ); 130 - let rootBackgroundOpacity = useEntity( 131 - root, 132 - "theme/card-background-image-opacity", 133 - ); 134 109 return ( 135 110 <ThemeProvider local entityID={root} className="w-full!"> 136 111 <div className="border border-border-light rounded-md w-full h-full overflow-hidden "> ··· 140 115 inert 141 116 className="leafletPreview grow shrink-0 h-full w-full px-2 pt-2 sm:px-3 sm:pt-3 flex items-end pointer-events-none" 142 117 > 143 - <div 144 - className={`leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`} 145 - style={ 146 - cardBorderHidden 147 - ? {} 148 - : { 149 - backgroundImage: rootBackgroundImage 150 - ? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})` 151 - : undefined, 152 - backgroundRepeat: rootBackgroundRepeat 153 - ? "repeat" 154 - : "no-repeat", 155 - backgroundPosition: "center", 156 - backgroundSize: !rootBackgroundRepeat 157 - ? "cover" 158 - : rootBackgroundRepeat?.data.value / 3, 159 - opacity: 160 - rootBackgroundImage?.data.src && rootBackgroundOpacity 161 - ? rootBackgroundOpacity.data.value 162 - : 1, 163 - backgroundColor: 164 - "rgba(var(--bg-page), var(--bg-page-alpha))", 165 - } 166 - } 167 - > 118 + <div className={contentWrapperClass} style={contentWrapperStyle}> 168 119 <LeafletContent entityID={page} isOnScreen={props.isVisible} /> 169 120 </div> 170 121 </div>
+25 -7
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 19 19 import { CommentTiny } from "components/Icons/CommentTiny"; 20 20 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 21 21 import { LeafletOptions } from "app/(home-pages)/home/LeafletList/LeafletOptions"; 22 + import { StaticLeafletDataContext } from "components/PageSWRDataProvider"; 22 23 23 24 export function PublishedPostsList(props: { 24 25 searchValue: string; ··· 84 85 </h3> 85 86 </a> 86 87 <div className="flex justify-start align-top flex-row gap-1"> 87 - {leaflet && ( 88 + {leaflet && leaflet.permission_tokens && ( 88 89 <> 89 90 <SpeedyLink 90 91 className="pt-[6px]" ··· 93 94 <EditTiny /> 94 95 </SpeedyLink> 95 96 96 - <LeafletOptions 97 - leaflet={leaflet?.permission_tokens!} 98 - document_uri={doc.documents.uri} 99 - shareLink={postLink} 100 - loggedIn={true} 101 - /> 97 + <StaticLeafletDataContext 98 + value={{ 99 + ...leaflet.permission_tokens, 100 + leaflets_in_publications: [ 101 + { 102 + ...leaflet, 103 + publications: publication, 104 + documents: doc.documents 105 + ? { 106 + uri: doc.documents.uri, 107 + indexed_at: doc.documents.indexed_at, 108 + data: doc.documents.data, 109 + } 110 + : null, 111 + }, 112 + ], 113 + leaflets_to_documents: [], 114 + blocked_by_admin: null, 115 + custom_domain_routes: [], 116 + }} 117 + > 118 + <LeafletOptions loggedIn={true} /> 119 + </StaticLeafletDataContext> 102 120 </> 103 121 )} 104 122 </div>
+3 -11
app/lish/createPub/getPublicationURL.ts
··· 3 3 import { isProductionDomain } from "src/utils/isProductionDeployment"; 4 4 import { Json } from "supabase/database.types"; 5 5 6 - export function getPublicationURL(pub: { 7 - uri: string; 8 - name: string; 9 - record: Json; 10 - }) { 6 + export function getPublicationURL(pub: { uri: string; record: Json }) { 11 7 let record = pub.record as PubLeafletPublication.Record; 12 8 if (isProductionDomain() && record?.base_path) 13 9 return `https://${record.base_path}`; 14 10 else return getBasePublicationURL(pub); 15 11 } 16 12 17 - export function getBasePublicationURL(pub: { 18 - uri: string; 19 - name: string; 20 - record: Json; 21 - }) { 13 + export function getBasePublicationURL(pub: { uri: string; record: Json }) { 22 14 let record = pub.record as PubLeafletPublication.Record; 23 15 let aturi = new AtUri(pub.uri); 24 - return `/lish/${aturi.host}/${encodeURIComponent(aturi.rkey || record?.name || pub.name)}`; 16 + return `/lish/${aturi.host}/${encodeURIComponent(aturi.rkey || record?.name)}`; 25 17 }
+46
components/PageSWRDataProvider.tsx
··· 8 8 import type { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; 9 9 import { createContext, useContext } from "react"; 10 10 import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; 11 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 12 + import { AtUri } from "@atproto/syntax"; 11 13 12 14 export const StaticLeafletDataContext = createContext< 13 15 null | GetLeafletDataReturnType["result"]["data"] ··· 80 82 let { data, mutate } = useLeafletData(); 81 83 return { data: data?.custom_domain_routes, mutate: mutate }; 82 84 } 85 + 86 + export function useLeafletPublicationStatus() { 87 + const data = useContext(StaticLeafletDataContext); 88 + if (!data) return null; 89 + 90 + const publishedInPublication = data.leaflets_in_publications?.find( 91 + (l) => l.doc, 92 + ); 93 + const publishedStandalone = data.leaflets_to_documents?.find( 94 + (l) => !!l.documents, 95 + ); 96 + 97 + const documentUri = 98 + publishedInPublication?.documents?.uri ?? publishedStandalone?.document; 99 + 100 + // Compute the full post URL for sharing 101 + let postShareLink: string | undefined; 102 + if (publishedInPublication?.publications && publishedInPublication.documents) { 103 + // Published in a publication - use publication URL + document rkey 104 + const docUri = new AtUri(publishedInPublication.documents.uri); 105 + postShareLink = `${getPublicationURL(publishedInPublication.publications)}/${docUri.rkey}`; 106 + } else if (publishedStandalone?.document) { 107 + // Standalone published post - use /p/{did}/{rkey} format 108 + const docUri = new AtUri(publishedStandalone.document); 109 + postShareLink = `/p/${docUri.host}/${docUri.rkey}`; 110 + } 111 + 112 + return { 113 + token: data, 114 + leafletId: data.root_entity, 115 + shareLink: data.id, 116 + // Draft state - in a publication but not yet published 117 + draftInPublication: 118 + data.leaflets_in_publications?.[0]?.publication ?? undefined, 119 + // Published state 120 + isPublished: !!(publishedInPublication || publishedStandalone), 121 + publishedAt: 122 + publishedInPublication?.documents?.indexed_at ?? 123 + publishedStandalone?.documents?.indexed_at, 124 + documentUri, 125 + // Full URL for sharing published posts 126 + postShareLink, 127 + }; 128 + }