diff --git a/app/page.tsx b/app/page.tsx
index 8e8615c..ec36807 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -4,8 +4,10 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card";
import Image from "next/image";
import Link from "next/link";
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 = {
"@context": "https://schema.org",
"@type": "ProfilePage",
@@ -54,6 +56,8 @@ export default function Home() {
const isStringIcon = typeof item.icon === "string";
const IconComponent = !isStringIcon ? item.icon : null;
+ const count = getCountForLink(item.name, counts);
+
return (
diff --git a/lib/followers/discord.ts b/lib/followers/discord.ts
new file mode 100644
index 0000000..9423fa0
--- /dev/null
+++ b/lib/followers/discord.ts
@@ -0,0 +1,21 @@
+export async function getDiscordMemberCount(): Promise {
+ 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;
+ }
+}
diff --git a/lib/followers/index.ts b/lib/followers/index.ts
new file mode 100644
index 0000000..801bcec
--- /dev/null
+++ b/lib/followers/index.ts
@@ -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 {
+ const [youtube, roblox, discord] = await Promise.all([
+ getYoutubeSubscriberCount(),
+ getRobloxFollowerCount(),
+ getDiscordMemberCount(),
+ ]);
+
+ return { youtube, roblox, discord };
+}
+
+const followerLabels: Record = {
+ 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();
+}
+
diff --git a/lib/followers/roblox.ts b/lib/followers/roblox.ts
new file mode 100644
index 0000000..0622bec
--- /dev/null
+++ b/lib/followers/roblox.ts
@@ -0,0 +1,17 @@
+export async function getRobloxFollowerCount(): Promise {
+ 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;
+ }
+}
diff --git a/lib/followers/youtube.ts b/lib/followers/youtube.ts
new file mode 100644
index 0000000..8ecf837
--- /dev/null
+++ b/lib/followers/youtube.ts
@@ -0,0 +1,19 @@
+export async function getYoutubeSubscriberCount(): Promise {
+ 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;
+ }
+}