tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
28
pulls
pipelines
add posts to profile page
awarm.space
3 months ago
2d0665ee
1bb5c4b7
+121
-40
5 changed files
expand all
collapse all
unified
split
app
(home-pages)
reader
getReaderFeed.ts
p
[didOrHandle]
ProfilePageLayout.tsx
ProfileTabs
Tabs.tsx
page.tsx
components
PostListing.tsx
+1
-1
app/(home-pages)/reader/getReaderFeed.ts
···
83
83
84
84
export type Post = {
85
85
author: string | null;
86
86
-
publication: {
86
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
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
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
26
-
let profileRecord = props.profile.record as unknown as ProfileViewDetailed;
27
27
-
28
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
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
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
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
105
-
<TabContent tab={tab} />
105
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
3
+
import { PostListing } from "components/PostListing";
4
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
40
-
export const TabContent = (props: { tab: profileTabsType }) => {
42
42
+
export const TabContent = (props: { tab: profileTabsType; posts: Post[] }) => {
41
43
switch (props.tab) {
42
44
case "posts":
43
43
-
return <div>posts here!</div>;
45
45
+
return (
46
46
+
<div className="flex flex-col gap-2 text-left">
47
47
+
{props.posts.length === 0 ? (
48
48
+
<div className="text-tertiary text-center py-4">No posts yet</div>
49
49
+
) : (
50
50
+
props.posts.map((post) => (
51
51
+
<PostListing key={post.documents.uri} {...post} />
52
52
+
))
53
53
+
)}
54
54
+
</div>
55
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
5
+
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
6
6
+
import type { Post } from "app/(home-pages)/reader/getReaderFeed";
7
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
29
-
let { data: profile } = await supabaseServerClient
30
30
-
.from("bsky_profiles")
31
31
-
.select(`*`)
32
32
-
.eq("did", did)
33
33
-
.single();
34
34
-
let { data: pubs } = await supabaseServerClient
35
35
-
.from("publications")
36
36
-
.select("*")
37
37
-
.eq("identity_did", did);
38
32
39
39
-
return <ProfilePageLayout profile={profile} publications={pubs || []} />;
33
33
+
// Fetch profile, publications, and documents in parallel
34
34
+
let [{ data: profile }, { data: pubs }, { data: docs }] = await Promise.all([
35
35
+
supabaseServerClient
36
36
+
.from("bsky_profiles")
37
37
+
.select(`*`)
38
38
+
.eq("did", did)
39
39
+
.single(),
40
40
+
supabaseServerClient
41
41
+
.from("publications")
42
42
+
.select("*")
43
43
+
.eq("identity_did", did),
44
44
+
supabaseServerClient
45
45
+
.from("documents")
46
46
+
.select(
47
47
+
`*,
48
48
+
comments_on_documents(count),
49
49
+
document_mentions_in_bsky(count),
50
50
+
documents_in_publications(publications(*))`,
51
51
+
)
52
52
+
.like("uri", `at://${did}/%`)
53
53
+
.order("indexed_at", { ascending: false }),
54
54
+
]);
55
55
+
56
56
+
// Build a map of publications for quick lookup
57
57
+
let pubMap = new Map<string, NonNullable<typeof pubs>[number]>();
58
58
+
for (let pub of pubs || []) {
59
59
+
pubMap.set(pub.uri, pub);
60
60
+
}
61
61
+
62
62
+
// Transform data to Post[] format
63
63
+
let handle = profile?.handle ? `@${profile.handle}` : null;
64
64
+
let posts: Post[] = [];
65
65
+
66
66
+
for (let doc of docs || []) {
67
67
+
// Find the publication for this document (if any)
68
68
+
let pubFromDoc = doc.documents_in_publications?.[0]?.publications;
69
69
+
let pub = pubFromDoc ? pubMap.get(pubFromDoc.uri) || pubFromDoc : null;
70
70
+
71
71
+
let post: Post = {
72
72
+
author: handle,
73
73
+
documents: {
74
74
+
data: doc.data,
75
75
+
uri: doc.uri,
76
76
+
indexed_at: doc.indexed_at,
77
77
+
comments_on_documents: doc.comments_on_documents,
78
78
+
document_mentions_in_bsky: doc.document_mentions_in_bsky,
79
79
+
},
80
80
+
};
81
81
+
82
82
+
if (pub) {
83
83
+
post.publication = {
84
84
+
href: getPublicationURL(pub),
85
85
+
pubRecord: pub.record,
86
86
+
uri: pub.uri,
87
87
+
};
88
88
+
}
89
89
+
90
90
+
posts.push(post);
91
91
+
}
92
92
+
93
93
+
return (
94
94
+
<ProfilePageLayout
95
95
+
profile={profile}
96
96
+
publications={pubs || []}
97
97
+
posts={posts}
98
98
+
/>
99
99
+
);
40
100
}
+31
-22
components/PostListing.tsx
···
15
15
import { InteractionPreview } from "./InteractionsPreview";
16
16
17
17
export const PostListing = (props: Post) => {
18
18
-
let pubRecord = props.publication.pubRecord as PubLeafletPublication.Record;
18
18
+
let pubRecord = props.publication?.pubRecord as
19
19
+
| PubLeafletPublication.Record
20
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
23
-
let theme = usePubTheme(pubRecord.theme);
24
24
-
let backgroundImage = pubRecord?.theme?.backgroundImage?.image?.ref
25
25
-
? blobRefToSrc(
26
26
-
pubRecord?.theme?.backgroundImage?.image?.ref,
27
27
-
new AtUri(props.publication.uri).host,
28
28
-
)
29
29
-
: null;
25
25
+
let theme = usePubTheme(pubRecord?.theme);
26
26
+
let backgroundImage =
27
27
+
pubRecord?.theme?.backgroundImage?.image?.ref && props.publication
28
28
+
? blobRefToSrc(
29
29
+
pubRecord.theme.backgroundImage.image.ref,
30
30
+
new AtUri(props.publication.uri).host,
31
31
+
)
32
32
+
: null;
30
33
31
34
let backgroundImageRepeat = pubRecord?.theme?.backgroundImage?.repeat;
32
35
let backgroundImageSize = pubRecord?.theme?.backgroundImage?.width || 500;
33
36
34
34
-
let showPageBackground = pubRecord.theme?.showPageBackground;
37
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
38
-
pubRecord.preferences?.showComments === false
41
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
46
+
// For standalone posts, link directly to the document
47
47
+
let postHref = props.publication
48
48
+
? `${props.publication.href}/${postUri.rkey}`
49
49
+
: `/doc/${postUri.host}/${postUri.rkey}`;
50
50
+
43
51
return (
44
52
<BaseThemeProvider {...theme} local>
45
53
<div
46
54
style={{
47
47
-
backgroundImage: `url(${backgroundImage})`,
55
55
+
backgroundImage: backgroundImage
56
56
+
? `url(${backgroundImage})`
57
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
58
-
<Link
59
59
-
className="h-full w-full absolute top-0 left-0"
60
60
-
href={`${props.publication.href}/${postUri.rkey}`}
61
61
-
/>
68
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
74
-
<PubInfo
75
75
-
href={props.publication.href}
76
76
-
pubRecord={pubRecord}
77
77
-
uri={props.publication.uri}
78
78
-
/>
81
81
+
{props.publication && pubRecord && (
82
82
+
<PubInfo
83
83
+
href={props.publication.href}
84
84
+
pubRecord={pubRecord}
85
85
+
uri={props.publication.uri}
86
86
+
/>
87
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
82
-
postUrl={`${props.publication.href}/${postUri.rkey}`}
91
91
+
postUrl={postHref}
83
92
quotesCount={quotes}
84
93
commentsCount={comments}
85
94
tags={tags}
86
86
-
showComments={pubRecord.preferences?.showComments}
95
95
+
showComments={pubRecord?.preferences?.showComments}
87
96
share
88
97
/>
89
98
</div>