feat: add Redis caching for follower counts, implement fetch timeouts, enable Nginx gzip compression, and refactor icon management.
This commit is contained in:
+8
-7
@@ -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[]
|
||||
};
|
||||
|
||||
@@ -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
@@ -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> = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
Reference in New Issue
Block a user