initial admin

This commit is contained in:
Dreamgineer
2025-10-15 01:03:31 -04:00 Unverified
parent ba6143f63b
commit fc97ebc35f
99 changed files with 2351 additions and 1408 deletions
+12
View File
@@ -0,0 +1,12 @@
import { DataTable } from "@/components/tantable";
import { db } from "@/lib/db";
import { cdn } from "@/lib/db/schema";
import { cdnColumns } from "./table";
export default async function CdnManagerPage() {
const files = await db
.select({ id: cdn.id, name: cdn.name, size: cdn.size })
.from(cdn)
.orderBy(cdn.name);
return <DataTable columns={cdnColumns} data={files} />;
}
+24
View File
@@ -0,0 +1,24 @@
"use client";
import type { ColumnDef } from "@tanstack/react-table";
export const cdnColumns: ColumnDef<{
id: string;
name: string | null;
size: string;
}>[] = [
{
accessorFn: (row) => row.name || `[${row.id}]`,
header: "Name",
},
{
accessorFn: (row) => b2s(Number(row.size)),
header: "Size",
},
];
const b2s = (t: number) => {
let e = (Math.log2(t) / 10) | 0;
// biome-ignore lint/suspicious/noAssignInExpressions: copied
return `${(t / 1024 ** (e = e <= 0 ? 0 : e)).toFixed(1)}${" KMGP"[e]}B`;
};
+42
View File
@@ -0,0 +1,42 @@
"use client";
import { useEffect, useState } from "react";
export function VersionCreateDialogForm() {
return <form>Todo</form>;
}
export function RandomWelcomeMessage({ list }: { list?: string[] }) {
const messages = list || [
"What are we cooking today?",
"What are we doing today?",
"Why are you here?",
"The food is ready sir.",
"Who left the artifact stats like that?",
"Don’t forget to hydrate, boss.",
"Another day, another reroll.",
"Still reviewing artifacts? Respect.",
"Let's make these builds shine.",
"You came back… impressive.",
"Processing luck… 0.3% found.",
"Someone's gotta keep this event alive.",
"New submissions incoming!",
"Welcome back, brave reviewer.",
"Did you bring Mora this time?",
"Your approval power is unmatched.",
"Remember: critique kindly, roast fairly.",
"Time to bless—or curse—some artifacts.",
"Don’t worry, no 5★ drama yet.",
"Admin mode: activated.",
];
const [message, setMessage] = useState("");
// biome-ignore lint/correctness/useExhaustiveDependencies: false
useEffect(
() => setMessage(messages[Math.floor(Math.random() * messages.length)]),
[messages.length],
);
return message;
}
+252
View File
@@ -0,0 +1,252 @@
import {
Badge,
BookUser,
Columns3Cog,
Computer,
Database,
GitGraph,
ListTree,
MoreHorizontal,
Package,
PlusCircle,
ScrollText,
Settings,
SquareUserRound,
UserRoundSearch,
} from "lucide-react";
import Link from "next/link";
import type { ReactNode } from "react";
import { SimpleTooltip } from "@/components/tooltip";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
} from "@/components/ui/sidebar";
import { NavUser } from "@/components/user";
import { db } from "@/lib/db";
import { versions } from "@/lib/db/schema";
import { VersionCreateDialogForm } from "./client";
export default async function AdminLayout({
children,
}: {
children: ReactNode;
}) {
const vers = await db
.select()
.from(versions)
.catch(() => [{ id: "..", name: "Error Fetching List." }]);
const health: { database: boolean; enka: boolean; amber: boolean } =
await fetch("http://localhost:3000/api/health").then((r) => r.json());
return (
<SidebarProvider>
<Sidebar variant="floating">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<Link href="/admin">
<SidebarMenuButton
asChild
className="data-[slot=sidebar-menu-button]:!p-1.5"
>
<span>
<Computer className="!size-5" />
<span className="text-base font-semibold">Buzzy Inc.</span>
</span>
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup className="pb-0">
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>
<div className="flex justify-between w-full">
<div className="flex gap-1 [&>svg]:size-4 [&>svg]:shrink-0">
{/* Database */}
<SimpleTooltip text="Database">
<Database
className={
health.database
? "text-emerald-400"
: "text-red-400"
}
/>
</SimpleTooltip>{" "}
{/* Enka */}
<SimpleTooltip text="Enka Network API">
<UserRoundSearch
className={
health.enka ? "text-emerald-400" : "text-red-400"
}
/>
</SimpleTooltip>{" "}
{/* Amber */}
<SimpleTooltip text="Project Amber">
<BookUser
className={
health.amber ? "text-emerald-400" : "text-red-400"
}
/>
</SimpleTooltip>
</div>
<span>
{Object.values(health).some((x) => !x)
? `${{ database: "ฐานข้อมูล", enka: "Enka ", amber: "Amber " }[Object.entries(health).find(([, v]) => !v)![0]]}มีปัญหา`
: "ปกติทุกอย่าง"}
</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Tierlist</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<Link href="/admin/tl/ver">
<SidebarMenuButton>
<GitGraph />
Versions
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
<SidebarMenuItem>
<Link href="/admin/tl/badges">
<SidebarMenuButton>
<Badge />
Badges
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
<SidebarMenuItem>
<Link href="/admin/tl/layout">
<SidebarMenuButton>
<Columns3Cog />
Layout
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Versions</SidebarGroupLabel>
<Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarGroupAction>
<MoreHorizontal />
</SidebarGroupAction>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DialogTrigger asChild>
<DropdownMenuItem>
<PlusCircle /> Create...
</DropdownMenuItem>
</DialogTrigger>
<Link href="/admin/ver/types">
<DropdownMenuItem>
<ListTree /> Types
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Version</DialogTitle>
</DialogHeader>
<VersionCreateDialogForm />
</DialogContent>
</Dialog>
<SidebarGroupContent>
<SidebarMenu>
{vers.map((v) => (
<SidebarMenuItem key={v.id}>
<Link href={`/admin/ver/${v.id}`}>
<SidebarMenuButton>{v.name}</SidebarMenuButton>
</Link>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarGroup className="mt-auto">
<SidebarGroupLabel>Global</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<Link href="/admin/char">
<SidebarMenuButton>
<SquareUserRound />
Characters
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
<SidebarMenuItem>
<Link href="/admin/cdn">
<SidebarMenuButton>
<Package />
CDN
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
<SidebarMenuItem className="mt-auto">
<Link href="/admin/log">
<SidebarMenuButton>
<ScrollText />
Audit Log
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
<SidebarMenuItem>
<Link href="/admin/settings">
<SidebarMenuButton>
<Settings />
Settings
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<NavUser />
</SidebarFooter>
</Sidebar>
<SidebarInset className="bg-transparent">{children}</SidebarInset>
</SidebarProvider>
);
}
export const dynamic = "force-dynamic";
+14
View File
@@ -0,0 +1,14 @@
import { RandomWelcomeMessage } from "./client";
export default function AdminPage() {
return (
<div className="flex h-full w-full justify-center items-center">
<div className="flex flex-col">
<span className="opacity-80 text-4xl font-bold">Welcome back!</span>
<span>
<RandomWelcomeMessage />
</span>
</div>
</div>
);
}