chore: remove 4 unused files (theme-editor-fields, use-user-bulk-actions, user-columns, use-media-library)
CI / check (push) Successful in 1m42s
CI / deploy (push) Successful in 1m36s
CI / e2e (push) Successful in 21s

This commit is contained in:
openhands committed 2026-09-05 23:27:02 +02:00
1 parent 9bc0834bbb
commit 9ae03056e2
17 files changed
+135 -1014

No files matched your search

+2 -1
View File
@@ -4,6 +4,7 @@ import { and, eq, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, WebsiteShopVouchers, WebsiteUsedShopVouchers } from "@/lib/db"; import { db, WebsiteShopVouchers, WebsiteUsedShopVouchers } from "@/lib/db";
import { formatNumber } from "@/lib/format-date";
import { clientIp, rateLimit } from "@/lib/rate-limit"; import { clientIp, rateLimit } from "@/lib/rate-limit";
import { rcon } from "@/lib/services/rcon"; import { rcon } from "@/lib/services/rcon";
import { currencyDb, sendCurrency } from "@/lib/services/send-currency"; import { currencyDb, sendCurrency } from "@/lib/services/send-currency";
@@ -175,6 +176,6 @@ export async function redeem(
return { return {
ok: true, ok: true,
message: `Success! Your balance has been increased by ${voucher.amount.toLocaleString()} credits.`, message: `Success! Your balance has been increased by ${formatNumber(voucher.amount)} credits.`,
}; };
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from "react";
import { DataTable } from "@/components/admin/data-table"; import { DataTable } from "@/components/admin/data-table";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { formatDate } from "@/lib/format-date";
import { getAvatarUrl } from "@/lib/imager"; import { getAvatarUrl } from "@/lib/imager";
import type { DataTableColumn, PaginatedResult } from "@/types/common"; import type { DataTableColumn, PaginatedResult } from "@/types/common";
@@ -123,7 +124,7 @@ export function OnlineTable({ data }: OnlineTableProps) {
</Badge> </Badge>
)} )}
<span className="text-xs text-muted-foreground ml-auto"> <span className="text-xs text-muted-foreground ml-auto">
Last updated: {lastUpdated.toLocaleTimeString()} Last updated: {formatDate(lastUpdated, "time")}
</span> </span>
</div> </div>
@@ -32,6 +32,7 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useServerAction } from "@/hooks/use-server-action"; import { useServerAction } from "@/hooks/use-server-action";
import { formatNumber } from "@/lib/format-date";
interface RankSummary { interface RankSummary {
id: number; id: number;
@@ -129,8 +130,7 @@ export function PermissionsList({
{/* Header */} {/* Header */}
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{ranks.length} ranks &middot; {totalUsers.toLocaleString()} users {ranks.length} ranks &middot; {formatNumber(totalUsers)} users total
total
</p> </p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button <Button
@@ -212,7 +212,7 @@ export function PermissionsList({
{/* User count */} {/* User count */}
<div className="flex items-center gap-1 text-xs text-muted-foreground"> <div className="flex items-center gap-1 text-xs text-muted-foreground">
<Users className="h-3 w-3" /> <Users className="h-3 w-3" />
{rank.userCount.toLocaleString()} users {formatNumber(rank.userCount)} users
</div> </div>
{/* Actions */} {/* Actions */}
File diff suppressed because it is too large. Load diff
@@ -34,6 +34,7 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useServerAction } from "@/hooks/use-server-action"; import { useServerAction } from "@/hooks/use-server-action";
import { formatNumber } from "@/lib/format-date";
interface UserQuickActionsProps { interface UserQuickActionsProps {
userId: number; userId: number;
@@ -110,7 +111,7 @@ export function UserQuickActions({
const amount = parseInt(creditAmount, 10); const amount = parseInt(creditAmount, 10);
if (!amount || amount <= 0) return; if (!amount || amount <= 0) return;
run(() => sendCredits({ userId, amount }), { run(() => sendCredits({ userId, amount }), {
successMessage: `${amount.toLocaleString()} credits sent to ${username}.`, successMessage: `${formatNumber(amount)} credits sent to ${username}.`,
}); });
} }
@@ -16,6 +16,7 @@ import Link from "@/components/link";
import { CurrencyIcon } from "@/components/shared/currency-icon"; import { CurrencyIcon } from "@/components/shared/currency-icon";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { formatNumber } from "@/lib/format-date";
import { getAvatarUrl } from "@/lib/imager"; import { getAvatarUrl } from "@/lib/imager";
import { userDetailHref, userListHref } from "../user-list-navigation"; import { userDetailHref, userListHref } from "../user-list-navigation";
@@ -164,28 +165,24 @@ export function UserHero({
<div className="flex items-center gap-1.5 text-sm"> <div className="flex items-center gap-1.5 text-sm">
<CurrencyIcon kind="credits" size={18} /> <CurrencyIcon kind="credits" size={18} />
<span className="font-semibold"> <span className="font-semibold">
{user.credits.toLocaleString()} {formatNumber(user.credits)}
</span> </span>
<span className="text-muted-foreground text-xs">credits</span> <span className="text-muted-foreground text-xs">credits</span>
</div> </div>
<div className="flex items-center gap-1.5 text-sm"> <div className="flex items-center gap-1.5 text-sm">
<CurrencyIcon kind="diamonds" size={18} /> <CurrencyIcon kind="diamonds" size={18} />
<span className="font-semibold"> <span className="font-semibold">{formatNumber(diamonds)}</span>
{diamonds.toLocaleString()}
</span>
<span className="text-muted-foreground text-xs">diamonds</span> <span className="text-muted-foreground text-xs">diamonds</span>
</div> </div>
<div className="flex items-center gap-1.5 text-sm"> <div className="flex items-center gap-1.5 text-sm">
<CurrencyIcon kind="duckets" size={18} /> <CurrencyIcon kind="duckets" size={18} />
<span className="font-semibold"> <span className="font-semibold">{formatNumber(duckets)}</span>
{duckets.toLocaleString()}
</span>
<span className="text-muted-foreground text-xs">duckets</span> <span className="text-muted-foreground text-xs">duckets</span>
</div> </div>
<div className="flex items-center gap-1.5 text-sm"> <div className="flex items-center gap-1.5 text-sm">
<CurrencyIcon kind="pixels" size={18} /> <CurrencyIcon kind="pixels" size={18} />
<span className="font-semibold"> <span className="font-semibold">
{user.pixels.toLocaleString()} {formatNumber(user.pixels)}
</span> </span>
<span className="text-muted-foreground text-xs">pixels</span> <span className="text-muted-foreground text-xs">pixels</span>
</div> </div>
@@ -204,7 +201,7 @@ export function UserHero({
</Link> </Link>
<div className="rounded-lg border bg-card px-3 py-2 text-center"> <div className="rounded-lg border bg-card px-3 py-2 text-center">
<MessageSquare className="h-4 w-4 mx-auto text-muted-foreground" /> <MessageSquare className="h-4 w-4 mx-auto text-muted-foreground" />
<p className="text-lg font-bold">{chatCount.toLocaleString()}</p> <p className="text-lg font-bold">{formatNumber(chatCount)}</p>
<p className="text-[10px] text-muted-foreground">Messages</p> <p className="text-[10px] text-muted-foreground">Messages</p>
</div> </div>
<div className="rounded-lg border bg-card px-3 py-2 text-center"> <div className="rounded-lg border bg-card px-3 py-2 text-center">
+2 -1
View File
@@ -4,6 +4,7 @@ import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell"; import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { fetchAdminList } from "@/lib/admin-list"; import { fetchAdminList } from "@/lib/admin-list";
import { db, User } from "@/lib/db"; import { db, User } from "@/lib/db";
import { formatNumber } from "@/lib/format-date";
import { canAccess, PERMS } from "@/lib/permissions"; import { canAccess, PERMS } from "@/lib/permissions";
import { CreateUserDialog } from "./_components/create-user-dialog"; import { CreateUserDialog } from "./_components/create-user-dialog";
import { UsersTable } from "./users-table"; import { UsersTable } from "./users-table";
@@ -86,7 +87,7 @@ export default async function UsersPage({
} }
> >
<p className="text-muted-foreground m-0"> <p className="text-muted-foreground m-0">
{result.total.toLocaleString()} users total {formatNumber(result.total)} users total
</p> </p>
<UsersTable <UsersTable
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -31,7 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { formatDate } from "@/lib/format-date"; import { formatDate, formatNumber } from "@/lib/format-date";
import { getAvatarUrl } from "@/lib/imager"; import { getAvatarUrl } from "@/lib/imager";
import type { DataTableColumn, PaginatedResult } from "@/types/common"; import type { DataTableColumn, PaginatedResult } from "@/types/common";
@@ -126,7 +126,7 @@ function getColumns(canEdit: boolean): DataTableColumn<UserRow>[] {
label: "Credits", label: "Credits",
sortable: true, sortable: true,
render: (value) => ( render: (value) => (
<span className="tabular-nums">{Number(value).toLocaleString()}</span> <span className="tabular-nums">{formatNumber(Number(value))}</span>
), ),
}, },
{ {
-67
View File
@@ -1,67 +0,0 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { uploadMediaAndReturn } from "@/actions/admin-media";
type MediaFile = { name: string; url: string };
export function useMediaLibrary(
open: boolean,
setOpen: (open: boolean) => void,
onSelect: (url: string) => void,
) {
const [files, setFiles] = useState<MediaFile[]>([]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
const fileRef = useRef<HTMLInputElement>(null);
const load = useCallback(async () => {
setLoading(true);
setError("");
try {
const res = await fetch("/api/media");
if (!res.ok) throw Error("Could not load media library");
const data = await res.json();
setFiles(data.files ?? []);
} catch {
setError("Could not load media library. Please retry.");
}
setLoading(false);
}, []);
useEffect(() => {
if (open) load();
}, [open, load]);
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const chosen = Array.from(e.target.files ?? []);
if (!chosen.length || uploading) return;
setUploading(true);
setError("");
const urls: string[] = [];
try {
for (const file of chosen) {
const fd = new FormData();
fd.set("file", file);
const url = await uploadMediaAndReturn(fd);
if (!url)
throw Error(
`Could not upload ${file.name}. Check file type and size.`,
);
urls.push(url);
}
await load();
onSelect(urls[urls.length - 1]);
setOpen(false);
} catch (err) {
if (urls.length) await load();
setError(
err instanceof Error ? err.message : "Upload failed. Please retry.",
);
} finally {
setUploading(false);
if (fileRef.current) fileRef.current.value = "";
}
}
return { files, loading, uploading, fileRef, error, load, handleUpload };
}
+17 -1
View File
@@ -1,7 +1,12 @@
export type DateFormatVariant = "date" | "datetime" | "datetime-seconds"; export type DateFormatVariant =
| "date"
| "time"
| "datetime"
| "datetime-seconds";
const FALLBACKS: Record<DateFormatVariant, string> = { const FALLBACKS: Record<DateFormatVariant, string> = {
date: "", date: "",
time: "—",
datetime: "—", datetime: "—",
"datetime-seconds": "—", "datetime-seconds": "—",
}; };
@@ -11,6 +16,7 @@ const FALLBACKS: Record<DateFormatVariant, string> = {
* admin/site pages no longer each redefine their own copy. * admin/site pages no longer each redefine their own copy.
* *
* - "date" → 2026-07-18 * - "date" → 2026-07-18
* - "time" → 18:30
* - "datetime" → 2026-07-18 18:30 * - "datetime" → 2026-07-18 18:30
* - "datetime-seconds"→ 2026-07-18 18:30:45 * - "datetime-seconds"→ 2026-07-18 18:30:45
* *
@@ -27,9 +33,19 @@ export function formatDate(
switch (variant) { switch (variant) {
case "date": case "date":
return iso.slice(0, 10); return iso.slice(0, 10);
case "time":
return iso.slice(11, 16);
case "datetime-seconds": case "datetime-seconds":
return iso.slice(0, 19).replace("T", " "); return iso.slice(0, 19).replace("T", " ");
default: default:
return iso.slice(0, 16).replace("T", " "); return iso.slice(0, 16).replace("T", " ");
} }
} }
/**
* Format a number with grouped thousands. Uses the provided locale or a
* consistent default for admin pages. Keeps number formatting in one place.
*/
export function formatNumber(n: number, locale: string = "en-US"): string {
return n.toLocaleString(locale);
}
@@ -25,6 +25,7 @@ const mocks = vi.hoisted(() => ({
dirs: vi.fn(), dirs: vi.fn(),
data: vi.fn(), data: vi.fn(),
settings: vi.fn(), settings: vi.fn(),
publicRoot: vi.fn(),
})); }));
vi.mock("@/lib/db", () => ({ vi.mock("@/lib/db", () => ({
db: { $client: { getConnection: async () => mocks } }, db: { $client: { getConnection: async () => mocks } },
@@ -36,6 +37,9 @@ vi.mock("@/lib/services/furni-asset-dirs", () => ({
vi.mock("@/lib/services/furni-data", () => ({ vi.mock("@/lib/services/furni-data", () => ({
getFurnitureDataPath: mocks.data, getFurnitureDataPath: mocks.data,
})); }));
vi.mock("@/lib/services/public-asset-root", () => ({
getPublicAssetRoot: mocks.publicRoot,
}));
vi.mock("@/lib/services/site-settings", () => ({ vi.mock("@/lib/services/site-settings", () => ({
siteSettings: { get: mocks.settings }, siteSettings: { get: mocks.settings },
})); }));
@@ -109,6 +113,7 @@ describe("catalog snapshot", () => {
await writeFile(path.join(root, "custom/FigureMap.json"), "{}"); await writeFile(path.join(root, "custom/FigureMap.json"), "{}");
await writeFile(path.join(root, "custom/EffectMap.json"), "{}"); await writeFile(path.join(root, "custom/EffectMap.json"), "{}");
mocks.gamedata.mockResolvedValue(root); mocks.gamedata.mockResolvedValue(root);
mocks.publicRoot.mockReturnValue(root);
mocks.dirs.mockResolvedValue({ mocks.dirs.mockResolvedValue({
nitroDir: path.join(root, "bundled/furniture"), nitroDir: path.join(root, "bundled/furniture"),
iconDir: path.join(root, "icons"), iconDir: path.join(root, "icons"),
+4 -3
View File
@@ -10,6 +10,7 @@ import {
getGamedataRoot, getGamedataRoot,
} from "@/lib/services/furni-asset-dirs"; } from "@/lib/services/furni-asset-dirs";
import { getFurnitureDataPath } from "@/lib/services/furni-data"; import { getFurnitureDataPath } from "@/lib/services/furni-data";
import { getPublicAssetRoot } from "@/lib/services/public-asset-root";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { import {
CATALOG_LANGUAGE_ROOT, CATALOG_LANGUAGE_ROOT,
@@ -59,12 +60,12 @@ export async function createCatalogSnapshot(
const furnitureData = await getFurnitureDataPath(); const furnitureData = await getFurnitureDataPath();
// Default files first; configured live locations take precedence. // Default files first; configured live locations take precedence.
await addTree( await addTree(
path.join(process.cwd(), "public/swf/c_images"), path.join(getPublicAssetRoot(), "public/swf/c_images"),
"Gamedata/c_images", "Gamedata/c_images",
/\.(png|gif|webp|jpe?g)$/i, /\.(png|gif|webp|jpe?g)$/i,
); );
await addTree( await addTree(
path.join(process.cwd(), "public/nitro-assets/bundled"), path.join(getPublicAssetRoot(), "public/nitro-assets/bundled"),
"Gamedata/bundled", "Gamedata/bundled",
/\.nitro$/i, /\.nitro$/i,
); );
@@ -94,7 +95,7 @@ export async function createCatalogSnapshot(
} }
// Pets currently write directly to the CMS directory. // Pets currently write directly to the CMS directory.
await addTree( await addTree(
path.join(process.cwd(), "public/nitro-assets/bundled/pet"), path.join(getPublicAssetRoot(), "public/nitro-assets/bundled/pet"),
"Gamedata/bundled/pet", "Gamedata/bundled/pet",
/\.nitro$/i, /\.nitro$/i,
); );