feat: add dynamic social media follower/member counts to profile links
This commit is contained in:
+10
-1
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user