From 1ac594293a15cffe8e5945d889e81e7340e18a27 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Mon, 10 Aug 2026 17:28:43 +0700 Subject: [PATCH] chore(file-path): correct /profile and /leaderboard file path --- app/form/layout.tsx | 2 +- app/leaderboard/client.tsx | 151 ++++++++++++++++ .../leaderboard/content.tsx | 170 +++++++++++++++++- app/leaderboard/layout.tsx | 11 ++ app/leaderboard/vc/page.tsx | 6 +- app/leaderboard/xp/page.tsx | 4 +- .../profile/client.tsx | 0 .../profile/content.tsx | 0 app/profile/layout.tsx | 27 +++ app/profile/page.tsx | 25 +-- components/leaderboard/channel-select.tsx | 58 ------ components/leaderboard/leaderboard-error.tsx | 33 ---- .../leaderboard/leaderboard-pagination.tsx | 93 ---------- components/leaderboard/leaderboard-search.tsx | 69 ------- components/leaderboard/leaderboard-shell.tsx | 63 ------- components/leaderboard/leaderboard-tabs.tsx | 31 ---- 16 files changed, 364 insertions(+), 379 deletions(-) create mode 100644 app/leaderboard/client.tsx rename components/leaderboard/leaderboard-board.tsx => app/leaderboard/content.tsx (53%) create mode 100644 app/leaderboard/layout.tsx rename components/profile/profile-login-card.tsx => app/profile/client.tsx (100%) rename components/profile/profile-passport.tsx => app/profile/content.tsx (100%) create mode 100644 app/profile/layout.tsx delete mode 100644 components/leaderboard/channel-select.tsx delete mode 100644 components/leaderboard/leaderboard-error.tsx delete mode 100644 components/leaderboard/leaderboard-pagination.tsx delete mode 100644 components/leaderboard/leaderboard-search.tsx delete mode 100644 components/leaderboard/leaderboard-shell.tsx delete mode 100644 components/leaderboard/leaderboard-tabs.tsx diff --git a/app/form/layout.tsx b/app/form/layout.tsx index 37a4030..4356ecb 100644 --- a/app/form/layout.tsx +++ b/app/form/layout.tsx @@ -49,7 +49,7 @@ export default async function FormLayout({ return (
- + Form diff --git a/app/leaderboard/client.tsx b/app/leaderboard/client.tsx new file mode 100644 index 0000000..9ec9109 --- /dev/null +++ b/app/leaderboard/client.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { Search, X } from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Field, FieldLabel } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import type { DiscordVoiceChannel } from "@/lib/leaderboard/discord"; + +const ALL_CHANNELS = "all"; + +export function ChannelSelect({ + channels, + selectedChannelId, + searchQuery, +}: { + channels: DiscordVoiceChannel[]; + selectedChannelId?: string; + searchQuery?: string; +}) { + const router = useRouter(); + + return ( + + ห้องเสียง + + + ); +} + +export function LeaderboardSearch({ query }: { query: string }) { + const pathname = usePathname(); + const router = useRouter(); + const searchParams = useSearchParams(); + + function navigate(nextValue: string) { + const params = new URLSearchParams(searchParams); + const trimmed = nextValue.trim(); + + params.delete("page"); + if (trimmed) { + params.set("q", trimmed); + } else { + params.delete("q"); + } + + const nextQuery = params.toString(); + router.push(nextQuery ? `${pathname}?${nextQuery}` : pathname); + } + + return ( +
{ + event.preventDefault(); + const formData = new FormData(event.currentTarget); + navigate(String(formData.get("q") ?? "")); + }} + > + + ค้นหาสมาชิก +
+ + {query && ( + + )} + +
+
+
+ ); +} + +export function LeaderboardTabs({ + active, + searchQuery, +}: { + active: "xp" | "vc"; + searchQuery: string; +}) { + const router = useRouter(); + + return ( + { + const params = new URLSearchParams(); + if (searchQuery) params.set("q", searchQuery); + const query = params.toString(); + router.push(query ? `/leaderboard/${value}?${query}` : `/leaderboard/${value}`); + }} + > + + อันดับ XP + เวลาในห้องเสียง + + + ); +} diff --git a/components/leaderboard/leaderboard-board.tsx b/app/leaderboard/content.tsx similarity index 53% rename from components/leaderboard/leaderboard-board.tsx rename to app/leaderboard/content.tsx index 0df444b..bc312d5 100644 --- a/components/leaderboard/leaderboard-board.tsx +++ b/app/leaderboard/content.tsx @@ -1,6 +1,10 @@ -import { Trophy } from "lucide-react"; -import { LeaderboardPagination } from "@/components/leaderboard/leaderboard-pagination"; +import { Fragment, type ReactNode } from "react"; +import Link from "next/link"; +import { ArrowLeft, DatabaseZap, RadioTower, Trophy } from "lucide-react"; +import { LeaderboardSearch, LeaderboardTabs } from "./client"; +import { ThemeToggle } from "@/components/theme-toggle"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -9,6 +13,15 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; import { Table, TableBody, @@ -116,6 +129,89 @@ function Podium({ data }: LeaderboardBoardProps) { ); } +function visiblePages(page: number, totalPages: number) { + const pages = new Set([1, totalPages, page - 1, page, page + 1]); + return [...pages] + .filter((value) => value >= 1 && value <= totalPages) + .sort((a, b) => a - b); +} + +function pageHref( + path: string, + page: number, + channelId?: string, + searchQuery?: string, +) { + const params = new URLSearchParams(); + if (channelId) params.set("channel", channelId); + if (searchQuery) params.set("q", searchQuery); + if (page > 1) params.set("page", String(page)); + const query = params.toString(); + return query ? `${path}?${query}` : path; +} + +function LeaderboardPagination({ + path, + page, + totalPages, + channelId, + searchQuery, +}: { + path: string; + page: number; + totalPages: number; + channelId?: string; + searchQuery?: string; +}) { + if (totalPages <= 1) return null; + const pages = visiblePages(page, totalPages); + + return ( + + + {page > 1 && ( + + + + )} + + {pages.map((value, index) => ( + + {index > 0 && value - pages[index - 1] > 1 && ( + + + + )} + + + {value} + + + + ))} + + {page < totalPages && ( + + + + )} + + + ); +} + export function LeaderboardBoard(props: LeaderboardBoardProps) { const { kind, data, channelId, searchQuery } = props; const firstRow = data.totalRows === 0 @@ -192,3 +288,73 @@ export function LeaderboardBoard(props: LeaderboardBoardProps) { ); } + +export function LeaderboardError({ source }: { source: "database" | "discord" }) { + const Icon = source === "database" ? DatabaseZap : RadioTower; + + return ( + + + + + {source === "database" + ? "โหลดข้อมูลอันดับไม่สำเร็จ" + : "โหลดรายชื่อห้อง Discord ไม่สำเร็จ"} + + + {source === "database" + ? "ฐานข้อมูลอันดับยังไม่พร้อมใช้งาน ลองเปิดหน้านี้ใหม่อีกครั้ง" + : "ยังเลือกขอบเขตห้องเสียงไม่ได้ ลองเปิดหน้านี้ใหม่เมื่อ Discord พร้อมใช้งาน"} + + + + หน้านี้ไม่มีการดึงข้อมูลซ้ำอัตโนมัติระหว่างที่ระบบขัดข้อง + + + ); +} + +type LeaderboardShellProps = { + active: "xp" | "vc"; + title: string; + description: string; + searchQuery: string; + children: ReactNode; +}; + +export function LeaderboardShell({ + active, + title, + description, + searchQuery, + children, +}: LeaderboardShellProps) { + return ( +
+ + + + + +

{title}

+
+ {description} +
+ + + + +
+ + {children} +
+ ); +} diff --git a/app/leaderboard/layout.tsx b/app/leaderboard/layout.tsx new file mode 100644 index 0000000..83754fb --- /dev/null +++ b/app/leaderboard/layout.tsx @@ -0,0 +1,11 @@ +export default function LeaderboardLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ {children} +
+ ); +} diff --git a/app/leaderboard/vc/page.tsx b/app/leaderboard/vc/page.tsx index 69cc602..9ef5f02 100644 --- a/app/leaderboard/vc/page.tsx +++ b/app/leaderboard/vc/page.tsx @@ -1,9 +1,7 @@ import type { Metadata } from "next"; import { redirect } from "next/navigation"; -import { ChannelSelect } from "@/components/leaderboard/channel-select"; -import { LeaderboardBoard } from "@/components/leaderboard/leaderboard-board"; -import { LeaderboardError } from "@/components/leaderboard/leaderboard-error"; -import { LeaderboardShell } from "@/components/leaderboard/leaderboard-shell"; +import { ChannelSelect } from "../client"; +import { LeaderboardBoard, LeaderboardError, LeaderboardShell } from "../content"; import { RealtimeRefresh } from "@/components/realtime-refresh"; import { getVoiceLeaderboardPage } from "@/lib/leaderboard/database"; import { getCurrentDiscordVoiceChannels } from "@/lib/leaderboard/discord"; diff --git a/app/leaderboard/xp/page.tsx b/app/leaderboard/xp/page.tsx index ea95e8c..7300bef 100644 --- a/app/leaderboard/xp/page.tsx +++ b/app/leaderboard/xp/page.tsx @@ -1,8 +1,6 @@ import type { Metadata } from "next"; import { redirect } from "next/navigation"; -import { LeaderboardBoard } from "@/components/leaderboard/leaderboard-board"; -import { LeaderboardError } from "@/components/leaderboard/leaderboard-error"; -import { LeaderboardShell } from "@/components/leaderboard/leaderboard-shell"; +import { LeaderboardBoard, LeaderboardError, LeaderboardShell } from "../content"; import { RealtimeRefresh } from "@/components/realtime-refresh"; import { getXpLeaderboardPage } from "@/lib/leaderboard/database"; import { normalizeLeaderboardSearch, normalizeRequestedPage } from "@/lib/leaderboard/ranking"; diff --git a/components/profile/profile-login-card.tsx b/app/profile/client.tsx similarity index 100% rename from components/profile/profile-login-card.tsx rename to app/profile/client.tsx diff --git a/components/profile/profile-passport.tsx b/app/profile/content.tsx similarity index 100% rename from components/profile/profile-passport.tsx rename to app/profile/content.tsx diff --git a/app/profile/layout.tsx b/app/profile/layout.tsx new file mode 100644 index 0000000..f7eeba4 --- /dev/null +++ b/app/profile/layout.tsx @@ -0,0 +1,27 @@ +import Link from "next/link"; +import { ArrowLeft } from "lucide-react"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { Button } from "@/components/ui/button"; + +export default function ProfileLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+
+ + {children} +
+
+ ); +} diff --git a/app/profile/page.tsx b/app/profile/page.tsx index a82572d..414f3f4 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -1,15 +1,11 @@ import type { Metadata } from "next"; -import Link from "next/link"; import { getServerSession } from "next-auth"; -import { ArrowLeft } from "lucide-react"; +import { ProfileLoginCard } from "./client"; +import { ProfilePassport } from "./content"; import { authOptions } from "@/lib/auth/auth-options"; import { getSessionDiscordId } from "@/lib/auth/auth"; import { loadGuildProfile } from "@/lib/profile/guild-profile"; -import { ProfileLoginCard } from "@/components/profile/profile-login-card"; -import { ProfilePassport } from "@/components/profile/profile-passport"; import { RealtimeRefresh } from "@/components/realtime-refresh"; -import { ThemeToggle } from "@/components/theme-toggle"; -import { Button } from "@/components/ui/button"; export const dynamic = "force-dynamic"; @@ -44,20 +40,5 @@ export default async function ProfilePage() { ); } - return ( -
-
- - {content} -
-
- ); + return content; } diff --git a/components/leaderboard/channel-select.tsx b/components/leaderboard/channel-select.tsx deleted file mode 100644 index 4506da4..0000000 --- a/components/leaderboard/channel-select.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Field, FieldLabel } from "@/components/ui/field"; -import type { DiscordVoiceChannel } from "@/lib/leaderboard/discord"; - -const ALL_CHANNELS = "all"; - -export function ChannelSelect({ - channels, - selectedChannelId, - searchQuery, -}: { - channels: DiscordVoiceChannel[]; - selectedChannelId?: string; - searchQuery?: string; -}) { - const router = useRouter(); - - return ( - - ห้องเสียง - - - ); -} diff --git a/components/leaderboard/leaderboard-error.tsx b/components/leaderboard/leaderboard-error.tsx deleted file mode 100644 index 031149b..0000000 --- a/components/leaderboard/leaderboard-error.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { DatabaseZap, RadioTower } from "lucide-react"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; - -export function LeaderboardError({ source }: { source: "database" | "discord" }) { - const Icon = source === "database" ? DatabaseZap : RadioTower; - - return ( - - - - - {source === "database" - ? "โหลดข้อมูลอันดับไม่สำเร็จ" - : "โหลดรายชื่อห้อง Discord ไม่สำเร็จ"} - - - {source === "database" - ? "ฐานข้อมูลอันดับยังไม่พร้อมใช้งาน ลองเปิดหน้านี้ใหม่อีกครั้ง" - : "ยังเลือกขอบเขตห้องเสียงไม่ได้ ลองเปิดหน้านี้ใหม่เมื่อ Discord พร้อมใช้งาน"} - - - - หน้านี้ไม่มีการดึงข้อมูลซ้ำอัตโนมัติระหว่างที่ระบบขัดข้อง - - - ); -} diff --git a/components/leaderboard/leaderboard-pagination.tsx b/components/leaderboard/leaderboard-pagination.tsx deleted file mode 100644 index b7a5164..0000000 --- a/components/leaderboard/leaderboard-pagination.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Fragment } from "react"; -import { - Pagination, - PaginationContent, - PaginationEllipsis, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, -} from "@/components/ui/pagination"; - -function visiblePages(page: number, totalPages: number) { - const pages = new Set([1, totalPages, page - 1, page, page + 1]); - return [...pages] - .filter((value) => value >= 1 && value <= totalPages) - .sort((a, b) => a - b); -} - -function pageHref( - path: string, - page: number, - channelId?: string, - searchQuery?: string, -) { - const params = new URLSearchParams(); - if (channelId) params.set("channel", channelId); - if (searchQuery) params.set("q", searchQuery); - if (page > 1) params.set("page", String(page)); - const query = params.toString(); - return query ? `${path}?${query}` : path; -} - -export function LeaderboardPagination({ - path, - page, - totalPages, - channelId, - searchQuery, -}: { - path: string; - page: number; - totalPages: number; - channelId?: string; - searchQuery?: string; -}) { - if (totalPages <= 1) return null; - const pages = visiblePages(page, totalPages); - - return ( - - - {page > 1 && ( - - - - )} - - {pages.map((value, index) => ( - - {index > 0 && value - pages[index - 1] > 1 && ( - - - - )} - - - {value} - - - - ))} - - {page < totalPages && ( - - - - )} - - - ); -} diff --git a/components/leaderboard/leaderboard-search.tsx b/components/leaderboard/leaderboard-search.tsx deleted file mode 100644 index 3ff9da3..0000000 --- a/components/leaderboard/leaderboard-search.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import { Search, X } from "lucide-react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { Field, FieldLabel } from "@/components/ui/field"; -import { Input } from "@/components/ui/input"; - -export function LeaderboardSearch({ query }: { query: string }) { - const pathname = usePathname(); - const router = useRouter(); - const searchParams = useSearchParams(); - - function navigate(nextValue: string) { - const params = new URLSearchParams(searchParams); - const trimmed = nextValue.trim(); - - params.delete("page"); - if (trimmed) { - params.set("q", trimmed); - } else { - params.delete("q"); - } - - const nextQuery = params.toString(); - router.push(nextQuery ? `${pathname}?${nextQuery}` : pathname); - } - - return ( -
{ - event.preventDefault(); - const formData = new FormData(event.currentTarget); - navigate(String(formData.get("q") ?? "")); - }} - > - - ค้นหาสมาชิก -
- - {query && ( - - )} - -
-
-
- ); -} diff --git a/components/leaderboard/leaderboard-shell.tsx b/components/leaderboard/leaderboard-shell.tsx deleted file mode 100644 index c715da6..0000000 --- a/components/leaderboard/leaderboard-shell.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { ReactNode } from "react"; -import Link from "next/link"; -import { ArrowLeft } from "lucide-react"; -import { LeaderboardSearch } from "@/components/leaderboard/leaderboard-search"; -import { LeaderboardTabs } from "@/components/leaderboard/leaderboard-tabs"; -import { SpaceBackground } from "@/components/space-background"; -import { ThemeToggle } from "@/components/theme-toggle"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; - -type LeaderboardShellProps = { - active: "xp" | "vc"; - title: string; - description: string; - searchQuery: string; - children: ReactNode; -}; - -export function LeaderboardShell({ - active, - title, - description, - searchQuery, - children, -}: LeaderboardShellProps) { - return ( -
- -
- - - - - -

{title}

-
- {description} -
- - - - -
- - {children} -
-
- ); -} diff --git a/components/leaderboard/leaderboard-tabs.tsx b/components/leaderboard/leaderboard-tabs.tsx deleted file mode 100644 index 381532c..0000000 --- a/components/leaderboard/leaderboard-tabs.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; - -export function LeaderboardTabs({ - active, - searchQuery, -}: { - active: "xp" | "vc"; - searchQuery: string; -}) { - const router = useRouter(); - - return ( - { - const params = new URLSearchParams(); - if (searchQuery) params.set("q", searchQuery); - const query = params.toString(); - router.push(query ? `/leaderboard/${value}?${query}` : `/leaderboard/${value}`); - }} - > - - อันดับ XP - เวลาในห้องเสียง - - - ); -}