UX/UI/perf pass: admin tabs, theme toggle, log polish, mod search, JAR cache
- Admin page split into tabs (Server/Players/Chat/Mods/Backups/Logs) with hash + localStorage persistence; inactive tabs no longer mount. - Log viewer: level color-coding, search, level filter chips, auto-scroll toggle, copy button, visible-line count. - Installed mods list: search field + side filter (all/both/server/client) with live count; public ModList gets skeleton + empty states and search. - Theme toggle with no-flash inline init, localStorage + system preference. - Layout: full OG / Twitter metadata, title template, keywords, dual-theme themeColor, metadataBase. - lib/mods.ts: per-jar mtime+size parse cache (cold 6s -> warm ~45ms on /api/mods for the full mod list); cache eviction on mod removal. - ChatBridge polling eased 3s -> 5s with 2s stale window. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b6cf8c7cdc
commit
6c91f7fef0
10 changed files with 490 additions and 89 deletions
|
|
@ -1,8 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
type Mod = {
|
||||
modId: string;
|
||||
|
|
@ -13,44 +16,87 @@ type Mod = {
|
|||
};
|
||||
|
||||
export function ModList() {
|
||||
const { data: mods = [] } = useQuery<Mod[]>({
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const { data: mods, isLoading, isError } = useQuery<Mod[]>({
|
||||
queryKey: ["mods"],
|
||||
queryFn: () => fetch("/api/mods").then((r) => r.json()),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!mods) return [];
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return mods;
|
||||
return mods.filter(
|
||||
(m) =>
|
||||
m.displayName.toLowerCase().includes(q) ||
|
||||
m.modId.toLowerCase().includes(q) ||
|
||||
m.filename.toLowerCase().includes(q)
|
||||
);
|
||||
}, [mods, query]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="text-base">
|
||||
Installed Mods ({mods.length})
|
||||
</CardTitle>
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<CardTitle className="text-base">
|
||||
Installed Mods {mods ? `(${mods.length})` : ""}
|
||||
</CardTitle>
|
||||
{mods && mods.length > 6 && (
|
||||
<Input
|
||||
placeholder="Search mods..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="h-8 text-sm max-w-[220px]"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="">
|
||||
<ul className="max-h-[350px] sm:max-h-[400px] overflow-y-auto -mx-1">
|
||||
{mods.map((mod, i) => (
|
||||
<li
|
||||
key={mod.filename}
|
||||
className={`flex justify-between items-center px-3 py-2.5 rounded-md ${
|
||||
i % 2 === 1 ? "bg-muted/50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{mod.displayName}
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<ul className="-mx-1 space-y-1">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<li key={i} className="flex justify-between items-center px-3 py-2.5">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-3 w-12" />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : isError ? (
|
||||
<p className="text-sm text-red-300 py-4 text-center">
|
||||
Failed to load mods.
|
||||
</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-8 text-center">
|
||||
{query ? `No mods match "${query}"` : "No mods installed."}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="max-h-[350px] sm:max-h-[400px] overflow-y-auto -mx-1">
|
||||
{filtered.map((mod, i) => (
|
||||
<li
|
||||
key={mod.filename}
|
||||
className={`flex justify-between items-center px-3 py-2.5 rounded-md ${
|
||||
i % 2 === 1 ? "bg-muted/50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{mod.displayName}
|
||||
</span>
|
||||
{mod.version && (
|
||||
<Badge variant="secondary" className="text-xs px-1.5 py-0 shrink-0 hidden sm:inline-flex">
|
||||
{mod.version}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap ml-3 tabular-nums">
|
||||
{mod.size}
|
||||
</span>
|
||||
{mod.version && (
|
||||
<Badge variant="secondary" className="text-xs px-1.5 py-0 shrink-0 hidden sm:inline-flex">
|
||||
{mod.version}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap ml-3 tabular-nums">
|
||||
{mod.size}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue