i18n observability hub: analytics pages and log tables.

Translate analytics overview/activity/economy plus peak-hours/revenue charts, and audit/chat/commands/trades DataTables (en/nl/it). Includes prior events/polls detail i18n.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-18 21:51:55 +02:00
1 parent a25008deb5
commit bcbdca841b
13 files changed
+576 -211

No files matched your search

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
File diff suppressed because it is too large. Load diff
+23 -20
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { DataTable } from "@/components/admin/data-table"; import { DataTable } from "@/components/admin/data-table";
import { formatTimestamp } from "@/lib/admin-helpers"; import { formatTimestamp } from "@/lib/admin-helpers";
import type { DataTableColumn, PaginatedResult } from "@/types"; import type { DataTableColumn, PaginatedResult } from "@/types";
@@ -12,35 +13,37 @@ interface ChatLogRow {
roomId: number; roomId: number;
} }
const columns: DataTableColumn<ChatLogRow>[] = [
{ key: "id", label: "ID", sortable: true },
{ key: "username", label: "User" },
{
key: "message",
label: "Message",
render: (value) => (
<span className="max-w-[300px] truncate block">{String(value)}</span>
),
},
{
key: "timestamp",
label: "Time",
sortable: true,
render: (value) => <span>{formatTimestamp(Number(value))}</span>,
},
{ key: "roomId", label: "Room ID", sortable: true },
];
interface ChatTableProps { interface ChatTableProps {
data: PaginatedResult<ChatLogRow>; data: PaginatedResult<ChatLogRow>;
} }
export function ChatTable({ data }: ChatTableProps) { export function ChatTable({ data }: ChatTableProps) {
const t = useTranslations("pages.admin.logs");
const columns: DataTableColumn<ChatLogRow>[] = [
{ key: "id", label: t("colId"), sortable: true },
{ key: "username", label: t("colUser") },
{
key: "message",
label: t("chat.colMessage"),
render: (value) => (
<span className="max-w-[300px] truncate block">{String(value)}</span>
),
},
{
key: "timestamp",
label: t("colTime"),
sortable: true,
render: (value) => <span>{formatTimestamp(Number(value))}</span>,
},
{ key: "roomId", label: t("chat.colRoomId"), sortable: true },
];
return ( return (
<DataTable <DataTable
data={data} data={data}
columns={columns} columns={columns}
searchPlaceholder="Search by username or message..." searchPlaceholder={t("chat.searchPlaceholder")}
exportUrl="/api/admin/export?type=chatlogs" exportUrl="/api/admin/export?type=chatlogs"
/> />
); );
+25 -22
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { DataTable } from "@/components/admin/data-table"; import { DataTable } from "@/components/admin/data-table";
import { formatTimestamp } from "@/lib/admin-helpers"; import { formatTimestamp } from "@/lib/admin-helpers";
import type { DataTableColumn, PaginatedResult } from "@/types"; import type { DataTableColumn, PaginatedResult } from "@/types";
@@ -12,37 +13,39 @@ interface CommandLogRow {
timestamp: number; timestamp: number;
} }
const columns: DataTableColumn<CommandLogRow>[] = [
{ key: "id", label: "ID", sortable: true },
{ key: "username", label: "User" },
{ key: "command", label: "Command", sortable: true },
{
key: "params",
label: "Params",
render: (value) => (
<span className="max-w-[200px] truncate block font-mono text-sm">
{String(value) || "-"}
</span>
),
},
{
key: "timestamp",
label: "Time",
sortable: true,
render: (value) => <span>{formatTimestamp(Number(value))}</span>,
},
];
interface CommandsTableProps { interface CommandsTableProps {
data: PaginatedResult<CommandLogRow>; data: PaginatedResult<CommandLogRow>;
} }
export function CommandsTable({ data }: CommandsTableProps) { export function CommandsTable({ data }: CommandsTableProps) {
const t = useTranslations("pages.admin.logs");
const columns: DataTableColumn<CommandLogRow>[] = [
{ key: "id", label: t("colId"), sortable: true },
{ key: "username", label: t("colUser") },
{ key: "command", label: t("commands.colCommand"), sortable: true },
{
key: "params",
label: t("commands.colParams"),
render: (value) => (
<span className="max-w-[200px] truncate block font-mono text-sm">
{String(value) || t("emptyValue")}
</span>
),
},
{
key: "timestamp",
label: t("colTime"),
sortable: true,
render: (value) => <span>{formatTimestamp(Number(value))}</span>,
},
];
return ( return (
<DataTable <DataTable
data={data} data={data}
columns={columns} columns={columns}
searchPlaceholder="Search by username, command, or params..." searchPlaceholder={t("commands.searchPlaceholder")}
exportUrl="/api/admin/export?type=commands" exportUrl="/api/admin/export?type=commands"
/> />
); );
+1 -1
View File
@@ -88,7 +88,7 @@ export default async function AdminStaffActivities({
when: fromDate(a.createdAt), when: fromDate(a.createdAt),
staff: t("staffWithId", { staff: t("staffWithId", {
id: Number(a.userId), id: Number(a.userId),
name: nameById.get(Number(a.userId)) ?? "unknown", name: nameById.get(Number(a.userId)) ?? t("unknownUser"),
}), }),
action: a.action, action: a.action,
description: a.description, description: a.description,
+33 -28
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
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 { formatTimestamp } from "@/lib/admin-helpers"; import { formatTimestamp } from "@/lib/admin-helpers";
@@ -13,43 +14,47 @@ interface TradeLogRow {
timestamp: number; timestamp: number;
} }
const columns: DataTableColumn<TradeLogRow>[] = [
{ key: "id", label: "ID", sortable: true },
{ key: "userOneName", label: "User One" },
{ key: "userTwoName", label: "User Two" },
{
key: "status",
label: "Status",
sortable: true,
render: (value) => {
const v = String(value);
const variant =
v === "completed"
? "default"
: v === "cancelled"
? "destructive"
: "secondary";
return <Badge variant={variant}>{v || "unknown"}</Badge>;
},
},
{
key: "timestamp",
label: "Time",
sortable: true,
render: (value) => <span>{formatTimestamp(Number(value))}</span>,
},
];
interface TradesTableProps { interface TradesTableProps {
data: PaginatedResult<TradeLogRow>; data: PaginatedResult<TradeLogRow>;
} }
export function TradesTable({ data }: TradesTableProps) { export function TradesTable({ data }: TradesTableProps) {
const t = useTranslations("pages.admin.logs");
const columns: DataTableColumn<TradeLogRow>[] = [
{ key: "id", label: t("colId"), sortable: true },
{ key: "userOneName", label: t("trades.colUserOne") },
{ key: "userTwoName", label: t("trades.colUserTwo") },
{
key: "status",
label: t("trades.colStatus"),
sortable: true,
render: (value) => {
const v = String(value);
const variant =
v === "completed"
? "default"
: v === "cancelled"
? "destructive"
: "secondary";
return (
<Badge variant={variant}>{v || t("trades.statusUnknown")}</Badge>
);
},
},
{
key: "timestamp",
label: t("colTime"),
sortable: true,
render: (value) => <span>{formatTimestamp(Number(value))}</span>,
},
];
return ( return (
<DataTable <DataTable
data={data} data={data}
columns={columns} columns={columns}
searchPlaceholder="Search by username..." searchPlaceholder={t("trades.searchPlaceholder")}
/> />
); );
} }
+16 -10
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Tooltip, Tooltip,
@@ -14,7 +15,7 @@ interface PeakHoursData {
max: number; max: number;
} }
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; const DAY_KEYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const;
const HOURS = Array.from({ length: 24 }, (_, i) => i); const HOURS = Array.from({ length: 24 }, (_, i) => i);
function getColor(value: number, max: number) { function getColor(value: number, max: number) {
@@ -28,13 +29,14 @@ function getColor(value: number, max: number) {
} }
export function PeakHoursHeatmap({ data }: { data: PeakHoursData }) { export function PeakHoursHeatmap({ data }: { data: PeakHoursData }) {
const t = useTranslations("pages.admin.analytics.peakHours");
const days = DAY_KEYS.map((key) => t(`days.${key}`));
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Peak Hours</CardTitle> <CardTitle className="text-sm font-medium">{t("title")}</CardTitle>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t("description")}</p>
User activity by day &amp; hour (last 7 days)
</p>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -51,8 +53,8 @@ export function PeakHoursHeatmap({ data }: { data: PeakHoursData }) {
))} ))}
</div> </div>
{/* Rows */} {/* Rows */}
{DAYS.map((day, dayIdx) => ( {days.map((day, dayIdx) => (
<div key={day} className="flex items-center gap-px mb-px"> <div key={DAY_KEYS[dayIdx]} className="flex items-center gap-px mb-px">
<span className="w-10 text-xs text-muted-foreground text-right pr-2 shrink-0"> <span className="w-10 text-xs text-muted-foreground text-right pr-2 shrink-0">
{day} {day}
</span> </span>
@@ -67,7 +69,7 @@ export function PeakHoursHeatmap({ data }: { data: PeakHoursData }) {
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" className="text-xs"> <TooltipContent side="top" className="text-xs">
{day} {hour}:00 — {count} users {t("tooltip", { day, hour, count })}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
); );
@@ -77,13 +79,17 @@ export function PeakHoursHeatmap({ data }: { data: PeakHoursData }) {
))} ))}
{/* Legend */} {/* Legend */}
<div className="flex items-center gap-2 mt-3 ml-10"> <div className="flex items-center gap-2 mt-3 ml-10">
<span className="text-[10px] text-muted-foreground">Less</span> <span className="text-[10px] text-muted-foreground">
{t("less")}
</span>
<div className="h-3 w-3 rounded-sm bg-muted" /> <div className="h-3 w-3 rounded-sm bg-muted" />
<div className="h-3 w-3 rounded-sm bg-[var(--admin-success)]/20" /> <div className="h-3 w-3 rounded-sm bg-[var(--admin-success)]/20" />
<div className="h-3 w-3 rounded-sm bg-[var(--admin-success)]/55" /> <div className="h-3 w-3 rounded-sm bg-[var(--admin-success)]/55" />
<div className="h-3 w-3 rounded-sm bg-[var(--admin-success)] dark:bg-[var(--admin-success)]" /> <div className="h-3 w-3 rounded-sm bg-[var(--admin-success)] dark:bg-[var(--admin-success)]" />
<div className="h-3 w-3 rounded-sm bg-[var(--admin-success-strong,var(--admin-success))]" /> <div className="h-3 w-3 rounded-sm bg-[var(--admin-success-strong,var(--admin-success))]" />
<span className="text-[10px] text-muted-foreground">More</span> <span className="text-[10px] text-muted-foreground">
{t("more")}
</span>
</div> </div>
</div> </div>
</div> </div>
+7 -3
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface RevenueDataPoint { interface RevenueDataPoint {
@@ -9,6 +10,7 @@ interface RevenueDataPoint {
} }
export function RevenueChart({ data }: { data: RevenueDataPoint[] }) { export function RevenueChart({ data }: { data: RevenueDataPoint[] }) {
const t = useTranslations("pages.admin.analytics.economy");
const max = Math.max( const max = Math.max(
1, 1,
...data.flatMap((point) => [point.credits, point.points]), ...data.flatMap((point) => [point.credits, point.points]),
@@ -16,7 +18,9 @@ export function RevenueChart({ data }: { data: RevenueDataPoint[] }) {
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Catalog Spending</CardTitle> <CardTitle className="text-sm font-medium">
{t("catalogSpendingTitle")}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-3"> <div className="space-y-3">
@@ -32,14 +36,14 @@ export function RevenueChart({ data }: { data: RevenueDataPoint[] }) {
style={{ style={{
width: `${Math.max(2, (point.credits / max) * 100)}%`, width: `${Math.max(2, (point.credits / max) * 100)}%`,
}} }}
title={`${point.credits} credits`} title={t("creditsTooltip", { count: point.credits })}
/> />
<div <div
className="h-2 rounded bg-[var(--admin-accent)]" className="h-2 rounded bg-[var(--admin-accent)]"
style={{ style={{
width: `${Math.max(2, (point.points / max) * 100)}%`, width: `${Math.max(2, (point.points / max) * 100)}%`,
}} }}
title={`${point.points} points`} title={t("pointsTooltip", { count: point.points })}
/> />
</div> </div>
</div> </div>
+104 -1
View File
File diff suppressed because it is too large. Load diff
+104 -1
View File
File diff suppressed because it is too large. Load diff
+104 -1
View File
File diff suppressed because it is too large. Load diff