diff --git a/src/app/(site)/friends/loading.tsx b/src/app/(site)/friends/loading.tsx index 66c1d10c8a..b21a1e1fce 100644 --- a/src/app/(site)/friends/loading.tsx +++ b/src/app/(site)/friends/loading.tsx @@ -1,15 +1,5 @@ -export default function FriendsLoading() { - return ( -
-
-
- ); +import { LoadingSpinner } from "@/components/ui/loading-spinner"; + +export default function Loading() { + return ; } diff --git a/src/app/(site)/guilds/[id]/forum/loading.tsx b/src/app/(site)/guilds/[id]/forum/loading.tsx index 7baf104778..b21a1e1fce 100644 --- a/src/app/(site)/guilds/[id]/forum/loading.tsx +++ b/src/app/(site)/guilds/[id]/forum/loading.tsx @@ -1,15 +1,5 @@ -export default function GuildForumLoading() { - return ( -
-
-
- ); +import { LoadingSpinner } from "@/components/ui/loading-spinner"; + +export default function Loading() { + return ; } diff --git a/src/app/(site)/loading.tsx b/src/app/(site)/loading.tsx index c6dd80fc89..b21a1e1fce 100644 --- a/src/app/(site)/loading.tsx +++ b/src/app/(site)/loading.tsx @@ -1,15 +1,5 @@ -export default function SiteLoading() { - return ( -
-
-
- ); +import { LoadingSpinner } from "@/components/ui/loading-spinner"; + +export default function Loading() { + return ; } diff --git a/src/app/(site)/messages/loading.tsx b/src/app/(site)/messages/loading.tsx index 059885f9b9..b21a1e1fce 100644 --- a/src/app/(site)/messages/loading.tsx +++ b/src/app/(site)/messages/loading.tsx @@ -1,15 +1,5 @@ -export default function MessagesLoading() { - return ( -
-
-
- ); +import { LoadingSpinner } from "@/components/ui/loading-spinner"; + +export default function Loading() { + return ; } diff --git a/src/app/(site)/news/loading.tsx b/src/app/(site)/news/loading.tsx index 3564f37f5f..b21a1e1fce 100644 --- a/src/app/(site)/news/loading.tsx +++ b/src/app/(site)/news/loading.tsx @@ -1,15 +1,5 @@ -export default function NewsLoading() { - return ( -
-
-
- ); +import { LoadingSpinner } from "@/components/ui/loading-spinner"; + +export default function Loading() { + return ; } diff --git a/src/app/(site)/shop/loading.tsx b/src/app/(site)/shop/loading.tsx index f55a197524..b21a1e1fce 100644 --- a/src/app/(site)/shop/loading.tsx +++ b/src/app/(site)/shop/loading.tsx @@ -1,15 +1,5 @@ -export default function ShopLoading() { - return ( -
-
-
- ); +import { LoadingSpinner } from "@/components/ui/loading-spinner"; + +export default function Loading() { + return ; } diff --git a/src/app/admin/catalog/[id]/catalog-items-table/catalog-items-table.tsx b/src/app/admin/catalog/[id]/catalog-items-table/catalog-items-table.tsx index bf626d59a8..5cbe8dd045 100644 --- a/src/app/admin/catalog/[id]/catalog-items-table/catalog-items-table.tsx +++ b/src/app/admin/catalog/[id]/catalog-items-table/catalog-items-table.tsx @@ -30,7 +30,6 @@ import { RotateCcw, Rows3, Save, - Search, Trash2, X, } from "lucide-react"; @@ -48,6 +47,7 @@ import { BulkImportItems } from "@/components/admin/catalog/bulk-import-items"; import { ItemsShopPreview } from "@/components/admin/catalog/items-shop-preview"; import { QuickAddFurni } from "@/components/admin/catalog/quick-add-furni"; import { useConfirmDialog } from "@/components/admin/confirm-dialog"; +import { SearchInput } from "@/components/admin/search-input"; import { CurrencyIcon, currencyKindFromPointsType, @@ -1082,24 +1082,12 @@ export function CatalogItemsTable({ {/* Search bar */} {items.length > 0 && ( -
- - setSearchQuery(e.target.value)} - className="pl-9 h-9" - /> - {searchQuery && ( - - )} -
+ setSearchQuery("")} + /> )} diff --git a/src/app/admin/devops/errors/page.tsx b/src/app/admin/devops/errors/page.tsx index a3b490cd0e..ee4c828841 100644 --- a/src/app/admin/devops/errors/page.tsx +++ b/src/app/admin/devops/errors/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { Card, CardContent } from "@/components/ui/card"; import { db, EmulatorErrors } from "@/lib/db"; +import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; export default async function EmulatorErrorsPage({ @@ -55,7 +56,7 @@ export default async function EmulatorErrorsPage({ #{error.id} - {new Date(error.timestamp * 1000).toLocaleString()} + {formatDate(new Date(error.timestamp * 1000))}
diff --git a/src/app/admin/devops/page.tsx b/src/app/admin/devops/page.tsx
index e28e1d587b..7ba1b773ac 100644
--- a/src/app/admin/devops/page.tsx
+++ b/src/app/admin/devops/page.tsx
@@ -7,6 +7,7 @@ import { Badge } from "@/components/ui/badge";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { fetchOpsHealth } from "@/lib/admin/ops-health";
 import { db, EmulatorErrors, EmulatorSettings } from "@/lib/db";
+import { formatDate } from "@/lib/format-date";
 import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
 import { HealthCheckClient } from "./health-check-client";
 
@@ -168,7 +169,7 @@ export default async function DevOpsPage() {
 											#{error.id}
 										
 										
-											{new Date(error.timestamp * 1000).toLocaleString()}
+											{formatDate(new Date(error.timestamp * 1000))}
 										
 									
diff --git a/src/app/admin/events/[id]/event-registrations.tsx b/src/app/admin/events/[id]/event-registrations.tsx
index 63b419a825..e579afea7d 100644
--- a/src/app/admin/events/[id]/event-registrations.tsx
+++ b/src/app/admin/events/[id]/event-registrations.tsx
@@ -2,6 +2,7 @@
 
 import { useTranslations } from "next-intl";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { formatDate } from "@/lib/format-date";
 
 interface Registration {
 	id: number;
@@ -36,7 +37,7 @@ export function EventRegistrations({ registrations }: Props) {
 							>
 								{r.username}
 								
-									{new Date(r.createdAt).toLocaleString()}
+									{formatDate(new Date(r.createdAt))}
 								
 							
))} diff --git a/src/app/admin/events/[id]/event-winners.tsx b/src/app/admin/events/[id]/event-winners.tsx index 7843b81466..de2e9f3402 100644 --- a/src/app/admin/events/[id]/event-winners.tsx +++ b/src/app/admin/events/[id]/event-winners.tsx @@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useServerAction } from "@/hooks/use-server-action"; +import { formatDate } from "@/lib/format-date"; interface Winner { id: number; @@ -93,7 +94,7 @@ export function EventWinnersManager({ eventId, winners }: Props) { {w.username} - {new Date(w.createdAt).toLocaleDateString()} + {formatDate(new Date(w.createdAt), "date")}
))} diff --git a/src/app/admin/events/[id]/page.tsx b/src/app/admin/events/[id]/page.tsx index a59fca3d35..50625cfa71 100644 --- a/src/app/admin/events/[id]/page.tsx +++ b/src/app/admin/events/[id]/page.tsx @@ -6,6 +6,7 @@ import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { db, User, WebsiteEvent, WebsiteEventType } from "@/lib/db"; +import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { EventForm } from "../event-form"; import { EventPrizesManager } from "./event-prizes"; @@ -94,7 +95,7 @@ export default async function EventDetailPage({

- {event.type.name} · {new Date(event.startsAt).toLocaleString()} + {event.type.name} · {formatDate(new Date(event.startsAt))}

@@ -156,7 +157,7 @@ export default async function EventDetailPage({ {event.endsAt && (

{t("endsAt", { - date: new Date(event.endsAt).toLocaleString(), + date: formatDate(new Date(event.endsAt)), })}

)} diff --git a/src/app/admin/events/events-table.tsx b/src/app/admin/events/events-table.tsx index 19d06cc07f..fd859fc1f3 100644 --- a/src/app/admin/events/events-table.tsx +++ b/src/app/admin/events/events-table.tsx @@ -9,6 +9,7 @@ import Link from "@/components/link"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { useServerAction } from "@/hooks/use-server-action"; +import { formatDate } from "@/lib/format-date"; import type { DataTableColumn, PaginatedResult } from "@/types/common"; interface EventRow { @@ -86,7 +87,7 @@ export function EventsTable({ data }: EventsTableProps) { sortable: true, render: (value) => ( - {new Date(value as unknown as string).toLocaleString()} + {formatDate(new Date(value as unknown as string))} ), }, diff --git a/src/app/admin/help-tickets/[id]/admin-help-ticket-detail.tsx b/src/app/admin/help-tickets/[id]/admin-help-ticket-detail.tsx index 29d8905426..3a7ecc6115 100644 --- a/src/app/admin/help-tickets/[id]/admin-help-ticket-detail.tsx +++ b/src/app/admin/help-tickets/[id]/admin-help-ticket-detail.tsx @@ -15,6 +15,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; import { useServerAction } from "@/hooks/use-server-action"; +import { formatDate } from "@/lib/format-date"; interface ThreadMessage { key: string; @@ -162,7 +163,7 @@ export function AdminHelpTicketDetail({ {msg.isStaff ? ` (${t("staffBadge")})` : ""} - {new Date(msg.createdAt).toLocaleString()} + {formatDate(new Date(msg.createdAt))}
{t("colCreated")} - {new Date(ticket.createdAt).toLocaleString()} + {formatDate(new Date(ticket.createdAt))}
{t("colUpdated")} - {new Date(ticket.updatedAt).toLocaleString()} + {formatDate(new Date(ticket.updatedAt))}
{t("messageCountLabel")} diff --git a/src/app/admin/import/badges/import-badges-client.tsx b/src/app/admin/import/badges/import-badges-client.tsx index 7978234ef5..7fa8b0f88a 100644 --- a/src/app/admin/import/badges/import-badges-client.tsx +++ b/src/app/admin/import/badges/import-badges-client.tsx @@ -1,7 +1,6 @@ "use client"; import { - AlertCircle, BadgeCheck, Check, CheckCircle2, @@ -15,6 +14,7 @@ import { } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import { ImportErrorBanner } from "@/components/admin/import-error-banner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -348,12 +348,7 @@ export function ImportBadgesClient() { )} {/* Error */} - {error && ( -
- - {error} -
- )} + {error && } {/* Loading skeleton */} {loading && badges.length === 0 && ( diff --git a/src/app/admin/import/clone/import-clone-client.tsx b/src/app/admin/import/clone/import-clone-client.tsx index df7f12f656..4eb3b12125 100644 --- a/src/app/admin/import/clone/import-clone-client.tsx +++ b/src/app/admin/import/clone/import-clone-client.tsx @@ -1,7 +1,6 @@ "use client"; import { - AlertCircle, Check, CloudDownload, Download, @@ -16,6 +15,7 @@ import { import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/admin/confirm-dialog"; +import { ImportErrorBanner } from "@/components/admin/import-error-banner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -782,12 +782,7 @@ function FurniGrid({ source }: FurniGridProps) { )} {/* Error */} - {error && ( -
- - {error} -
- )} + {error && } {/* Empty state */} {!loading && items.length === 0 && !error && ( diff --git a/src/app/admin/import/clothing/import-clothing-client.tsx b/src/app/admin/import/clothing/import-clothing-client.tsx index 9032485682..9714fca743 100644 --- a/src/app/admin/import/clothing/import-clothing-client.tsx +++ b/src/app/admin/import/clothing/import-clothing-client.tsx @@ -1,7 +1,6 @@ "use client"; import { - AlertCircle, Check, Download, Loader2, @@ -13,6 +12,7 @@ import { } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import { ImportErrorBanner } from "@/components/admin/import-error-banner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -361,12 +361,7 @@ function LibsView() {
)} - {error && ( -
- - {error} -
- )} + {error && } {!loading && items.length === 0 && !error && (
@@ -749,12 +744,7 @@ function SetsView() {
)} - {error && ( -
- - {error} -
- )} + {error && } {!loading && items.length === 0 && !error && (
diff --git a/src/app/admin/import/effects/import-effects-client.tsx b/src/app/admin/import/effects/import-effects-client.tsx index 4451d90239..c74108e76e 100644 --- a/src/app/admin/import/effects/import-effects-client.tsx +++ b/src/app/admin/import/effects/import-effects-client.tsx @@ -2,7 +2,6 @@ "use client"; import { - AlertCircle, Check, Download, Loader2, @@ -15,6 +14,7 @@ import { } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import { ImportErrorBanner } from "@/components/admin/import-error-banner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -314,12 +314,7 @@ export function ImportEffectsClient() { )} {/* Error */} - {error && ( -
- - {error} -
- )} + {error && } {/* Empty */} {!loading && visible.length === 0 && !error && ( diff --git a/src/app/admin/import/pets/import-pets-client.tsx b/src/app/admin/import/pets/import-pets-client.tsx index f2120caf60..22e669d0b4 100644 --- a/src/app/admin/import/pets/import-pets-client.tsx +++ b/src/app/admin/import/pets/import-pets-client.tsx @@ -1,7 +1,6 @@ "use client"; import { - AlertCircle, Check, Download, Loader2, @@ -13,6 +12,7 @@ import { } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import { ImportErrorBanner } from "@/components/admin/import-error-banner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -293,12 +293,7 @@ export function ImportPetsClient() {
)} - {error && ( -
- - {error} -
- )} + {error && } {!loading && pets.length === 0 && !error && (
diff --git a/src/app/admin/loading.tsx b/src/app/admin/loading.tsx index 2573bc13fc..2b05206fd7 100644 --- a/src/app/admin/loading.tsx +++ b/src/app/admin/loading.tsx @@ -1,15 +1,5 @@ -export default function AdminLoading() { - return ( -
-
-
- ); +import { LoadingSpinner } from "@/components/ui/loading-spinner"; + +export default function Loading() { + return ; } diff --git a/src/app/admin/logs/_lib/load-ignored-logs.ts b/src/app/admin/logs/_lib/load-ignored-logs.ts index 348985e076..daf7ae765b 100644 --- a/src/app/admin/logs/_lib/load-ignored-logs.ts +++ b/src/app/admin/logs/_lib/load-ignored-logs.ts @@ -9,6 +9,16 @@ type ListParamsInput = { defaultPerPage?: number; }; +type SqlFragment = ReturnType; + +type LogListSpec = { + select: SqlFragment; + from: SqlFragment; + searchWhere: (like: string) => SqlFragment; + orderBy: SqlFragment; + mapRow: (raw: RawRow, index: number, offset: number) => Row; +}; + async function requireLogsView() { const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.LOGS_VIEW, session.user.rank)) { @@ -26,11 +36,56 @@ function parsePageParams( return { search: parsed.search.trim(), page: parsed.page, perPage }; } -async function rawRows(query: ReturnType): Promise { +async function rawRows(query: SqlFragment): Promise { const [rows] = (await db.execute(query)) as unknown as [T[], unknown]; return rows ?? []; } +/** + * Shared list loader for the chat / command / trade log tables. The SQL + * fragments come from a per-table spec while the request handling, search + * branching, count + pagination and row mapping stay in one place. + */ +async function loadLogList( + input: ListParamsInput, + spec: LogListSpec, +): Promise<{ + rows: Row[]; + total: number; + page: number; + perPage: number; + lastPage: number; + offset: number; +}> { + await requireLogsView(); + + const rawParams = await input.searchParams; + const { search, page, perPage } = parsePageParams( + rawParams, + input.defaultPerPage ?? 50, + ); + const offset = (page - 1) * perPage; + const like = `%${search}%`; + const where = search ? sql`WHERE ${spec.searchWhere(like)}` : sql``; + + const [rawRowsResult, countRows] = await Promise.all([ + rawRows( + sql`${spec.select} ${spec.from} ${where} ${spec.orderBy} LIMIT ${perPage} OFFSET ${offset}`, + ), + rawRows<{ count: bigint }>( + sql`SELECT COUNT(*) AS count ${spec.from} ${where}`, + ), + ]); + + const total = Number(countRows[0]?.count ?? 0); + const pagination = calcPagination(total, page, perPage); + const rows = rawRowsResult.map((row, index) => + spec.mapRow(row, index, pagination.offset), + ); + + return { rows, ...pagination }; +} + export type ChatLogListRow = { id: number; username: string; @@ -39,17 +94,7 @@ export type ChatLogListRow = { roomId: number; }; -export async function loadChatLogList({ - searchParams, - defaultPerPage = 50, -}: ListParamsInput) { - await requireLogsView(); - - const rawParams = await searchParams; - const { search, page, perPage } = parsePageParams(rawParams, defaultPerPage); - const offset = (page - 1) * perPage; - const like = `%${search}%`; - +export function loadChatLogList(input: ListParamsInput) { type RawRow = { roomId: number; message: string; @@ -57,43 +102,20 @@ export async function loadChatLogList({ username: string | null; }; - const [rawRowsResult, countRows] = search - ? await Promise.all([ - rawRows(sql` - SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username - FROM chatlogs_room c - LEFT JOIN users u ON u.id = c.user_from_id - WHERE c.message LIKE ${like} OR u.username LIKE ${like} - ORDER BY c.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`), - rawRows<{ count: bigint }>(sql` - SELECT COUNT(*) AS count - FROM chatlogs_room c - LEFT JOIN users u ON u.id = c.user_from_id - WHERE c.message LIKE ${like} OR u.username LIKE ${like}`), - ]) - : await Promise.all([ - rawRows(sql` - SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username - FROM chatlogs_room c - LEFT JOIN users u ON u.id = c.user_from_id - ORDER BY c.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`), - rawRows<{ count: bigint }>(sql` - SELECT COUNT(*) AS count FROM chatlogs_room`), - ]); - - const total = Number(countRows[0]?.count ?? 0); - const pagination = calcPagination(total, page, perPage); - const rows: ChatLogListRow[] = rawRowsResult.map((row, index) => ({ - id: pagination.offset + index + 1, - username: row.username ?? "-", - message: row.message, - timestamp: row.timestamp, - roomId: row.roomId, - })); - - return { rows, ...pagination }; + return loadLogList(input, { + select: sql`SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username`, + from: sql`FROM chatlogs_room c LEFT JOIN users u ON u.id = c.user_from_id`, + searchWhere: (like) => + sql`c.message LIKE ${like} OR u.username LIKE ${like}`, + orderBy: sql`ORDER BY c.timestamp DESC`, + mapRow: (row, index, offset) => ({ + id: offset + index + 1, + username: row.username ?? "-", + message: row.message, + timestamp: row.timestamp, + roomId: row.roomId, + }), + }); } export type CommandLogListRow = { @@ -104,17 +126,7 @@ export type CommandLogListRow = { timestamp: number; }; -export async function loadCommandLogList({ - searchParams, - defaultPerPage = 50, -}: ListParamsInput) { - await requireLogsView(); - - const rawParams = await searchParams; - const { search, page, perPage } = parsePageParams(rawParams, defaultPerPage); - const offset = (page - 1) * perPage; - const like = `%${search}%`; - +export function loadCommandLogList(input: ListParamsInput) { type RawRow = { command: string; params: string; @@ -122,47 +134,20 @@ export async function loadCommandLogList({ username: string | null; }; - const [rawRowsResult, countRows] = search - ? await Promise.all([ - rawRows(sql` - SELECT c.command, c.params, c.timestamp, u.username AS username - FROM commandlogs c - LEFT JOIN users u ON u.id = c.user_id - WHERE c.command LIKE ${like} - OR c.params LIKE ${like} - OR u.username LIKE ${like} - ORDER BY c.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`), - rawRows<{ count: bigint }>(sql` - SELECT COUNT(*) AS count - FROM commandlogs c - LEFT JOIN users u ON u.id = c.user_id - WHERE c.command LIKE ${like} - OR c.params LIKE ${like} - OR u.username LIKE ${like}`), - ]) - : await Promise.all([ - rawRows(sql` - SELECT c.command, c.params, c.timestamp, u.username AS username - FROM commandlogs c - LEFT JOIN users u ON u.id = c.user_id - ORDER BY c.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`), - rawRows<{ count: bigint }>(sql` - SELECT COUNT(*) AS count FROM commandlogs`), - ]); - - const total = Number(countRows[0]?.count ?? 0); - const pagination = calcPagination(total, page, perPage); - const rows: CommandLogListRow[] = rawRowsResult.map((row, index) => ({ - id: pagination.offset + index + 1, - username: row.username ?? "-", - command: row.command, - params: row.params, - timestamp: row.timestamp, - })); - - return { rows, ...pagination }; + return loadLogList(input, { + select: sql`SELECT c.command, c.params, c.timestamp, u.username AS username`, + from: sql`FROM commandlogs c LEFT JOIN users u ON u.id = c.user_id`, + searchWhere: (like) => + sql`c.command LIKE ${like} OR c.params LIKE ${like} OR u.username LIKE ${like}`, + orderBy: sql`ORDER BY c.timestamp DESC`, + mapRow: (row, index, offset) => ({ + id: offset + index + 1, + username: row.username ?? "-", + command: row.command, + params: row.params, + timestamp: row.timestamp, + }), + }); } export type TradeLogListRow = { @@ -173,17 +158,7 @@ export type TradeLogListRow = { timestamp: number; }; -export async function loadTradeLogList({ - searchParams, - defaultPerPage = 50, -}: ListParamsInput) { - await requireLogsView(); - - const rawParams = await searchParams; - const { search, page, perPage } = parsePageParams(rawParams, defaultPerPage); - const offset = (page - 1) * perPage; - const like = `%${search}%`; - +export function loadTradeLogList(input: ListParamsInput) { type RawRow = { id: number; timestamp: number; @@ -191,47 +166,19 @@ export async function loadTradeLogList({ userTwoName: string | null; }; - const [rawRowsResult, countRows] = search - ? await Promise.all([ - rawRows(sql` - SELECT t.id, t.timestamp, - u1.username AS userOneName, u2.username AS userTwoName - FROM room_trade_log t - LEFT JOIN users u1 ON u1.id = t.user_one_id - LEFT JOIN users u2 ON u2.id = t.user_two_id - WHERE u1.username LIKE ${like} OR u2.username LIKE ${like} - ORDER BY t.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`), - rawRows<{ count: bigint }>(sql` - SELECT COUNT(*) AS count - FROM room_trade_log t - LEFT JOIN users u1 ON u1.id = t.user_one_id - LEFT JOIN users u2 ON u2.id = t.user_two_id - WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}`), - ]) - : await Promise.all([ - rawRows(sql` - SELECT t.id, t.timestamp, - u1.username AS userOneName, u2.username AS userTwoName - FROM room_trade_log t - LEFT JOIN users u1 ON u1.id = t.user_one_id - LEFT JOIN users u2 ON u2.id = t.user_two_id - ORDER BY t.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`), - rawRows<{ count: bigint }>(sql` - SELECT COUNT(*) AS count FROM room_trade_log`), - ]); - - const total = Number(countRows[0]?.count ?? 0); - const pagination = calcPagination(total, page, perPage); - const rows: TradeLogListRow[] = rawRowsResult.map((row) => ({ - id: row.id, - userOneName: row.userOneName ?? "-", - userTwoName: row.userTwoName ?? "-", - // room_trade_log only stores completed trades; no status column exists - status: "completed", - timestamp: row.timestamp, - })); - - return { rows, ...pagination }; + return loadLogList(input, { + select: sql`SELECT t.id, t.timestamp, u1.username AS userOneName, u2.username AS userTwoName`, + from: sql`FROM room_trade_log t LEFT JOIN users u1 ON u1.id = t.user_one_id LEFT JOIN users u2 ON u2.id = t.user_two_id`, + searchWhere: (like) => + sql`u1.username LIKE ${like} OR u2.username LIKE ${like}`, + orderBy: sql`ORDER BY t.timestamp DESC`, + mapRow: (row) => ({ + id: row.id, + userOneName: row.userOneName ?? "-", + userTwoName: row.userTwoName ?? "-", + // room_trade_log only stores completed trades; no status column exists + status: "completed", + timestamp: row.timestamp, + }), + }); } diff --git a/src/app/admin/moderation/cfh/[id]/cfh-detail.tsx b/src/app/admin/moderation/cfh/[id]/cfh-detail.tsx index 0e3fe3282d..0aadf1b819 100644 --- a/src/app/admin/moderation/cfh/[id]/cfh-detail.tsx +++ b/src/app/admin/moderation/cfh/[id]/cfh-detail.tsx @@ -34,6 +34,7 @@ import { } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { useServerAction } from "@/hooks/use-server-action"; +import { formatDate } from "@/lib/format-date"; const CFH_STATES: Record = { 0: { @@ -130,7 +131,7 @@ export function CfhDetail({

Call For Help Report

- {new Date(ticket.timestamp * 1000).toLocaleString()} · Room # + {formatDate(new Date(ticket.timestamp * 1000))} · Room # {ticket.roomId}

@@ -317,7 +318,7 @@ export function CfhDetail({

{ban.reason || "No reason"}

{ban.type} · by {ban.staff} ·{" "} - {new Date(ban.timestamp * 1000).toLocaleDateString()} + {formatDate(new Date(ban.timestamp * 1000), "date")}

))} @@ -431,9 +432,7 @@ export function CfhDetail({
Created - - {new Date(ticket.timestamp * 1000).toLocaleString()} - + {formatDate(new Date(ticket.timestamp * 1000))}
Room diff --git a/src/app/admin/moderation/team/page.tsx b/src/app/admin/moderation/team/page.tsx index fb84944e80..79c6f8b08f 100644 --- a/src/app/admin/moderation/team/page.tsx +++ b/src/app/admin/moderation/team/page.tsx @@ -12,6 +12,7 @@ import { User, WebsiteTicket, } from "@/lib/db"; +import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; export default async function ModTeamPage() { @@ -169,7 +170,7 @@ export default async function ModTeamPage() { {member.online === "1" ? "Online" : member.lastOnline - ? new Date(member.lastOnline * 1000).toLocaleDateString() + ? formatDate(new Date(member.lastOnline * 1000), "date") : "—"} diff --git a/src/app/admin/permissions/[id]/rank-edit-client.tsx b/src/app/admin/permissions/[id]/rank-edit-client.tsx index 125245c2fe..2fc5aa5fe6 100644 --- a/src/app/admin/permissions/[id]/rank-edit-client.tsx +++ b/src/app/admin/permissions/[id]/rank-edit-client.tsx @@ -6,7 +6,6 @@ import { Crown, Loader2, Save, - Search, Settings, Shield, ShieldCheck, @@ -19,6 +18,7 @@ import { saveRank as saveRankAction, setCmsPermissions, } from "@/actions/permissions"; +import { SearchInput } from "@/components/admin/search-input"; import Link from "@/components/link"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -521,15 +521,11 @@ export function RankEditClient({
-
- - setPermSearch(e.target.value)} - className="pl-9" - /> -
+ @@ -606,15 +602,11 @@ export function RankEditClient({ -
- - setCmsSearch(e.target.value)} - className="pl-9" - /> -
+
{filteredCms.map((perm) => { diff --git a/src/app/admin/photos/page.tsx b/src/app/admin/photos/page.tsx index 639a3273ca..e094025ee4 100644 --- a/src/app/admin/photos/page.tsx +++ b/src/app/admin/photos/page.tsx @@ -9,6 +9,7 @@ import Link from "@/components/link"; import { Button } from "@/components/ui/button"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { CameraWeb, db, User } from "@/lib/db"; +import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; export default async function AdminPhotos({ @@ -133,11 +134,7 @@ export default async function AdminPhotos({ > {t("byUser", { username })} {" "} - ·{" "} - {new Date(p.timestamp * 1000) - .toISOString() - .slice(0, 16) - .replace("T", " ")} + · {formatDate(new Date(p.timestamp * 1000))}

{canEdit ? (
( - {new Date(value as unknown as string).toLocaleDateString()} + {formatDate(new Date(value as unknown as string), "date")} ), }, diff --git a/src/app/admin/prefixes/prefixes-client.tsx b/src/app/admin/prefixes/prefixes-client.tsx index aad2e8a584..684684ae0d 100644 --- a/src/app/admin/prefixes/prefixes-client.tsx +++ b/src/app/admin/prefixes/prefixes-client.tsx @@ -24,6 +24,7 @@ import { } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useServerAction } from "@/hooks/use-server-action"; +import { formatDate } from "@/lib/format-date"; import { PrefixDialog } from "./prefix-dialog"; import { @@ -314,7 +315,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { {prefix.active ? "Yes" : "No"} {prefix.created_at - ? new Date(prefix.created_at).toLocaleDateString() + ? formatDate(new Date(prefix.created_at), "date") : "—"} {canEdit && ( diff --git a/src/app/admin/settings/advanced-settings-panel.tsx b/src/app/admin/settings/advanced-settings-panel.tsx index 8bc1822988..38116dbfb4 100644 --- a/src/app/admin/settings/advanced-settings-panel.tsx +++ b/src/app/admin/settings/advanced-settings-panel.tsx @@ -1,12 +1,13 @@ "use client"; -import { Search, Trash2 } from "lucide-react"; +import { Trash2 } from "lucide-react"; import { useMemo, useState } from "react"; import { createSetting, deleteSetting, updateSetting, } from "@/actions/admin-settings"; +import { SearchInput } from "@/components/admin/search-input"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { MANAGED_SETTING_KEYS } from "./cms-settings-config"; @@ -64,15 +65,11 @@ export function AdvancedSettingsPanel({
-
- - setQuery(e.target.value)} - placeholder="Search keys, values, comments…" - className="pl-9" - /> -
+ {canEdit ? ( -
- setVal(e.target.value)} - className="w-[42px] h-[36px] p-0 border-none bg-none cursor-pointer flex-shrink-0 rounded" - /> -
-
- - {label} - -
- - {description} - -
-
- - ); -} diff --git a/src/app/admin/theme-builder/theme-editor-tabs.tsx b/src/app/admin/theme-builder/theme-editor-tabs.tsx deleted file mode 100644 index ef5b7e17ee..0000000000 --- a/src/app/admin/theme-builder/theme-editor-tabs.tsx +++ /dev/null @@ -1,273 +0,0 @@ -"use client"; -import { BLOCK_KEY_PREFIX, getBlocksByCategory } from "@/lib/theme-blocks"; -import { ColorField } from "./color-field"; -import { - COLOR_FIELDS, - EFFECT_FIELDS, - getVal, - LAYOUT_FIELDS, - MEDIA_FIELDS, - type ScopeValues, -} from "./theme-editor-fields"; -// ─── Colors Tab ────────────────────────────────────────────────────────────── - -export function ColorsTab({ - values, - mode, -}: { - values: ScopeValues; - mode: "light" | "dark"; -}) { - return ( -
-

- Colors ({mode} mode) -

-

- Only values you set here override the parent scope. Leave empty to - inherit. -

- -
- {COLOR_FIELDS.map((field) => ( - - ))} -
-
- ); -} - -// ─── Blocks Tab ────────────────────────────────────────────────────────────── - -export function BlocksTab({ values }: { values: ScopeValues }) { - const grouped = getBlocksByCategory(); - - const categoryLabels: Record = { - chrome: "Site Chrome", - layout: "Layout", - components: "Components", - effects: "Effects", - }; - - return ( -
- {Object.entries(grouped).map(([category, blocks]) => ( -
-

- {categoryLabels[category] ?? category} -

-

- Toggle visibility of blocks in this scope. Hidden blocks use{" "} - - display: none - - . -

- -
- {blocks.map((block) => { - const key = `${BLOCK_KEY_PREFIX}${block.id}`; - const currentVal = values.blocks[key]; - const isChecked = - currentVal === "true" || - (currentVal === undefined && block.defaultVisible); - - return ( - - ); - })} -
-
- ))} -
- ); -} - -// ─── Layout Tab ────────────────────────────────────────────────────────────── - -export function LayoutTab({ values }: { values: ScopeValues }) { - return ( -
-

- Layout Settings -

-

- Control spacing, width, and grid settings for this scope. Values are - applied as CSS custom properties. -

- -
- {LAYOUT_FIELDS.map((field) => ( -
- -
- ))} -
-
- ); -} - -// ─── Effects Tab ───────────────────────────────────────────────────────────── - -export function EffectsTab({ values }: { values: ScopeValues }) { - return ( -
-

- Effects & Transitions -

-

- Control shadows, borders, and animation timing for this scope. -

- -
- {EFFECT_FIELDS.map((field) => ( -
- -
- ))} -
-
- ); -} - -// ─── Media Tab ─────────────────────────────────────────────────────────────── - -export function MediaTab({ values }: { values: ScopeValues }) { - return ( -
-

- Media & Backgrounds -

-

- Set custom logos, background images, and background properties for this - scope. -

- -
- {MEDIA_FIELDS.map((field) => ( -
- -
- ))} -
-
- ); -} - -// ─── Custom Tab ────────────────────────────────────────────────────────────── - -export function CustomTab({ values }: { values: ScopeValues }) { - return ( -
-
-

- Custom CSS -

-

- Add custom CSS that applies within this scope. Use{" "} - - &{" "} - {" "} - as a placeholder for the scope selector (e.g.{" "} - - & .content-card {"{"} border: 2px solid red; {"}"} - - ). -

-