Skip to content
This repository was archived by the owner on Apr 24, 2026. It is now read-only.

Commit c27dbca

Browse files
authored
Merge pull request #84 from renderghost/feature/issue-72-bluesky-stats
feat: add Bluesky stats to profile header
2 parents 312bbb8 + abd8520 commit c27dbca

6 files changed

Lines changed: 85 additions & 1 deletion

File tree

src/app/[handle]/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ export default async function ProfilePage({ params }: PageProps) {
117117
country: location.country,
118118
}
119119
: undefined,
120+
followersCount: bskyProfile.data.followersCount,
121+
followsCount: bskyProfile.data.followsCount,
122+
postsCount: bskyProfile.data.postsCount,
120123
}}
121124
affiliations={affiliations}
122125
qualifications={qualifications}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const BLUESKY_BASE_URL = 'https://bsky.app/profile';
2+
3+
export const formatCount = (count?: number): string => {
4+
if (count === undefined) return '0';
5+
if (count >= 1000) {
6+
return `${(count / 1000).toFixed(1)}K`;
7+
}
8+
return count.toString();
9+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const styles = {
2+
container: 'flex items-center gap-4 leading-normal',
3+
stat: 'flex items-center gap-1',
4+
statNumber: 'font-bold hover:underline',
5+
statLabel: 'text-muted-foreground',
6+
} as const;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use client';
2+
3+
import Link from 'next/link';
4+
import { cn } from '@/lib/utils';
5+
import type { BlueskyStatsProps } from './BlueskyStats.types';
6+
import { styles } from './BlueskyStats.styles';
7+
import { BLUESKY_BASE_URL, formatCount } from './BlueskyStats.constants';
8+
9+
export default function BlueskyStats({
10+
handle,
11+
followersCount,
12+
followsCount,
13+
postsCount,
14+
}: BlueskyStatsProps) {
15+
const stats = [
16+
{
17+
label: 'followers',
18+
value: formatCount(followersCount),
19+
href: `${BLUESKY_BASE_URL}/${handle}/followers`,
20+
},
21+
{
22+
label: 'following',
23+
value: formatCount(followsCount),
24+
href: `${BLUESKY_BASE_URL}/${handle}/follows`,
25+
},
26+
{
27+
label: 'posts',
28+
value: formatCount(postsCount),
29+
href: `${BLUESKY_BASE_URL}/${handle}`,
30+
},
31+
];
32+
33+
return (
34+
<div className={cn(styles.container)}>
35+
{stats.map((stat) => (
36+
<div key={stat.label} className={cn(styles.stat)}>
37+
<Link
38+
href={stat.href}
39+
target="_blank"
40+
rel="noopener noreferrer"
41+
className={cn(styles.statNumber)}
42+
>
43+
{stat.value}
44+
</Link>
45+
<span className={cn(styles.statLabel)}>{stat.label}</span>
46+
</div>
47+
))}
48+
</div>
49+
);
50+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface BlueskyStatsProps {
2+
handle: string;
3+
followersCount?: number;
4+
followsCount?: number;
5+
postsCount?: number;
6+
}

src/components/profile/ProfileView.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import Image from 'next/image';
44
import Link from 'next/link';
55
import QRCodeButton from './QRCodeButton';
6+
import BlueskyStats from './BlueskyStats/BlueskyStats';
67
import { normalizeDOI } from '@/lib/data/doi';
78
import { formatDateUS, formatDisplayURL, formatWorkType } from '@/lib/utils';
89
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -42,6 +43,9 @@ interface ProfileViewProps {
4243
city?: string;
4344
country?: string;
4445
};
46+
followersCount?: number;
47+
followsCount?: number;
48+
postsCount?: number;
4549
};
4650
affiliations: (Affiliation & { rkey: string; uri: string })[];
4751
qualifications: (Qualification & { rkey: string; uri: string })[];
@@ -140,9 +144,15 @@ export default function ProfileView({
140144
>
141145
@{profile.handle}
142146
</a>
147+
<BlueskyStats
148+
handle={profile.handle}
149+
followersCount={profile.followersCount}
150+
followsCount={profile.followsCount}
151+
postsCount={profile.postsCount}
152+
/>
143153
{profile.location &&
144154
(profile.location.city || profile.location.country) && (
145-
<div className="flex items-center gap-1 text-sm text-foreground">
155+
<div className="flex items-center gap-1 text-foreground">
146156
<MapPin className="h-4 w-4" />
147157
<span className="leading-normal">
148158
{profile.location.city && `${profile.location.city}, `}

0 commit comments

Comments
 (0)