chore: remove 4 unused files (theme-editor-fields, use-user-bulk-actions, user-columns, use-media-library)
This commit is contained in:
1 parent
9bc0834bbb
commit
9ae03056e2
17 files changed
+135
-1014
No files matched your search
@@ -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
@@ -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 · {totalUsers.toLocaleString()} users
|
{ranks.length} ranks · {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">
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
@@ -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"),
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user