a tool for shared writing and social publishing

add posts to profile page

+121 -40
+1 -1
app/(home-pages)/reader/getReaderFeed.ts
··· 83 83 84 84 export type Post = { 85 85 author: string | null; 86 - publication: { 86 + publication?: { 87 87 href: string; 88 88 pubRecord: Json; 89 89 uri: string;
+5 -5
app/p/[didOrHandle]/ProfilePageLayout.tsx
··· 11 11 import { PubIcon } from "components/ActionBar/Publications"; 12 12 import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; 13 13 import { colorToString } from "components/ThemeManager/useColorAttribute"; 14 + import type { Post } from "app/(home-pages)/reader/getReaderFeed"; 14 15 15 16 export const ProfilePageLayout = (props: { 16 17 publications: { record: Json; uri: string }[]; 18 + posts: Post[]; 17 19 profile: { 18 20 did: string; 19 21 handle: string | null; ··· 23 25 }) => { 24 26 if (!props.profile) return null; 25 27 26 - let profileRecord = props.profile.record as unknown as ProfileViewDetailed; 27 - 28 - console.log(profileRecord); 29 28 return ( 30 29 <DashboardLayout 31 30 id={props.profile.did} ··· 37 36 <ProfilePageContent 38 37 profile={props.profile} 39 38 publications={props.publications} 39 + posts={props.posts} 40 40 /> 41 41 ), 42 42 controls: null, ··· 51 51 export type profileTabsType = "posts" | "comments" | "subscriptions"; 52 52 const ProfilePageContent = (props: { 53 53 publications: { record: Json; uri: string }[]; 54 + posts: Post[]; 54 55 profile: { 55 56 did: string; 56 57 handle: string | null; ··· 61 62 let [tab, setTab] = useState<profileTabsType>("posts"); 62 63 63 64 let profileRecord = props.profile?.record as AppBskyActorProfile.Record; 64 - console.log(profileRecord); 65 65 66 66 if (!props.profile) return; 67 67 return ( ··· 102 102 ))} 103 103 </div> 104 104 <ProfileTabs tab={tab} setTab={setTab} /> 105 - <TabContent tab={tab} /> 105 + <TabContent tab={tab} posts={props.posts} /> 106 106 </div> 107 107 ); 108 108 };
+14 -2
app/p/[didOrHandle]/ProfileTabs/Tabs.tsx
··· 1 1 import { Tab } from "components/Tab"; 2 2 import { profileTabsType } from "../ProfilePageLayout"; 3 + import { PostListing } from "components/PostListing"; 4 + import type { Post } from "app/(home-pages)/reader/getReaderFeed"; 3 5 4 6 export const ProfileTabs = (props: { 5 7 tab: profileTabsType; ··· 37 39 ); 38 40 }; 39 41 40 - export const TabContent = (props: { tab: profileTabsType }) => { 42 + export const TabContent = (props: { tab: profileTabsType; posts: Post[] }) => { 41 43 switch (props.tab) { 42 44 case "posts": 43 - return <div>posts here!</div>; 45 + return ( 46 + <div className="flex flex-col gap-2 text-left"> 47 + {props.posts.length === 0 ? ( 48 + <div className="text-tertiary text-center py-4">No posts yet</div> 49 + ) : ( 50 + props.posts.map((post) => ( 51 + <PostListing key={post.documents.uri} {...post} /> 52 + )) 53 + )} 54 + </div> 55 + ); 44 56 case "comments": 45 57 return <div>comments here!</div>; 46 58 case "subscriptions":
+70 -10
app/p/[didOrHandle]/page.tsx
··· 2 2 import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 3 3 import { ProfilePageLayout } from "./ProfilePageLayout"; 4 4 import { supabaseServerClient } from "supabase/serverClient"; 5 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 6 + import type { Post } from "app/(home-pages)/reader/getReaderFeed"; 7 + 5 8 export default async function ProfilePage(props: { 6 9 params: Promise<{ didOrHandle: string }>; 7 10 }) { ··· 26 29 } 27 30 did = resolved; 28 31 } 29 - let { data: profile } = await supabaseServerClient 30 - .from("bsky_profiles") 31 - .select(`*`) 32 - .eq("did", did) 33 - .single(); 34 - let { data: pubs } = await supabaseServerClient 35 - .from("publications") 36 - .select("*") 37 - .eq("identity_did", did); 38 32 39 - return <ProfilePageLayout profile={profile} publications={pubs || []} />; 33 + // Fetch profile, publications, and documents in parallel 34 + let [{ data: profile }, { data: pubs }, { data: docs }] = await Promise.all([ 35 + supabaseServerClient 36 + .from("bsky_profiles") 37 + .select(`*`) 38 + .eq("did", did) 39 + .single(), 40 + supabaseServerClient 41 + .from("publications") 42 + .select("*") 43 + .eq("identity_did", did), 44 + supabaseServerClient 45 + .from("documents") 46 + .select( 47 + `*, 48 + comments_on_documents(count), 49 + document_mentions_in_bsky(count), 50 + documents_in_publications(publications(*))`, 51 + ) 52 + .like("uri", `at://${did}/%`) 53 + .order("indexed_at", { ascending: false }), 54 + ]); 55 + 56 + // Build a map of publications for quick lookup 57 + let pubMap = new Map<string, NonNullable<typeof pubs>[number]>(); 58 + for (let pub of pubs || []) { 59 + pubMap.set(pub.uri, pub); 60 + } 61 + 62 + // Transform data to Post[] format 63 + let handle = profile?.handle ? `@${profile.handle}` : null; 64 + let posts: Post[] = []; 65 + 66 + for (let doc of docs || []) { 67 + // Find the publication for this document (if any) 68 + let pubFromDoc = doc.documents_in_publications?.[0]?.publications; 69 + let pub = pubFromDoc ? pubMap.get(pubFromDoc.uri) || pubFromDoc : null; 70 + 71 + let post: Post = { 72 + author: handle, 73 + documents: { 74 + data: doc.data, 75 + uri: doc.uri, 76 + indexed_at: doc.indexed_at, 77 + comments_on_documents: doc.comments_on_documents, 78 + document_mentions_in_bsky: doc.document_mentions_in_bsky, 79 + }, 80 + }; 81 + 82 + if (pub) { 83 + post.publication = { 84 + href: getPublicationURL(pub), 85 + pubRecord: pub.record, 86 + uri: pub.uri, 87 + }; 88 + } 89 + 90 + posts.push(post); 91 + } 92 + 93 + return ( 94 + <ProfilePageLayout 95 + profile={profile} 96 + publications={pubs || []} 97 + posts={posts} 98 + /> 99 + ); 40 100 }
+31 -22
components/PostListing.tsx
··· 15 15 import { InteractionPreview } from "./InteractionsPreview"; 16 16 17 17 export const PostListing = (props: Post) => { 18 - let pubRecord = props.publication.pubRecord as PubLeafletPublication.Record; 18 + let pubRecord = props.publication?.pubRecord as 19 + | PubLeafletPublication.Record 20 + | undefined; 19 21 20 22 let postRecord = props.documents.data as PubLeafletDocument.Record; 21 23 let postUri = new AtUri(props.documents.uri); 22 24 23 - let theme = usePubTheme(pubRecord.theme); 24 - let backgroundImage = pubRecord?.theme?.backgroundImage?.image?.ref 25 - ? blobRefToSrc( 26 - pubRecord?.theme?.backgroundImage?.image?.ref, 27 - new AtUri(props.publication.uri).host, 28 - ) 29 - : null; 25 + let theme = usePubTheme(pubRecord?.theme); 26 + let backgroundImage = 27 + pubRecord?.theme?.backgroundImage?.image?.ref && props.publication 28 + ? blobRefToSrc( 29 + pubRecord.theme.backgroundImage.image.ref, 30 + new AtUri(props.publication.uri).host, 31 + ) 32 + : null; 30 33 31 34 let backgroundImageRepeat = pubRecord?.theme?.backgroundImage?.repeat; 32 35 let backgroundImageSize = pubRecord?.theme?.backgroundImage?.width || 500; 33 36 34 - let showPageBackground = pubRecord.theme?.showPageBackground; 37 + let showPageBackground = pubRecord?.theme?.showPageBackground; 35 38 36 39 let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 37 40 let comments = 38 - pubRecord.preferences?.showComments === false 41 + pubRecord?.preferences?.showComments === false 39 42 ? 0 40 43 : props.documents.comments_on_documents?.[0]?.count || 0; 41 44 let tags = (postRecord?.tags as string[] | undefined) || []; 42 45 46 + // For standalone posts, link directly to the document 47 + let postHref = props.publication 48 + ? `${props.publication.href}/${postUri.rkey}` 49 + : `/doc/${postUri.host}/${postUri.rkey}`; 50 + 43 51 return ( 44 52 <BaseThemeProvider {...theme} local> 45 53 <div 46 54 style={{ 47 - backgroundImage: `url(${backgroundImage})`, 55 + backgroundImage: backgroundImage 56 + ? `url(${backgroundImage})` 57 + : undefined, 48 58 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 49 59 backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 50 60 }} ··· 55 65 hover:outline-accent-contrast hover:border-accent-contrast 56 66 `} 57 67 > 58 - <Link 59 - className="h-full w-full absolute top-0 left-0" 60 - href={`${props.publication.href}/${postUri.rkey}`} 61 - /> 68 + <Link className="h-full w-full absolute top-0 left-0" href={postHref} /> 62 69 <div 63 70 className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2`} 64 71 style={{ ··· 71 78 72 79 <p className="text-secondary italic">{postRecord.description}</p> 73 80 <div className="flex flex-col-reverse md:flex-row md gap-2 text-sm text-tertiary items-center justify-start pt-1.5 md:pt-3 w-full"> 74 - <PubInfo 75 - href={props.publication.href} 76 - pubRecord={pubRecord} 77 - uri={props.publication.uri} 78 - /> 81 + {props.publication && pubRecord && ( 82 + <PubInfo 83 + href={props.publication.href} 84 + pubRecord={pubRecord} 85 + uri={props.publication.uri} 86 + /> 87 + )} 79 88 <div className="flex flex-row justify-between gap-2 items-center w-full"> 80 89 <PostInfo publishedAt={postRecord.publishedAt} /> 81 90 <InteractionPreview 82 - postUrl={`${props.publication.href}/${postUri.rkey}`} 91 + postUrl={postHref} 83 92 quotesCount={quotes} 84 93 commentsCount={comments} 85 94 tags={tags} 86 - showComments={pubRecord.preferences?.showComments} 95 + showComments={pubRecord?.preferences?.showComments} 87 96 share 88 97 /> 89 98 </div>