feat: add dynamic social media follower/member counts to profile links

This commit is contained in:
2026-06-20 20:41:37 +07:00 Unverified
parent c7e431d99e
commit 45945c0b07
5 changed files with 115 additions and 1 deletions
+10 -1
View File
@@ -4,8 +4,10 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import ShaderBackground from "@/components/ShaderBackground"; import ShaderBackground from "@/components/ShaderBackground";
import { getFollowerCounts, getCountForLink, formatCount } from "@/lib/followers";
export default function Home() { export default async function Home() {
const counts = await getFollowerCounts();
const jsonLd = { const jsonLd = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "ProfilePage", "@type": "ProfilePage",
@@ -54,6 +56,8 @@ export default function Home() {
const isStringIcon = typeof item.icon === "string"; const isStringIcon = typeof item.icon === "string";
const IconComponent = !isStringIcon ? item.icon : null; const IconComponent = !isStringIcon ? item.icon : null;
const count = getCountForLink(item.name, counts);
return ( return (
<Button <Button
key={index} key={index}
@@ -82,6 +86,11 @@ export default function Home() {
{item.desc && <span className="text-xs text-zinc-500 dark:text-zinc-400">{item.desc}</span>} {item.desc && <span className="text-xs text-zinc-500 dark:text-zinc-400">{item.desc}</span>}
</div> </div>
</div> </div>
{count !== null && (
<span className="text-xs font-medium text-zinc-500 dark:text-zinc-400 shrink-0">
{formatCount(count.count)} {count.label}
</span>
)}
</div> </div>
</Link> </Link>
</Button> </Button>
+21
View File
@@ -0,0 +1,21 @@
export async function getDiscordMemberCount(): Promise<number | null> {
const token = process.env.DISCORD_BOT_TOKEN;
const guildId = process.env.DISCORD_GUILD_ID;
if (!token || !guildId) return null;
try {
const res = await fetch(
`https://discord.com/api/v10/guilds/${encodeURIComponent(guildId)}?with_counts=true`,
{
headers: { Authorization: `Bot ${token}` },
next: { revalidate: 300 },
}
);
if (!res.ok) return null;
const data = await res.json();
return data.approximate_member_count ?? null;
} catch {
return null;
}
}
+48
View File
@@ -0,0 +1,48 @@
import { getYoutubeSubscriberCount } from "./youtube";
import { getRobloxFollowerCount } from "./roblox";
import { getDiscordMemberCount } from "./discord";
export interface FollowerCounts {
youtube: number | null;
roblox: number | null;
discord: number | null;
}
export interface FollowerInfo {
count: number;
label: string;
}
export async function getFollowerCounts(): Promise<FollowerCounts> {
const [youtube, roblox, discord] = await Promise.all([
getYoutubeSubscriberCount(),
getRobloxFollowerCount(),
getDiscordMemberCount(),
]);
return { youtube, roblox, discord };
}
const followerLabels: Record<keyof FollowerCounts, string> = {
youtube: "ผู้ติดตาม",
roblox: "ผู้ติดตาม",
discord: "สมาชิก",
};
export function getCountForLink(name: string, counts: FollowerCounts): FollowerInfo | null {
const key = name.toLowerCase();
for (const [platform, label] of Object.entries(followerLabels)) {
const count = counts[platform as keyof FollowerCounts];
if (key.includes(platform) && count !== null) {
return { count, label };
}
}
return null;
}
export function formatCount(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return n.toString();
}
+17
View File
@@ -0,0 +1,17 @@
export async function getRobloxFollowerCount(): Promise<number | null> {
const userId = process.env.ROBLOX_USER_ID;
if (!userId) return null;
try {
const res = await fetch(
`https://friends.roblox.com/v1/users/${encodeURIComponent(userId)}/followers/count`,
{ next: { revalidate: 300 } }
);
if (!res.ok) return null;
const data = await res.json();
return data.count ?? null;
} catch {
return null;
}
}
+19
View File
@@ -0,0 +1,19 @@
export async function getYoutubeSubscriberCount(): Promise<number | null> {
const apiKey = process.env.YOUTUBE_API_KEY;
const channelId = process.env.YOUTUBE_CHANNEL_ID;
if (!apiKey || !channelId) return null;
try {
const res = await fetch(
`https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${encodeURIComponent(channelId)}&key=${encodeURIComponent(apiKey)}`,
{ next: { revalidate: 300 } }
);
if (!res.ok) return null;
const data = await res.json();
const count = data.items?.[0]?.statistics?.subscriberCount;
return count ? Number(count) : null;
} catch {
return null;
}
}