From 98d2b922182e535fa3b1e814b3a50700ec3fe4a9 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 18 Jul 2026 13:37:19 +0200 Subject: [PATCH] Feature: Plugin system, site search, notifications, webhooks, caching & loading states - Plugin/module system with registry, hooks for admin nav/permissions/i18n - Example Games plugin demonstrating the plugin system - Site-wide search with Cmd+K command palette (cmdk-based) - Real-time notification system (SSE + Redis pub/sub) - Extensible webhook system with admin management UI - Caching strategy: tiered data cache (unstable_cache + Redis) - Theme import/export as JSON - Loading states (spinner/skeleton) for all public pages - Mobile responsive fixes (admin dashboard, guilds page) - Webhooks admin page under Settings hub --- src/actions/admin-theme.ts | 23 ++ src/app/(site)/apply/staff/loading.tsx | 5 + src/app/(site)/apply/team/loading.tsx | 5 + src/app/(site)/badges/loading.tsx | 5 + src/app/(site)/banned/loading.tsx | 5 + src/app/(site)/client/loading.tsx | 5 + src/app/(site)/community/loading.tsx | 5 + src/app/(site)/developers/loading.tsx | 5 + src/app/(site)/draw-badge/loading.tsx | 5 + src/app/(site)/forgot/loading.tsx | 5 + src/app/(site)/friends/loading.tsx | 5 + src/app/(site)/guilds/[id]/forum/loading.tsx | 5 + .../(site)/guilds/[id]/forum/new/loading.tsx | 5 + src/app/(site)/guilds/[id]/loading.tsx | 5 + src/app/(site)/guilds/[id]/page.tsx | 2 +- src/app/(site)/guilds/loading.tsx | 5 + src/app/(site)/help/[category]/loading.tsx | 5 + src/app/(site)/help/loading.tsx | 5 + src/app/(site)/help/tickets/loading.tsx | 5 + src/app/(site)/leaderboard/loading.tsx | 5 + src/app/(site)/login/loading.tsx | 5 + src/app/(site)/logo/loading.tsx | 5 + src/app/(site)/maintenance/loading.tsx | 5 + src/app/(site)/marketplace/loading.tsx | 5 + src/app/(site)/me/loading.tsx | 5 + src/app/(site)/messages/loading.tsx | 5 + src/app/(site)/news/[slug]/loading.tsx | 5 + src/app/(site)/photos/loading.tsx | 5 + src/app/(site)/radio/apply/loading.tsx | 5 + .../(site)/radio/contests/[id]/loading.tsx | 5 + src/app/(site)/radio/contests/loading.tsx | 5 + .../(site)/radio/giveaways/[id]/loading.tsx | 5 + src/app/(site)/radio/giveaways/loading.tsx | 5 + src/app/(site)/radio/leaderboard/loading.tsx | 5 + src/app/(site)/radio/loading.tsx | 5 + src/app/(site)/radio/requests/loading.tsx | 5 + src/app/(site)/radio/schedule/loading.tsx | 5 + src/app/(site)/radio/shouts/loading.tsx | 5 + src/app/(site)/rankings/loading.tsx | 5 + src/app/(site)/rares/[category]/loading.tsx | 5 + src/app/(site)/rares/loading.tsx | 5 + src/app/(site)/redeem/loading.tsx | 5 + src/app/(site)/register/loading.tsx | 5 + src/app/(site)/reset/loading.tsx | 5 + src/app/(site)/room/[id]/loading.tsx | 5 + src/app/(site)/settings/2fa/loading.tsx | 5 + .../(site)/settings/connections/loading.tsx | 5 + src/app/(site)/settings/loading.tsx | 5 + src/app/(site)/settings/sessions/loading.tsx | 5 + src/app/(site)/shop/loading.tsx | 5 + src/app/(site)/shop/topup/loading.tsx | 5 + src/app/(site)/staff/loading.tsx | 5 + src/app/(site)/u/[username]/loading.tsx | 5 + src/app/(site)/verify/loading.tsx | 5 + src/app/admin/games/page.tsx | 75 +++++++ src/app/admin/games/rooms/page.tsx | 80 +++++++ src/app/admin/layout.tsx | 4 +- src/app/admin/page.tsx | 2 +- src/app/admin/theme/page.tsx | 25 +++ src/app/admin/webhooks/page.tsx | 179 ++++++++++++++++ src/app/api/admin/theme/export/route.ts | 35 +++ src/app/api/admin/webhooks/route.ts | 65 ++++++ src/app/api/notifications/route.ts | 36 ++++ src/app/api/notifications/stream/route.ts | 65 ++++++ src/app/api/search/route.ts | 45 ++++ src/components/navigation.tsx | 7 +- src/components/notification-bell.tsx | 173 +++++++++++++++ src/components/public/ui.tsx | 23 ++ src/components/search-command.tsx | 201 ++++++++++++++++++ src/components/top-header.tsx | 2 + src/i18n/request.ts | 1 + src/lib/admin-nav.ts | 46 +++- src/lib/data-cache.ts | 76 +++++++ src/lib/notifications/service.ts | 85 ++++++++ src/lib/notifications/types.ts | 19 ++ src/lib/permissions.ts | 6 + src/lib/plugins/index.ts | 12 ++ src/lib/plugins/loader.ts | 8 + src/lib/plugins/registry.ts | 63 ++++++ src/lib/plugins/types.ts | 70 ++++++ src/lib/webhooks/service.ts | 101 +++++++++ src/lib/webhooks/types.ts | 31 +++ src/messages/en.json | 2 + src/plugins/games/index.ts | 45 ++++ src/plugins/games/messages/en.json | 29 +++ 85 files changed, 1889 insertions(+), 7 deletions(-) create mode 100644 src/app/(site)/apply/staff/loading.tsx create mode 100644 src/app/(site)/apply/team/loading.tsx create mode 100644 src/app/(site)/badges/loading.tsx create mode 100644 src/app/(site)/banned/loading.tsx create mode 100644 src/app/(site)/client/loading.tsx create mode 100644 src/app/(site)/community/loading.tsx create mode 100644 src/app/(site)/developers/loading.tsx create mode 100644 src/app/(site)/draw-badge/loading.tsx create mode 100644 src/app/(site)/forgot/loading.tsx create mode 100644 src/app/(site)/friends/loading.tsx create mode 100644 src/app/(site)/guilds/[id]/forum/loading.tsx create mode 100644 src/app/(site)/guilds/[id]/forum/new/loading.tsx create mode 100644 src/app/(site)/guilds/[id]/loading.tsx create mode 100644 src/app/(site)/guilds/loading.tsx create mode 100644 src/app/(site)/help/[category]/loading.tsx create mode 100644 src/app/(site)/help/loading.tsx create mode 100644 src/app/(site)/help/tickets/loading.tsx create mode 100644 src/app/(site)/leaderboard/loading.tsx create mode 100644 src/app/(site)/login/loading.tsx create mode 100644 src/app/(site)/logo/loading.tsx create mode 100644 src/app/(site)/maintenance/loading.tsx create mode 100644 src/app/(site)/marketplace/loading.tsx create mode 100644 src/app/(site)/me/loading.tsx create mode 100644 src/app/(site)/messages/loading.tsx create mode 100644 src/app/(site)/news/[slug]/loading.tsx create mode 100644 src/app/(site)/photos/loading.tsx create mode 100644 src/app/(site)/radio/apply/loading.tsx create mode 100644 src/app/(site)/radio/contests/[id]/loading.tsx create mode 100644 src/app/(site)/radio/contests/loading.tsx create mode 100644 src/app/(site)/radio/giveaways/[id]/loading.tsx create mode 100644 src/app/(site)/radio/giveaways/loading.tsx create mode 100644 src/app/(site)/radio/leaderboard/loading.tsx create mode 100644 src/app/(site)/radio/loading.tsx create mode 100644 src/app/(site)/radio/requests/loading.tsx create mode 100644 src/app/(site)/radio/schedule/loading.tsx create mode 100644 src/app/(site)/radio/shouts/loading.tsx create mode 100644 src/app/(site)/rankings/loading.tsx create mode 100644 src/app/(site)/rares/[category]/loading.tsx create mode 100644 src/app/(site)/rares/loading.tsx create mode 100644 src/app/(site)/redeem/loading.tsx create mode 100644 src/app/(site)/register/loading.tsx create mode 100644 src/app/(site)/reset/loading.tsx create mode 100644 src/app/(site)/room/[id]/loading.tsx create mode 100644 src/app/(site)/settings/2fa/loading.tsx create mode 100644 src/app/(site)/settings/connections/loading.tsx create mode 100644 src/app/(site)/settings/loading.tsx create mode 100644 src/app/(site)/settings/sessions/loading.tsx create mode 100644 src/app/(site)/shop/loading.tsx create mode 100644 src/app/(site)/shop/topup/loading.tsx create mode 100644 src/app/(site)/staff/loading.tsx create mode 100644 src/app/(site)/u/[username]/loading.tsx create mode 100644 src/app/(site)/verify/loading.tsx create mode 100644 src/app/admin/games/page.tsx create mode 100644 src/app/admin/games/rooms/page.tsx create mode 100644 src/app/admin/webhooks/page.tsx create mode 100644 src/app/api/admin/theme/export/route.ts create mode 100644 src/app/api/admin/webhooks/route.ts create mode 100644 src/app/api/notifications/route.ts create mode 100644 src/app/api/notifications/stream/route.ts create mode 100644 src/app/api/search/route.ts create mode 100644 src/components/notification-bell.tsx create mode 100644 src/components/search-command.tsx create mode 100644 src/lib/data-cache.ts create mode 100644 src/lib/notifications/service.ts create mode 100644 src/lib/notifications/types.ts create mode 100644 src/lib/plugins/index.ts create mode 100644 src/lib/plugins/loader.ts create mode 100644 src/lib/plugins/registry.ts create mode 100644 src/lib/plugins/types.ts create mode 100644 src/lib/webhooks/service.ts create mode 100644 src/lib/webhooks/types.ts create mode 100644 src/plugins/games/index.ts create mode 100644 src/plugins/games/messages/en.json diff --git a/src/actions/admin-theme.ts b/src/actions/admin-theme.ts index 20e7f047..0a292661 100644 --- a/src/actions/admin-theme.ts +++ b/src/actions/admin-theme.ts @@ -211,3 +211,26 @@ export async function deleteCustomTheme(formData: FormData): Promise { } redirect("/admin/theme?deletedTheme=1"); } + +export async function importThemeJson(formData: FormData): Promise { + await requirePermission(PERMS.SETTINGS_EDIT); + const raw = String(formData.get("theme_json") ?? "").trim(); + if (!raw) redirect("/admin/theme?error=empty"); + try { + const parsed = JSON.parse(raw); + const settings = parsed.settings; + if (!settings || typeof settings !== "object") { + redirect("/admin/theme?error=invalid"); + } + for (const [key, value] of Object.entries(settings)) { + if (typeof value === "string") { + await writeSetting(key, value); + } + } + await siteSettings.reload(); + revalidatePath("/admin/theme"); + } catch { + redirect("/admin/theme?error=parse"); + } + redirect("/admin/theme?imported=1"); +} diff --git a/src/app/(site)/apply/staff/loading.tsx b/src/app/(site)/apply/staff/loading.tsx new file mode 100644 index 00000000..12837be7 --- /dev/null +++ b/src/app/(site)/apply/staff/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function StaffApplyLoading() { + return ; +} diff --git a/src/app/(site)/apply/team/loading.tsx b/src/app/(site)/apply/team/loading.tsx new file mode 100644 index 00000000..667c45ce --- /dev/null +++ b/src/app/(site)/apply/team/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function TeamApplyLoading() { + return ; +} diff --git a/src/app/(site)/badges/loading.tsx b/src/app/(site)/badges/loading.tsx new file mode 100644 index 00000000..751a89db --- /dev/null +++ b/src/app/(site)/badges/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function BadgesLoading() { + return ; +} diff --git a/src/app/(site)/banned/loading.tsx b/src/app/(site)/banned/loading.tsx new file mode 100644 index 00000000..b1e6f9a4 --- /dev/null +++ b/src/app/(site)/banned/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function BannedLoading() { + return ; +} diff --git a/src/app/(site)/client/loading.tsx b/src/app/(site)/client/loading.tsx new file mode 100644 index 00000000..47a54585 --- /dev/null +++ b/src/app/(site)/client/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function ClientLoading() { + return ; +} diff --git a/src/app/(site)/community/loading.tsx b/src/app/(site)/community/loading.tsx new file mode 100644 index 00000000..e97e1bdc --- /dev/null +++ b/src/app/(site)/community/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function CommunityLoading() { + return ; +} diff --git a/src/app/(site)/developers/loading.tsx b/src/app/(site)/developers/loading.tsx new file mode 100644 index 00000000..a6489c2a --- /dev/null +++ b/src/app/(site)/developers/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function DevelopersLoading() { + return ; +} diff --git a/src/app/(site)/draw-badge/loading.tsx b/src/app/(site)/draw-badge/loading.tsx new file mode 100644 index 00000000..52f98718 --- /dev/null +++ b/src/app/(site)/draw-badge/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function DrawBadgeLoading() { + return ; +} diff --git a/src/app/(site)/forgot/loading.tsx b/src/app/(site)/forgot/loading.tsx new file mode 100644 index 00000000..c13d4b2a --- /dev/null +++ b/src/app/(site)/forgot/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function ForgotLoading() { + return ; +} diff --git a/src/app/(site)/friends/loading.tsx b/src/app/(site)/friends/loading.tsx new file mode 100644 index 00000000..f900b00a --- /dev/null +++ b/src/app/(site)/friends/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function FriendsLoading() { + return ; +} diff --git a/src/app/(site)/guilds/[id]/forum/loading.tsx b/src/app/(site)/guilds/[id]/forum/loading.tsx new file mode 100644 index 00000000..57587ee4 --- /dev/null +++ b/src/app/(site)/guilds/[id]/forum/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function GuildForumLoading() { + return ; +} diff --git a/src/app/(site)/guilds/[id]/forum/new/loading.tsx b/src/app/(site)/guilds/[id]/forum/new/loading.tsx new file mode 100644 index 00000000..5aaf311e --- /dev/null +++ b/src/app/(site)/guilds/[id]/forum/new/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function NewForumThreadLoading() { + return ; +} diff --git a/src/app/(site)/guilds/[id]/loading.tsx b/src/app/(site)/guilds/[id]/loading.tsx new file mode 100644 index 00000000..51ad191e --- /dev/null +++ b/src/app/(site)/guilds/[id]/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function GuildDetailLoading() { + return ; +} diff --git a/src/app/(site)/guilds/[id]/page.tsx b/src/app/(site)/guilds/[id]/page.tsx index aadbd2dd..9ccbaecc 100644 --- a/src/app/(site)/guilds/[id]/page.tsx +++ b/src/app/(site)/guilds/[id]/page.tsx @@ -133,7 +133,7 @@ export default async function GuildPage({
{guild.badge ? ( // eslint-disable-next-line @next/next/no-img-element diff --git a/src/app/(site)/guilds/loading.tsx b/src/app/(site)/guilds/loading.tsx new file mode 100644 index 00000000..b00d913c --- /dev/null +++ b/src/app/(site)/guilds/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function GuildsLoading() { + return ; +} diff --git a/src/app/(site)/help/[category]/loading.tsx b/src/app/(site)/help/[category]/loading.tsx new file mode 100644 index 00000000..9bf6fd92 --- /dev/null +++ b/src/app/(site)/help/[category]/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function HelpCategoryLoading() { + return ; +} diff --git a/src/app/(site)/help/loading.tsx b/src/app/(site)/help/loading.tsx new file mode 100644 index 00000000..af8963c5 --- /dev/null +++ b/src/app/(site)/help/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function HelpLoading() { + return ; +} diff --git a/src/app/(site)/help/tickets/loading.tsx b/src/app/(site)/help/tickets/loading.tsx new file mode 100644 index 00000000..963c7475 --- /dev/null +++ b/src/app/(site)/help/tickets/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function HelpTicketsLoading() { + return ; +} diff --git a/src/app/(site)/leaderboard/loading.tsx b/src/app/(site)/leaderboard/loading.tsx new file mode 100644 index 00000000..33f33ea7 --- /dev/null +++ b/src/app/(site)/leaderboard/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function LeaderboardLoading() { + return ; +} diff --git a/src/app/(site)/login/loading.tsx b/src/app/(site)/login/loading.tsx new file mode 100644 index 00000000..4396e44c --- /dev/null +++ b/src/app/(site)/login/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function LoginLoading() { + return ; +} diff --git a/src/app/(site)/logo/loading.tsx b/src/app/(site)/logo/loading.tsx new file mode 100644 index 00000000..0ad09806 --- /dev/null +++ b/src/app/(site)/logo/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function LogoLoading() { + return ; +} diff --git a/src/app/(site)/maintenance/loading.tsx b/src/app/(site)/maintenance/loading.tsx new file mode 100644 index 00000000..6e6bfe2b --- /dev/null +++ b/src/app/(site)/maintenance/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function MaintenanceLoading() { + return ; +} diff --git a/src/app/(site)/marketplace/loading.tsx b/src/app/(site)/marketplace/loading.tsx new file mode 100644 index 00000000..32d7b478 --- /dev/null +++ b/src/app/(site)/marketplace/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function MarketplaceLoading() { + return ; +} diff --git a/src/app/(site)/me/loading.tsx b/src/app/(site)/me/loading.tsx new file mode 100644 index 00000000..603e7af1 --- /dev/null +++ b/src/app/(site)/me/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function MeLoading() { + return ; +} diff --git a/src/app/(site)/messages/loading.tsx b/src/app/(site)/messages/loading.tsx new file mode 100644 index 00000000..db17d9c6 --- /dev/null +++ b/src/app/(site)/messages/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function MessagesLoading() { + return ; +} diff --git a/src/app/(site)/news/[slug]/loading.tsx b/src/app/(site)/news/[slug]/loading.tsx new file mode 100644 index 00000000..0552397f --- /dev/null +++ b/src/app/(site)/news/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function ArticleLoading() { + return ; +} diff --git a/src/app/(site)/photos/loading.tsx b/src/app/(site)/photos/loading.tsx new file mode 100644 index 00000000..949b03eb --- /dev/null +++ b/src/app/(site)/photos/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function PhotosLoading() { + return ; +} diff --git a/src/app/(site)/radio/apply/loading.tsx b/src/app/(site)/radio/apply/loading.tsx new file mode 100644 index 00000000..ef0d8be4 --- /dev/null +++ b/src/app/(site)/radio/apply/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function RadioApplyLoading() { + return ; +} diff --git a/src/app/(site)/radio/contests/[id]/loading.tsx b/src/app/(site)/radio/contests/[id]/loading.tsx new file mode 100644 index 00000000..13ec7bc0 --- /dev/null +++ b/src/app/(site)/radio/contests/[id]/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RadioContestDetailLoading() { + return ; +} diff --git a/src/app/(site)/radio/contests/loading.tsx b/src/app/(site)/radio/contests/loading.tsx new file mode 100644 index 00000000..1f0d5c74 --- /dev/null +++ b/src/app/(site)/radio/contests/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RadioContestsLoading() { + return ; +} diff --git a/src/app/(site)/radio/giveaways/[id]/loading.tsx b/src/app/(site)/radio/giveaways/[id]/loading.tsx new file mode 100644 index 00000000..6cbe7b52 --- /dev/null +++ b/src/app/(site)/radio/giveaways/[id]/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RadioGiveawayDetailLoading() { + return ; +} diff --git a/src/app/(site)/radio/giveaways/loading.tsx b/src/app/(site)/radio/giveaways/loading.tsx new file mode 100644 index 00000000..0f14bd26 --- /dev/null +++ b/src/app/(site)/radio/giveaways/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RadioGiveawaysLoading() { + return ; +} diff --git a/src/app/(site)/radio/leaderboard/loading.tsx b/src/app/(site)/radio/leaderboard/loading.tsx new file mode 100644 index 00000000..686e8a46 --- /dev/null +++ b/src/app/(site)/radio/leaderboard/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RadioLeaderboardLoading() { + return ; +} diff --git a/src/app/(site)/radio/loading.tsx b/src/app/(site)/radio/loading.tsx new file mode 100644 index 00000000..f8c2852d --- /dev/null +++ b/src/app/(site)/radio/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RadioLoading() { + return ; +} diff --git a/src/app/(site)/radio/requests/loading.tsx b/src/app/(site)/radio/requests/loading.tsx new file mode 100644 index 00000000..c2c6665d --- /dev/null +++ b/src/app/(site)/radio/requests/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RadioRequestsLoading() { + return ; +} diff --git a/src/app/(site)/radio/schedule/loading.tsx b/src/app/(site)/radio/schedule/loading.tsx new file mode 100644 index 00000000..920a6201 --- /dev/null +++ b/src/app/(site)/radio/schedule/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RadioScheduleLoading() { + return ; +} diff --git a/src/app/(site)/radio/shouts/loading.tsx b/src/app/(site)/radio/shouts/loading.tsx new file mode 100644 index 00000000..1f6995e8 --- /dev/null +++ b/src/app/(site)/radio/shouts/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RadioShoutsLoading() { + return ; +} diff --git a/src/app/(site)/rankings/loading.tsx b/src/app/(site)/rankings/loading.tsx new file mode 100644 index 00000000..83461c82 --- /dev/null +++ b/src/app/(site)/rankings/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RankingsLoading() { + return ; +} diff --git a/src/app/(site)/rares/[category]/loading.tsx b/src/app/(site)/rares/[category]/loading.tsx new file mode 100644 index 00000000..080dda48 --- /dev/null +++ b/src/app/(site)/rares/[category]/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RareCategoryLoading() { + return ; +} diff --git a/src/app/(site)/rares/loading.tsx b/src/app/(site)/rares/loading.tsx new file mode 100644 index 00000000..947102ec --- /dev/null +++ b/src/app/(site)/rares/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RaresLoading() { + return ; +} diff --git a/src/app/(site)/redeem/loading.tsx b/src/app/(site)/redeem/loading.tsx new file mode 100644 index 00000000..c52622a4 --- /dev/null +++ b/src/app/(site)/redeem/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RedeemLoading() { + return ; +} diff --git a/src/app/(site)/register/loading.tsx b/src/app/(site)/register/loading.tsx new file mode 100644 index 00000000..a202dfd9 --- /dev/null +++ b/src/app/(site)/register/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RegisterLoading() { + return ; +} diff --git a/src/app/(site)/reset/loading.tsx b/src/app/(site)/reset/loading.tsx new file mode 100644 index 00000000..523668ee --- /dev/null +++ b/src/app/(site)/reset/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function ResetLoading() { + return ; +} diff --git a/src/app/(site)/room/[id]/loading.tsx b/src/app/(site)/room/[id]/loading.tsx new file mode 100644 index 00000000..a2708ca4 --- /dev/null +++ b/src/app/(site)/room/[id]/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function RoomLoading() { + return ; +} diff --git a/src/app/(site)/settings/2fa/loading.tsx b/src/app/(site)/settings/2fa/loading.tsx new file mode 100644 index 00000000..ce340712 --- /dev/null +++ b/src/app/(site)/settings/2fa/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function TwoFaLoading() { + return ; +} diff --git a/src/app/(site)/settings/connections/loading.tsx b/src/app/(site)/settings/connections/loading.tsx new file mode 100644 index 00000000..d1edf204 --- /dev/null +++ b/src/app/(site)/settings/connections/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function ConnectionsLoading() { + return ; +} diff --git a/src/app/(site)/settings/loading.tsx b/src/app/(site)/settings/loading.tsx new file mode 100644 index 00000000..7ad800ee --- /dev/null +++ b/src/app/(site)/settings/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function SettingsLoading() { + return ; +} diff --git a/src/app/(site)/settings/sessions/loading.tsx b/src/app/(site)/settings/sessions/loading.tsx new file mode 100644 index 00000000..cdcd3e70 --- /dev/null +++ b/src/app/(site)/settings/sessions/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function SessionsLoading() { + return ; +} diff --git a/src/app/(site)/shop/loading.tsx b/src/app/(site)/shop/loading.tsx new file mode 100644 index 00000000..34d0d729 --- /dev/null +++ b/src/app/(site)/shop/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function ShopLoading() { + return ; +} diff --git a/src/app/(site)/shop/topup/loading.tsx b/src/app/(site)/shop/topup/loading.tsx new file mode 100644 index 00000000..a4693738 --- /dev/null +++ b/src/app/(site)/shop/topup/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function TopUpLoading() { + return ; +} diff --git a/src/app/(site)/staff/loading.tsx b/src/app/(site)/staff/loading.tsx new file mode 100644 index 00000000..cf8434c5 --- /dev/null +++ b/src/app/(site)/staff/loading.tsx @@ -0,0 +1,5 @@ +import { PageSkeleton } from "@/components/public/ui"; + +export default function StaffLoading() { + return ; +} diff --git a/src/app/(site)/u/[username]/loading.tsx b/src/app/(site)/u/[username]/loading.tsx new file mode 100644 index 00000000..3dfdd1ff --- /dev/null +++ b/src/app/(site)/u/[username]/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function UserProfileLoading() { + return ; +} diff --git a/src/app/(site)/verify/loading.tsx b/src/app/(site)/verify/loading.tsx new file mode 100644 index 00000000..f5cd337f --- /dev/null +++ b/src/app/(site)/verify/loading.tsx @@ -0,0 +1,5 @@ +import { PageSpinner } from "@/components/public/ui"; + +export default function VerifyLoading() { + return ; +} diff --git a/src/app/admin/games/page.tsx b/src/app/admin/games/page.tsx new file mode 100644 index 00000000..60c53a35 --- /dev/null +++ b/src/app/admin/games/page.tsx @@ -0,0 +1,75 @@ +import { Gamepad2, Swords, Trophy, Users } from "lucide-react"; +import { getTranslations } from "next-intl/server"; +import { prisma } from "@/lib/prisma"; + +export const dynamic = "force-dynamic"; + +export default async function GamesOverviewPage() { + const t = await getTranslations("pages.admin.games"); + + let totalScores = 0; + let totalRooms = 0; + let totalRanks = 0; + let totalChallenges = 0; + try { + [totalScores, totalRooms, totalRanks, totalChallenges] = + await Promise.all([ + prisma.gameScores.count(), + prisma.gameRooms.count(), + prisma.gameRanks.count(), + prisma.dailyChallenges.count(), + ]); + } catch {} + + const stats = [ + { + icon: Gamepad2, + label: t("totalScores"), + value: totalScores.toLocaleString(), + }, + { + icon: Swords, + label: t("activeRooms"), + value: totalRooms.toLocaleString(), + }, + { + icon: Trophy, + label: t("ranks"), + value: totalRanks.toLocaleString(), + }, + { + icon: Users, + label: t("dailyChallenges"), + value: totalChallenges.toLocaleString(), + }, + ]; + + return ( +
+ {stats.map((s) => { + const Icon = s.icon; + return ( +
+
+ +
+
+
+ {s.value} +
+
+ {s.label} +
+
+
+ ); + })} +
+ ); +} diff --git a/src/app/admin/games/rooms/page.tsx b/src/app/admin/games/rooms/page.tsx new file mode 100644 index 00000000..844a2b36 --- /dev/null +++ b/src/app/admin/games/rooms/page.tsx @@ -0,0 +1,80 @@ +import { fetchAdminList } from "@/lib/admin-list"; + +export default async function GameRoomsPage({ + params: paramsPromise, + searchParams, +}: { + params: Promise<{ locale: string }>; + searchParams: Promise>; +}) { + const result = await fetchAdminList( + { + permission: "admin.games.view", + model: "gameRoom", + searchFields: ["game"], + defaultSort: { createdAt: "desc" }, + mapRow: (r: { + id: number; + game: string; + status: string; + player1Id: number; + winnerId: number | null; + createdAt: Date | null; + }) => r, + }, + paramsPromise, + searchParams, + ); + + return ( +
+
+ + + + + + + + + + + + + {result.rows.map((r) => ( + + + + + + + + + ))} + {result.rows.length === 0 && ( + + + + )} + +
IDGameStatusPlayer 1WinnerCreated
{r.id}{r.game} + + {r.status} + + #{r.player1Id}{r.winnerId ? `#${r.winnerId}` : "—"} + {r.createdAt?.toISOString().slice(0, 16).replace("T", " ") ?? "—"} +
+ No game rooms found +
+
+
+ + Page {result.page} of {result.lastPage} ({result.total} total) + +
+
+ ); +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 1062e66b..e79ec08f 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -10,7 +10,7 @@ import { AdminTopbar } from "@/components/admin/admin-topbar"; import { LanguageSwitcher } from "@/components/language-switcher"; import { ThemeSwitcher } from "@/components/theme-switcher"; import { requireStaff } from "@/lib/admin/guard"; -import { ADMIN_NAV_GROUPS } from "@/lib/admin-nav"; +import { allNavGroups } from "@/lib/admin-nav"; import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; @@ -83,7 +83,7 @@ async function Sidebar({