feat: add Redis caching for follower counts, implement fetch timeouts, enable Nginx gzip compression, and refactor icon management.

This commit is contained in:
2026-06-29 21:58:13 +07:00 Unverified
parent d0ce59b209
commit 14abb54108
10 changed files with 108 additions and 32 deletions
+8 -7
View File
@@ -1,10 +1,11 @@
import { Gamepad2, UserCircle, MessageSquare, LucideIcon } from "lucide-react";
import type { ComponentType, SVGProps } from "react";
import { RobloxIcon, DiscordIcon, TiktokIcon, YoutubeIcon, FormIcon } from "./icons";
export interface LinkConfig {
name: string;
link: string;
desc: string;
icon: LucideIcon | string;
icon: ComponentType<SVGProps<SVGSVGElement>>;
}
export interface ProfileConfig {
@@ -24,25 +25,25 @@ export const config = {
name: "Roblox profile",
link: "https://www.roblox.com/users/4269713791/profile",
desc: "โปรไฟล์กุเองน้อง",
icon: "/icon/roblox.webp",
icon: RobloxIcon,
},
{
name: "Discord server",
link: "https://discord.com/invite/3cZQbvW6cF",
desc: "เข้าเซิร์ฟเวอร์ในดิส",
icon: "/icon/discord.webp",
icon: DiscordIcon,
},
{
name: "Tiktok",
link: "https://www.tiktok.com/@erika.bombay007",
desc: "กดติดตามใน Tiktok",
icon: "/icon/tiktok.webp",
icon: TiktokIcon,
},
{
name: "Youtube",
link: "https://www.youtube.com/@Erika%E0%B9%80%E0%B8%AD%E0%B8%A3%E0%B8%B4%E0%B8%81%E0%B9%89%E0%B8%B2/videos",
desc: "กดติดตามใน Youtube",
icon: "/icon/youtube.webp",
icon: YoutubeIcon,
}
] as LinkConfig[]
};
@@ -54,7 +55,7 @@ export const config_p2 = {
name: "Form",
link: "/form",
desc: "Custom Form ของช่อง erika",
icon: "/icon/form.webp",
icon: FormIcon,
}
] as LinkConfig[]
};
+1
View File
@@ -9,6 +9,7 @@ export async function getDiscordMemberCount(): Promise<number | null> {
{
headers: { Authorization: `Bot ${token}` },
next: { revalidate: 3600 },
signal: AbortSignal.timeout(5000),
}
);
if (!res.ok) return null;
+25 -1
View File
@@ -15,7 +15,21 @@ export interface FollowerInfo {
label: string;
}
const CACHE_KEY = "erika:follower_counts";
const CACHE_TTL = 3600; // 1 hour
export async function getFollowerCounts(): Promise<FollowerCounts> {
try {
const { getRedisClient } = await import("@/lib/redis");
const redis = await getRedisClient();
const cached = await redis.get(CACHE_KEY);
if (cached) {
return JSON.parse(cached);
}
} catch {
// Redis unavailable, fall through to fetch
}
const [youtube, roblox, discord, tiktok] = await Promise.all([
getYoutubeSubscriberCount(),
getRobloxFollowerCount(),
@@ -23,7 +37,17 @@ export async function getFollowerCounts(): Promise<FollowerCounts> {
getTiktokFollowerCount(),
]);
return { youtube, roblox, discord, tiktok };
const counts: FollowerCounts = { youtube, roblox, discord, tiktok };
try {
const { getRedisClient } = await import("@/lib/redis");
const redis = await getRedisClient();
await redis.set(CACHE_KEY, JSON.stringify(counts), { ex: CACHE_TTL });
} catch {
// Redis unavailable, skip caching
}
return counts;
}
const followerLabels: Record<keyof FollowerCounts, string> = {
+1 -1
View File
@@ -5,7 +5,7 @@ export async function getRobloxFollowerCount(): Promise<number | null> {
try {
const res = await fetch(
`https://friends.roblox.com/v1/users/${encodeURIComponent(userId)}/followers/count`,
{ next: { revalidate: 3600 } }
{ next: { revalidate: 3600 }, signal: AbortSignal.timeout(5000) }
);
if (!res.ok) return null;
+2 -1
View File
@@ -8,7 +8,8 @@ export async function getTiktokFollowerCount(): Promise<number | null> {
`https://api.scrapecreators.com/v1/tiktok/profile?user_id=${encodeURIComponent(userId)}`,
{
headers: { 'x-api-key': apiKey },
next: { revalidate: 86400 }
next: { revalidate: 86400 },
signal: AbortSignal.timeout(5000),
}
);
if (!res.ok) return null;
+1 -1
View File
@@ -6,7 +6,7 @@ export async function getYoutubeSubscriberCount(): Promise<number | null> {
try {
const res = await fetch(
`https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${encodeURIComponent(channelId)}&key=${encodeURIComponent(apiKey)}`,
{ next: { revalidate: 3600 } }
{ next: { revalidate: 3600 }, signal: AbortSignal.timeout(5000) }
);
if (!res.ok) return null;
+37
View File
@@ -0,0 +1,37 @@
import type { ComponentType, SVGProps } from "react";
type IconProps = SVGProps<SVGSVGElement>;
export const RobloxIcon: ComponentType<IconProps> = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M5.164 0 0 18.627 18.836 24 24 5.373ZM13.474 15.898l-5.063-1.33 1.37-5.188 5.063 1.33z" />
</svg>
);
export const DiscordIcon: ComponentType<IconProps> = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
);
export const TiktokIcon: ComponentType<IconProps> = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-2.88 2.5 2.89 2.89 0 0 1-2.89-2.89 2.89 2.89 0 0 1 2.89-2.89c.28 0 .54.04.79.1v-3.5a6.37 6.37 0 0 0-.79-.05A6.34 6.34 0 0 0 3.15 15a6.34 6.34 0 0 0 6.34 6.34 6.34 6.34 0 0 0 6.34-6.34V9.11a8.16 8.16 0 0 0 4.76 1.52v-3.4c-.85 0-1.65-.2-2.37-.54h-.02z" />
</svg>
);
export const YoutubeIcon: ComponentType<IconProps> = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
);
export const FormIcon: ComponentType<IconProps> = (props) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
);