style: clean up code with prettier and eslint

This commit is contained in:
openhands committed 2026-07-12 20:31:05 +02:00
1 parent 60278b9e71
commit fb8e77bb68
376 files changed
+22229 -20511

No files matched your search

+5872 -3394
View File
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -3,7 +3,9 @@ import { mysqlConnectionUrl } from "./db-url";
describe("mysqlConnectionUrl", () => { describe("mysqlConnectionUrl", () => {
it("removes Prisma-only pool options before passing the URL to MySQL2", () => { it("removes Prisma-only pool options before passing the URL to MySQL2", () => {
const result = mysqlConnectionUrl("mysql://user:pass@localhost:3306/cms?connection_limit=20&pool_timeout=30&charset=utf8mb4"); const result = mysqlConnectionUrl(
"mysql://user:pass@localhost:3306/cms?connection_limit=20&pool_timeout=30&charset=utf8mb4",
);
expect(result).not.toContain("connection_limit"); expect(result).not.toContain("connection_limit");
expect(result).not.toContain("pool_timeout"); expect(result).not.toContain("pool_timeout");
expect(result).toContain("charset=utf8mb4"); expect(result).toContain("charset=utf8mb4");
+1 -3
View File
@@ -1,7 +1,5 @@
export function splitSqlStatements(sql: string): string[] { export function splitSqlStatements(sql: string): string[] {
const withoutComments = sql const withoutComments = sql.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*--.*$/gm, "");
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/^\s*--.*$/gm, "");
return withoutComments return withoutComments
.split(";") .split(";")
+7 -1
View File
@@ -19,7 +19,13 @@ export async function updateBadge({ code, name, desc }: { code: string; name: st
await prisma.websiteBadges.upsert({ await prisma.websiteBadges.upsert({
where: { badgeKey: code }, where: { badgeKey: code },
update: { badgeName: name, badgeDescription: desc, updatedAt: new Date() }, update: { badgeName: name, badgeDescription: desc, updatedAt: new Date() },
create: { badgeKey: code, badgeName: name, badgeDescription: desc, createdAt: new Date(), updatedAt: new Date() }, create: {
badgeKey: code,
badgeName: name,
badgeDescription: desc,
createdAt: new Date(),
updatedAt: new Date(),
},
}); });
revalidatePath("/admin/import/badges"); revalidatePath("/admin/import/badges");
} }
+34 -34
View File
File diff suppressed because it is too large. Load diff
+34 -4
View File
@@ -7,7 +7,11 @@ import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity"; import { logStaffActivity } from "@/lib/services/staff-activity";
import type { ActionResult } from "@/lib/safe-action-shared"; import type { ActionResult } from "@/lib/safe-action-shared";
export async function bulkUnban({ userIds }: { userIds: number[] }): Promise<ActionResult<{ unbanned: number; total: number }>> { export async function bulkUnban({
userIds,
}: {
userIds: number[];
}): Promise<ActionResult<{ unbanned: number; total: number }>> {
const staff = await requireStaff(); const staff = await requireStaff();
const result = await prisma.ban.deleteMany({ where: { userId: { in: userIds } } }); const result = await prisma.ban.deleteMany({ where: { userId: { in: userIds } } });
await logStaffActivity({ await logStaffActivity({
@@ -19,7 +23,15 @@ export async function bulkUnban({ userIds }: { userIds: number[] }): Promise<Act
return { ok: true as const, data: { unbanned: result.count, total: userIds.length } }; return { ok: true as const, data: { unbanned: result.count, total: userIds.length } };
} }
export async function bulkBan({ userIds, reason, duration }: { userIds: number[]; reason: string; duration: number }): Promise<ActionResult<{ banned: number }>> { export async function bulkBan({
userIds,
reason,
duration,
}: {
userIds: number[];
reason: string;
duration: number;
}): Promise<ActionResult<{ banned: number }>> {
const staff = await requireStaff(); const staff = await requireStaff();
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
let banned = 0; let banned = 0;
@@ -53,7 +65,17 @@ export async function bulkBan({ userIds, reason, duration }: { userIds: number[]
return { ok: true as const, data: { banned } }; return { ok: true as const, data: { banned } };
} }
export async function bulkGiveCurrency({ userIds, amount, type }: { userIds: number[]; amount: number; type: "credits" | "pixels" | "points" }): Promise<ActionResult<{ given: number; total: number; failedIds: Array<{ userId: number; reason: string }> }>> { export async function bulkGiveCurrency({
userIds,
amount,
type,
}: {
userIds: number[];
amount: number;
type: "credits" | "pixels" | "points";
}): Promise<
ActionResult<{ given: number; total: number; failedIds: Array<{ userId: number; reason: string }> }>
> {
const staff = await requireStaff(); const staff = await requireStaff();
let given = 0; let given = 0;
const failedIds: Array<{ userId: number; reason: string }> = []; const failedIds: Array<{ userId: number; reason: string }> = [];
@@ -93,7 +115,15 @@ export async function bulkGiveCurrency({ userIds, amount, type }: { userIds: num
return { ok: true as const, data: { given, total: userIds.length, failedIds } }; return { ok: true as const, data: { given, total: userIds.length, failedIds } };
} }
export async function bulkGiveBadge({ userIds, badgeCode }: { userIds: number[]; badgeCode: string }): Promise<ActionResult<{ given: number; total: number; failedIds: Array<{ userId: number; reason: string }> }>> { export async function bulkGiveBadge({
userIds,
badgeCode,
}: {
userIds: number[];
badgeCode: string;
}): Promise<
ActionResult<{ given: number; total: number; failedIds: Array<{ userId: number; reason: string }> }>
> {
const staff = await requireStaff(); const staff = await requireStaff();
let given = 0; let given = 0;
const failedIds: Array<{ userId: number; reason: string }> = []; const failedIds: Array<{ userId: number; reason: string }> = [];
+24 -3
View File
@@ -33,7 +33,16 @@ export async function deleteBcItem({ id }: { id: number }) {
revalidatePath("/admin/catalog/builder-club"); revalidatePath("/admin/catalog/builder-club");
} }
export async function updateBcItem({ id, ...data }: { id: number; itemIds?: string; catalogName?: string; orderNumber?: number; extradata?: string }) { export async function updateBcItem({
id,
...data
}: {
id: number;
itemIds?: string;
catalogName?: string;
orderNumber?: number;
extradata?: string;
}) {
const staff = await requireStaff(); const staff = await requireStaff();
await prisma.catalogItemsBc.update({ where: { id }, data: data as any }); await prisma.catalogItemsBc.update({ where: { id }, data: data as any });
await logStaffActivity({ await logStaffActivity({
@@ -46,7 +55,16 @@ export async function updateBcItem({ id, ...data }: { id: number; itemIds?: stri
revalidatePath("/admin/catalog/builder-club"); revalidatePath("/admin/catalog/builder-club");
} }
export async function createBcItem({ pageId, ...data }: { pageId: number; itemIds: string; catalogName: string; orderNumber: number; extradata: string }) { export async function createBcItem({
pageId,
...data
}: {
pageId: number;
itemIds: string;
catalogName: string;
orderNumber: number;
extradata: string;
}) {
const staff = await requireStaff(); const staff = await requireStaff();
const created = await prisma.catalogItemsBc.create({ const created = await prisma.catalogItemsBc.create({
data: { pageId, ...data }, data: { pageId, ...data },
@@ -63,7 +81,10 @@ export async function createBcItem({ pageId, ...data }: { pageId: number; itemId
export async function toggleBcPage({ id, field }: { id: number; field: "enabled" | "visible" }) { export async function toggleBcPage({ id, field }: { id: number; field: "enabled" | "visible" }) {
await requireStaff(); await requireStaff();
const page = await prisma.catalogPagesBc.findUnique({ where: { id }, select: { enabled: true, visible: true } }); const page = await prisma.catalogPagesBc.findUnique({
where: { id },
select: { enabled: true, visible: true },
});
if (!page) return; if (!page) return;
await prisma.catalogPagesBc.update({ await prisma.catalogPagesBc.update({
where: { id }, where: { id },
+34 -6
View File
@@ -62,7 +62,11 @@ export async function moveCatalogItems({ ids, targetPageId }: { ids: number[]; t
return { ok: true as const, data: {} }; return { ok: true as const, data: {} };
} }
export async function reorderCatalogItems({ orders }: { orders: Array<{ id: number; orderNumber: number }> }) { export async function reorderCatalogItems({
orders,
}: {
orders: Array<{ id: number; orderNumber: number }>;
}) {
await requireStaff(); await requireStaff();
for (const { id, orderNumber } of orders) { for (const { id, orderNumber } of orders) {
await prisma.catalogItems.update({ where: { id }, data: { orderNumber } }); await prisma.catalogItems.update({ where: { id }, data: { orderNumber } });
@@ -72,7 +76,15 @@ export async function reorderCatalogItems({ orders }: { orders: Array<{ id: numb
return { ok: true as const, data: {} }; return { ok: true as const, data: {} };
} }
export async function updateCatalogItem({ id, catalogFields, baseItem }: { id: number; catalogFields: Record<string, unknown>; baseItem?: { id: number; fields: Record<string, unknown> } }) { export async function updateCatalogItem({
id,
catalogFields,
baseItem,
}: {
id: number;
catalogFields: Record<string, unknown>;
baseItem?: { id: number; fields: Record<string, unknown> };
}) {
const staff = await requireStaff(); const staff = await requireStaff();
await prisma.catalogItems.update({ where: { id }, data: catalogFields as any }); await prisma.catalogItems.update({ where: { id }, data: catalogFields as any });
if (baseItem) { if (baseItem) {
@@ -90,15 +102,22 @@ export async function updateCatalogItem({ id, catalogFields, baseItem }: { id: n
return { ok: true as const, data: {} }; return { ok: true as const, data: {} };
} }
export async function translateCatalogItems({ items }: { items: Array<{ id: number; publicName: string; description: string }> }) { export async function translateCatalogItems({
items,
}: {
items: Array<{ id: number; publicName: string; description: string }>;
}) {
await requireStaff(); await requireStaff();
let namesUpdated = 0; let namesUpdated = 0;
let descriptionsUpdated = 0; let descriptionsUpdated = 0;
let furniDataUpdated = 0; let furniDataUpdated = 0;
let furniDataInserted = 0; const furniDataInserted = 0;
for (const item of items) { for (const item of items) {
const existing = await prisma.catalogItems.findUnique({ where: { id: item.id }, select: { catalogName: true } }); const existing = await prisma.catalogItems.findUnique({
where: { id: item.id },
select: { catalogName: true },
});
if (!existing) continue; if (!existing) continue;
if (item.publicName && item.publicName !== existing.catalogName) { if (item.publicName && item.publicName !== existing.catalogName) {
@@ -127,5 +146,14 @@ export async function translateCatalogItems({ items }: { items: Array<{ id: numb
await rcon.updateCatalog(); await rcon.updateCatalog();
revalidatePath("/admin/catalog"); revalidatePath("/admin/catalog");
return { ok: true as const, data: { namesUpdated, descriptionsUpdated, furniDataUpdated, furniDataInserted: 0, updated: items.length } }; return {
ok: true as const,
data: {
namesUpdated,
descriptionsUpdated,
furniDataUpdated,
furniDataInserted: 0,
updated: items.length,
},
};
} }
+27 -9
View File
@@ -7,7 +7,10 @@ import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity"; import { logStaffActivity } from "@/lib/services/staff-activity";
import type { ActionResult } from "@/lib/safe-action-shared"; import type { ActionResult } from "@/lib/safe-action-shared";
export async function updateCatalogPage({ id, ...fields }: { id: number } & Record<string, unknown>): Promise<ActionResult> { export async function updateCatalogPage({
id,
...fields
}: { id: number } & Record<string, unknown>): Promise<ActionResult> {
const staff = await requireStaff(); const staff = await requireStaff();
await prisma.catalogPages.update({ where: { id }, data: fields as any }); await prisma.catalogPages.update({ where: { id }, data: fields as any });
await rcon.updateCatalog(); await rcon.updateCatalog();
@@ -37,9 +40,18 @@ export async function deleteCatalogPage({ id }: { id: number }) {
return { ok: true as const, data: {} }; return { ok: true as const, data: {} };
} }
export async function toggleCatalogPage({ id, action }: { id: number; action: "toggleEnabled" | "toggleVisible" }) { export async function toggleCatalogPage({
id,
action,
}: {
id: number;
action: "toggleEnabled" | "toggleVisible";
}) {
await requireStaff(); await requireStaff();
const page = await prisma.catalogPages.findUnique({ where: { id }, select: { enabled: true, visible: true } }); const page = await prisma.catalogPages.findUnique({
where: { id },
select: { enabled: true, visible: true },
});
if (!page) return { ok: false as const, error: "Catalog page not found" }; if (!page) return { ok: false as const, error: "Catalog page not found" };
const field = action === "toggleEnabled" ? "enabled" : "visible"; const field = action === "toggleEnabled" ? "enabled" : "visible";
const current = action === "toggleEnabled" ? page.enabled : page.visible; const current = action === "toggleEnabled" ? page.enabled : page.visible;
@@ -51,7 +63,17 @@ export async function toggleCatalogPage({ id, action }: { id: number; action: "t
return { ok: true as const, data: {} }; return { ok: true as const, data: {} };
} }
export async function createCatalogPage(input: { caption: string; parentId: number; pageLayout?: string; iconImage?: number; iconColor?: number; enabled?: "0" | "1"; visible?: "0" | "1"; minRank?: number; orderNum?: number }): Promise<ActionResult<{ id: number }>> { export async function createCatalogPage(input: {
caption: string;
parentId: number;
pageLayout?: string;
iconImage?: number;
iconColor?: number;
enabled?: "0" | "1";
visible?: "0" | "1";
minRank?: number;
orderNum?: number;
}): Promise<ActionResult<{ id: number }>> {
const staff = await requireStaff(); const staff = await requireStaff();
const created = await prisma.catalogPages.create({ const created = await prisma.catalogPages.create({
data: { data: {
@@ -83,11 +105,7 @@ export async function createCatalogPage(input: { caption: string; parentId: numb
return { ok: true as const, data: { id: created.id } }; return { ok: true as const, data: { id: created.id } };
} }
export async function reorderTreePage(input: { export async function reorderTreePage(input: { pageId: number; newParentId?: number; newOrderNum: number }) {
pageId: number;
newParentId?: number;
newOrderNum: number;
}) {
await requireStaff(); await requireStaff();
await prisma.catalogPages.update({ await prisma.catalogPages.update({
where: { id: input.pageId }, where: { id: input.pageId },
+17 -17
View File
@@ -1,39 +1,39 @@
'use server' "use server";
import { z } from 'zod' import { z } from "zod";
import { PERMS } from '@/lib/permissions' import { PERMS } from "@/lib/permissions";
import { prisma } from '@/lib/prisma' import { prisma } from "@/lib/prisma";
import { adminAction } from '@/lib/safe-action' import { adminAction } from "@/lib/safe-action";
import { actionOk } from '@/lib/safe-action-shared' import { actionOk } from "@/lib/safe-action-shared";
import { logAudit } from '@/lib/services/audit' import { logAudit } from "@/lib/services/audit";
import { rcon } from '@/lib/services/rcon' import { rcon } from "@/lib/services/rcon";
const saveEmulatorSettingsSchema = z.object({ const saveEmulatorSettingsSchema = z.object({
settings: z.record(z.string(), z.string()), settings: z.record(z.string(), z.string()),
}) });
export const saveEmulatorSettings = adminAction( export const saveEmulatorSettings = adminAction(
{ permission: PERMS.SETTINGS_EDIT, schema: saveEmulatorSettingsSchema }, { permission: PERMS.SETTINGS_EDIT, schema: saveEmulatorSettingsSchema },
async (ctx) => { async (ctx) => {
const entries = Object.entries(ctx.data.settings) const entries = Object.entries(ctx.data.settings);
for (const [key, value] of entries) { for (const [key, value] of entries) {
await prisma.emulatorSettings.upsert({ await prisma.emulatorSettings.upsert({
where: { key }, where: { key },
update: { value: String(value) }, update: { value: String(value) },
create: { key, value: String(value) }, create: { key, value: String(value) },
}) });
} }
await rcon.updateConfig() await rcon.updateConfig();
logAudit({ logAudit({
userId: ctx.session.user.id, userId: ctx.session.user.id,
action: 'emulator_settings_update', action: "emulator_settings_update",
target: 'EmulatorSettings', target: "EmulatorSettings",
after: ctx.data.settings, after: ctx.data.settings,
}) });
return actionOk() return actionOk();
}, },
) );
+71 -71
View File
File diff suppressed because it is too large. Load diff
+16 -2
View File
@@ -3,13 +3,27 @@
import { requireStaff } from "@/lib/admin/guard"; import { requireStaff } from "@/lib/admin/guard";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
export async function importBadgeFromRemote({ code, name, description }: { code: string; name: string; description: string }) { export async function importBadgeFromRemote({
code,
name,
description,
}: {
code: string;
name: string;
description: string;
}) {
await requireStaff(); await requireStaff();
try { try {
await prisma.websiteBadges.upsert({ await prisma.websiteBadges.upsert({
where: { badgeKey: code }, where: { badgeKey: code },
update: { badgeName: name, badgeDescription: description, updatedAt: new Date() }, update: { badgeName: name, badgeDescription: description, updatedAt: new Date() },
create: { badgeKey: code, badgeName: name, badgeDescription: description, createdAt: new Date(), updatedAt: new Date() }, create: {
badgeKey: code,
badgeName: name,
badgeDescription: description,
createdAt: new Date(),
updatedAt: new Date(),
},
}); });
return { ok: true as const }; return { ok: true as const };
} catch { } catch {
+13 -12
View File
@@ -1,19 +1,20 @@
'use server' "use server";
import { z } from 'zod' import { z } from "zod";
import { PERMS } from '@/lib/permissions' import { PERMS } from "@/lib/permissions";
import { adminAction } from '@/lib/safe-action' import { adminAction } from "@/lib/safe-action";
import { actionOk } from '@/lib/safe-action-shared' import { actionOk } from "@/lib/safe-action-shared";
import { cleanupConvertedSwfs, deleteImportedItem } from '@/lib/services/furni-import' import { cleanupConvertedSwfs, deleteImportedItem } from "@/lib/services/furni-import";
export const cleanSwfFiles = adminAction({ permission: PERMS.ASSETS_IMPORT }, async () => { export const cleanSwfFiles = adminAction({ permission: PERMS.ASSETS_IMPORT }, async () => {
const result = await cleanupConvertedSwfs() const result = await cleanupConvertedSwfs();
return actionOk(result as unknown as Record<string, unknown>) return actionOk(result as unknown as Record<string, unknown>);
}) });
const deleteSchema = z.object({ classname: z.string().trim().min(1) }) const deleteSchema = z.object({ classname: z.string().trim().min(1) });
export const deleteImportedFurni = adminAction( export const deleteImportedFurni = adminAction(
{ permission: PERMS.ASSETS_IMPORT, schema: deleteSchema }, { permission: PERMS.ASSETS_IMPORT, schema: deleteSchema },
async (ctx) => actionOk((await deleteImportedItem(ctx.data.classname)) as unknown as Record<string, unknown>), async (ctx) =>
) actionOk((await deleteImportedItem(ctx.data.classname)) as unknown as Record<string, unknown>),
);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+45 -45
View File
File diff suppressed because it is too large. Load diff
+45 -45
View File
File diff suppressed because it is too large. Load diff
+10 -1
View File
@@ -71,7 +71,16 @@ export async function deleteRoom({ id }: { id: number }) {
revalidatePath("/admin/rooms"); revalidatePath("/admin/rooms");
} }
export async function updateRoom({ id, ...data }: { id: number; name?: string; description?: string; state?: string; usersMax?: number }) { export async function updateRoom({
id,
...data
}: {
id: number;
name?: string;
description?: string;
state?: string;
usersMax?: number;
}) {
const staff = await requireStaff(); const staff = await requireStaff();
await prisma.rooms.update({ where: { id }, data: data as any }); await prisma.rooms.update({ where: { id }, data: data as any });
await logStaffActivity({ await logStaffActivity({
+13 -1
View File
@@ -10,7 +10,19 @@ export async function deleteSoundtrack({ id }: { id: number }) {
revalidatePath("/admin/sounds"); revalidatePath("/admin/sounds");
} }
export async function updateSoundtrack({ id, name, author, track, length }: { id: number; name: string; author: string; track: string; length: number }) { export async function updateSoundtrack({
id,
name,
author,
track,
length,
}: {
id: number;
name: string;
author: string;
track: string;
length: number;
}) {
await requireStaff(); await requireStaff();
await prisma.soundtracks.update({ await prisma.soundtracks.update({
where: { id }, where: { id },
+22 -24
View File
@@ -1,47 +1,45 @@
'use server' "use server";
import { z } from 'zod' import { z } from "zod";
import { PERMS } from '@/lib/permissions' import { PERMS } from "@/lib/permissions";
import { prisma } from '@/lib/prisma' import { prisma } from "@/lib/prisma";
import { adminAction } from '@/lib/safe-action' import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from '@/lib/safe-action-shared' import { ActionError, actionOk } from "@/lib/safe-action-shared";
const templateSchema = z.object({ const templateSchema = z.object({
title: z.string().min(1).max(255), title: z.string().min(1).max(255),
content: z.string().min(1), content: z.string().min(1),
category: z.string().max(50).optional().default('general'), category: z.string().max(50).optional().default("general"),
sortOrder: z.coerce.number().int().min(0).default(0), sortOrder: z.coerce.number().int().min(0).default(0),
}) });
export const createTemplate = adminAction( export const createTemplate = adminAction(
{ permission: PERMS.TICKETS_EDIT, schema: templateSchema }, { permission: PERMS.TICKETS_EDIT, schema: templateSchema },
async (ctx) => { async (ctx) => {
const tpl = await prisma.websiteTicketTemplate.create({ data: ctx.data }) const tpl = await prisma.websiteTicketTemplate.create({ data: ctx.data });
return actionOk({ id: tpl.id }) return actionOk({ id: tpl.id });
}, },
) );
const updateTemplateInput = templateSchema const updateTemplateInput = templateSchema.partial().extend({ id: z.coerce.number().int().positive() });
.partial()
.extend({ id: z.coerce.number().int().positive() })
export const updateTemplate = adminAction( export const updateTemplate = adminAction(
{ permission: PERMS.TICKETS_EDIT, schema: updateTemplateInput }, { permission: PERMS.TICKETS_EDIT, schema: updateTemplateInput },
async (ctx) => { async (ctx) => {
const { id, ...data } = ctx.data const { id, ...data } = ctx.data;
const existing = await prisma.websiteTicketTemplate.findUnique({ where: { id } }) const existing = await prisma.websiteTicketTemplate.findUnique({ where: { id } });
if (!existing) throw new ActionError('Template not found') if (!existing) throw new ActionError("Template not found");
await prisma.websiteTicketTemplate.update({ where: { id }, data }) await prisma.websiteTicketTemplate.update({ where: { id }, data });
return actionOk({ id }) return actionOk({ id });
}, },
) );
const deleteTemplateInput = z.object({ id: z.coerce.number().int().positive() }) const deleteTemplateInput = z.object({ id: z.coerce.number().int().positive() });
export const deleteTemplate = adminAction( export const deleteTemplate = adminAction(
{ permission: PERMS.TICKETS_EDIT, schema: deleteTemplateInput }, { permission: PERMS.TICKETS_EDIT, schema: deleteTemplateInput },
async (ctx) => { async (ctx) => {
await prisma.websiteTicketTemplate.delete({ where: { id: ctx.data.id } }) await prisma.websiteTicketTemplate.delete({ where: { id: ctx.data.id } });
return actionOk() return actionOk();
}, },
) );
+73 -73
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+163 -184
View File
File diff suppressed because it is too large. Load diff
+19 -19
View File
@@ -1,16 +1,16 @@
'use server' "use server";
import { revalidateTag } from 'next/cache' import { revalidateTag } from "next/cache";
import { z } from 'zod' import { z } from "zod";
import { PERMS } from '@/lib/permission-slugs' import { PERMS } from "@/lib/permission-slugs";
import { prisma } from '@/lib/prisma' import { prisma } from "@/lib/prisma";
import { adminAction } from '@/lib/safe-action' import { adminAction } from "@/lib/safe-action";
import { actionOk } from '@/lib/safe-action-shared' import { actionOk } from "@/lib/safe-action-shared";
const toggleWatchSchema = z.object({ const toggleWatchSchema = z.object({
targetUserId: z.coerce.number().int().positive(), targetUserId: z.coerce.number().int().positive(),
reason: z.string().max(255).optional(), reason: z.string().max(255).optional(),
}) });
/** /**
* Toggle a watch entry for the calling staff on the given target user. * Toggle a watch entry for the calling staff on the given target user.
@@ -21,23 +21,23 @@ const toggleWatchSchema = z.object({
export const toggleUserWatch = adminAction( export const toggleUserWatch = adminAction(
{ permission: PERMS.USERS_VIEW, schema: toggleWatchSchema }, { permission: PERMS.USERS_VIEW, schema: toggleWatchSchema },
async (ctx) => { async (ctx) => {
const staffId = ctx.session.user.id const staffId = ctx.session.user.id;
const { targetUserId, reason } = ctx.data const { targetUserId, reason } = ctx.data;
const existing = await prisma.userWatch.findUnique({ const existing = await prisma.userWatch.findUnique({
where: { staffId_targetUserId: { staffId, targetUserId } }, where: { staffId_targetUserId: { staffId, targetUserId } },
}) });
if (existing) { if (existing) {
await prisma.userWatch.delete({ where: { id: existing.id } }) await prisma.userWatch.delete({ where: { id: existing.id } });
revalidateTag(`user-watch:${staffId}`, { expire: 0 }) revalidateTag(`user-watch:${staffId}`, { expire: 0 });
return actionOk({ watching: false }) return actionOk({ watching: false });
} }
await prisma.userWatch.create({ await prisma.userWatch.create({
data: { staffId, targetUserId, reason: reason ?? '' }, data: { staffId, targetUserId, reason: reason ?? "" },
}) });
revalidateTag(`user-watch:${staffId}`, { expire: 0 }) revalidateTag(`user-watch:${staffId}`, { expire: 0 });
return actionOk({ watching: true }) return actionOk({ watching: true });
}, },
) );
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
+5 -1
View File
@@ -39,7 +39,11 @@ export default async function EditArticle({
</p> </p>
</div> </div>
</div> </div>
{error ? <p className="text-xs text-[var(--admin-error)] dark:text-[var(--admin-error)] mb-4">Error: {error}</p> : null} {error ? (
<p className="text-xs text-[var(--admin-error)] dark:text-[var(--admin-error)] mb-4">
Error: {error}
</p>
) : null}
<ArticleForm <ArticleForm
action={async (fd) => { action={async (fd) => {
"use server"; "use server";
+5 -1
View File
@@ -21,7 +21,11 @@ export default async function NewArticle({ searchParams }: { searchParams: Promi
</p> </p>
</div> </div>
</div> </div>
{error ? <p className="text-xs text-[var(--admin-error)] dark:text-[var(--admin-error)] mb-4">Error: {error}</p> : null} {error ? (
<p className="text-xs text-[var(--admin-error)] dark:text-[var(--admin-error)] mb-4">
Error: {error}
</p>
) : null}
<ArticleForm action={createArticle} /> <ArticleForm action={createArticle} />
</main> </main>
); );
+5 -1
View File
@@ -35,7 +35,11 @@ export default async function AdminArticles({ searchParams }: { searchParams: Pr
</Link> </Link>
</div> </div>
{error ? <p className="text-xs text-[var(--admin-error)] dark:text-[var(--admin-error)] mb-4">Error: {error}</p> : null} {error ? (
<p className="text-xs text-[var(--admin-error)] dark:text-[var(--admin-error)] mb-4">
Error: {error}
</p>
) : null}
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6"> <div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
<StatusCard label={t("articlesShown")} value={articles.length} icon="📰" /> <StatusCard label={t("articlesShown")} value={articles.length} icon="📰" />
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -1,17 +1,17 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from '@/lib/prisma' import { prisma } from "@/lib/prisma";
import { BannersManager } from './banners-manager' import { BannersManager } from "./banners-manager";
export default async function BannersPage() { export default async function BannersPage() {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.BANNERS_VIEW, session.user.rank)) { if (!canAccess(permissions, PERMS.BANNERS_VIEW, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
const banners = await prisma.websiteBanner.findMany({ const banners = await prisma.websiteBanner.findMany({
orderBy: { sortOrder: 'asc' }, orderBy: { sortOrder: "asc" },
}) });
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -21,5 +21,5 @@ export default async function BannersPage() {
</div> </div>
<BannersManager banners={banners} /> <BannersManager banners={banners} />
</div> </div>
) );
} }
File diff suppressed because it is too large. Load diff
@@ -2,6 +2,6 @@
* Re-export barrel for backward compatibility. * Re-export barrel for backward compatibility.
* Implementation moved to ./catalog-items-table/ directory. * Implementation moved to ./catalog-items-table/ directory.
*/ */
export { CatalogItemsTable } from './catalog-items-table/catalog-items-table' export { CatalogItemsTable } from "./catalog-items-table/catalog-items-table";
export { FurniIcon } from './catalog-items-table/furni-icon' export { FurniIcon } from "./catalog-items-table/furni-icon";
export type { BaseItemData, CatalogItemData } from './catalog-items-table/types' export type { BaseItemData, CatalogItemData } from "./catalog-items-table/types";
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,7 +1,7 @@
'use client' "use client";
import { Image as ImageIcon } from 'lucide-react' import { Image as ImageIcon } from "lucide-react";
import { getHabboCdnIconUrl, getLocalIconUrl } from '@/lib/furni/classname' import { getHabboCdnIconUrl, getLocalIconUrl } from "@/lib/furni/classname";
export function FurniIcon({ classname, revision }: { classname: string; revision: number }) { export function FurniIcon({ classname, revision }: { classname: string; revision: number }) {
if (!classname) { if (!classname) {
@@ -9,34 +9,34 @@ export function FurniIcon({ classname, revision }: { classname: string; revision
<div className="w-10 h-10 rounded bg-muted flex items-center justify-center"> <div className="w-10 h-10 rounded bg-muted flex items-center justify-center">
<ImageIcon className="h-4 w-4 text-muted-foreground" /> <ImageIcon className="h-4 w-4 text-muted-foreground" />
</div> </div>
) );
} }
const localUrl = getLocalIconUrl(classname) const localUrl = getLocalIconUrl(classname);
function handleError(e: React.SyntheticEvent<HTMLImageElement>) { function handleError(e: React.SyntheticEvent<HTMLImageElement>) {
const img = e.target as HTMLImageElement const img = e.target as HTMLImageElement;
const stage = img.dataset.fallback const stage = img.dataset.fallback;
if (!stage && revision > 0) { if (!stage && revision > 0) {
img.dataset.fallback = 'cdn' img.dataset.fallback = "cdn";
img.src = getHabboCdnIconUrl(classname, revision) img.src = getHabboCdnIconUrl(classname, revision);
} else { } else {
img.dataset.fallback = 'none' img.dataset.fallback = "none";
img.style.opacity = '0' img.style.opacity = "0";
} }
} }
return ( return (
<div className="w-10 h-10 rounded bg-muted/50 flex items-center justify-center overflow-hidden"> <div className="w-10 h-10 rounded bg-muted/50 flex items-center justify-center overflow-hidden">
{/* eslint-disable-next-line @next/next/no-img-element */} { }
<img <img
src={localUrl} src={localUrl}
alt={classname} alt={classname}
className="max-w-full max-h-full object-contain" className="max-w-full max-h-full object-contain"
style={{ imageRendering: 'pixelated' }} style={{ imageRendering: "pixelated" }}
onError={handleError} onError={handleError}
loading="lazy" loading="lazy"
/> />
</div> </div>
) );
} }
@@ -1,4 +1,4 @@
export { CatalogItemsTable } from './catalog-items-table' export { CatalogItemsTable } from "./catalog-items-table";
export { FurniIcon } from './furni-icon' export { FurniIcon } from "./furni-icon";
export type { BaseItemData, CatalogItemData, InlineEdit } from './types' export type { BaseItemData, CatalogItemData, InlineEdit } from "./types";
export { DEFAULT_NEW_ITEM, getPointsLabel, POINTS_TYPES } from './types' export { DEFAULT_NEW_ITEM, getPointsLabel, POINTS_TYPES } from "./types";
@@ -1,59 +1,53 @@
'use client' "use client";
import { Loader2, Music } from 'lucide-react' import { Loader2, Music } from "lucide-react";
import { useEffect, useState } from 'react' import { useEffect, useState } from "react";
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { import {
getCachedSongPickerOptions, getCachedSongPickerOptions,
loadSongPickerOptions, loadSongPickerOptions,
type SoundtrackOption, type SoundtrackOption,
} from '@/lib/client-cache/song-picker-cache' } from "@/lib/client-cache/song-picker-cache";
// Re-export so existing `import { invalidateSongPickerCache } from '.../song-picker'` // Re-export so existing `import { invalidateSongPickerCache } from '.../song-picker'`
// callsites keep working. New code should import from the lib module directly. // callsites keep working. New code should import from the lib module directly.
export { invalidateSongPickerCache } from '@/lib/client-cache/song-picker-cache' export { invalidateSongPickerCache } from "@/lib/client-cache/song-picker-cache";
interface Props { interface Props {
value: number value: number;
onChange: (id: number) => void onChange: (id: number) => void;
disabled?: boolean disabled?: boolean;
} }
function formatLength(seconds: number): string { function formatLength(seconds: number): string {
if (!seconds || seconds <= 0) return '' if (!seconds || seconds <= 0) return "";
const m = Math.floor(seconds / 60) const m = Math.floor(seconds / 60);
const s = seconds % 60 const s = seconds % 60;
return `${m}:${String(s).padStart(2, '0')}` return `${m}:${String(s).padStart(2, "0")}`;
} }
export function SongPicker({ value, onChange, disabled }: Props) { export function SongPicker({ value, onChange, disabled }: Props) {
const initial = getCachedSongPickerOptions() const initial = getCachedSongPickerOptions();
const [options, setOptions] = useState<SoundtrackOption[]>(initial ?? []) const [options, setOptions] = useState<SoundtrackOption[]>(initial ?? []);
const [loading, setLoading] = useState(!initial) const [loading, setLoading] = useState(!initial);
useEffect(() => { useEffect(() => {
if (getCachedSongPickerOptions()) return if (getCachedSongPickerOptions()) return;
let cancelled = false let cancelled = false;
loadSongPickerOptions() loadSongPickerOptions()
.then((items) => { .then((items) => {
if (!cancelled) setOptions(items) if (!cancelled) setOptions(items);
}) })
.catch(() => { .catch(() => {
if (!cancelled) setOptions([]) if (!cancelled) setOptions([]);
}) })
.finally(() => { .finally(() => {
if (!cancelled) setLoading(false) if (!cancelled) setLoading(false);
}) });
return () => { return () => {
cancelled = true cancelled = true;
} };
}, []) }, []);
return ( return (
<Select <Select
@@ -81,14 +75,12 @@ export function SongPicker({ value, onChange, disabled }: Props) {
<span className="font-medium">{opt.name}</span> <span className="font-medium">{opt.name}</span>
{opt.author && <span className="text-xs text-muted-foreground">— {opt.author}</span>} {opt.author && <span className="text-xs text-muted-foreground">— {opt.author}</span>}
{opt.length > 0 && ( {opt.length > 0 && (
<span className="text-xs text-muted-foreground font-mono"> <span className="text-xs text-muted-foreground font-mono">{formatLength(opt.length)}</span>
{formatLength(opt.length)}
</span>
)} )}
</span> </span>
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
) );
} }
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 -2
View File
@@ -1,4 +1,4 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from "@/components/ui/skeleton";
export default function CatalogDetailLoading() { export default function CatalogDetailLoading() {
return ( return (
@@ -37,5 +37,5 @@ export default function CatalogDetailLoading() {
</div> </div>
</div> </div>
</div> </div>
) );
} }
File diff suppressed because it is too large. Load diff
@@ -1,31 +1,31 @@
import { notFound, redirect } from 'next/navigation' import { notFound, redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from '@/lib/prisma' import { prisma } from "@/lib/prisma";
import { BcPageDetail } from '../bc-manager' import { BcPageDetail } from "../bc-manager";
export default async function BcPageDetailPage({ export default async function BcPageDetailPage({
params: paramsPromise, params: paramsPromise,
}: { }: {
params: Promise<{ locale: string; id: string }> params: Promise<{ locale: string; id: string }>;
}) { }) {
const { id: idStr } = await paramsPromise const { id: idStr } = await paramsPromise;
const id = parseInt(idStr, 10) const id = parseInt(idStr, 10);
if (Number.isNaN(id)) notFound() if (Number.isNaN(id)) notFound();
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.CATALOG_VIEW, session.user.rank)) { if (!canAccess(permissions, PERMS.CATALOG_VIEW, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank) const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
const bcPage = await prisma.catalogPagesBc.findUnique({ where: { id } }) const bcPage = await prisma.catalogPagesBc.findUnique({ where: { id } });
if (!bcPage) notFound() if (!bcPage) notFound();
const bcItems = await prisma.catalogItemsBc.findMany({ const bcItems = await prisma.catalogItemsBc.findMany({
where: { pageId: id }, where: { pageId: id },
orderBy: { orderNumber: 'asc' }, orderBy: { orderNumber: "asc" },
}) });
return ( return (
<BcPageDetail <BcPageDetail
@@ -41,11 +41,11 @@ export default async function BcPageDetailPage({
enabled: bcPage.enabled, enabled: bcPage.enabled,
pageHeadline: bcPage.pageHeadline, pageHeadline: bcPage.pageHeadline,
pageTeaser: bcPage.pageTeaser, pageTeaser: bcPage.pageTeaser,
pageSpecial: bcPage.pageSpecial ?? '', pageSpecial: bcPage.pageSpecial ?? "",
pageText1: bcPage.pageText1 ?? '', pageText1: bcPage.pageText1 ?? "",
pageText2: bcPage.pageText2 ?? '', pageText2: bcPage.pageText2 ?? "",
pageTextDetails: bcPage.pageTextDetails ?? '', pageTextDetails: bcPage.pageTextDetails ?? "",
pageTextTeaser: bcPage.pageTextTeaser ?? '', pageTextTeaser: bcPage.pageTextTeaser ?? "",
}} }}
items={bcItems.map((i) => ({ items={bcItems.map((i) => ({
id: i.id, id: i.id,
@@ -57,5 +57,5 @@ export default async function BcPageDetailPage({
}))} }))}
canEdit={canEdit} canEdit={canEdit}
/> />
) );
} }
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,5 +1,5 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
export default function BuilderClubPage() { export default function BuilderClubPage() {
redirect('/admin/catalog?catalog=bc') redirect("/admin/catalog?catalog=bc");
} }
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 -2
View File
@@ -1,4 +1,4 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from "@/components/ui/skeleton";
export default function CatalogLoading() { export default function CatalogLoading() {
return ( return (
@@ -40,5 +40,5 @@ export default function CatalogLoading() {
</div> </div>
</div> </div>
</div> </div>
) );
} }
File diff suppressed because it is too large. Load diff
+15 -15
View File
@@ -1,32 +1,32 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from "@/components/ui/card";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from '@/lib/prisma' import { prisma } from "@/lib/prisma";
export default async function EmulatorErrorsPage({ export default async function EmulatorErrorsPage({
searchParams, searchParams,
}: { }: {
searchParams: Promise<Record<string, string>> searchParams: Promise<Record<string, string>>;
}) { }) {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.DEVOPS_VIEW, session.user.rank)) { if (!canAccess(permissions, PERMS.DEVOPS_VIEW, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
const params = await searchParams const params = await searchParams;
const page = Math.max(1, Number(params.page) || 1) const page = Math.max(1, Number(params.page) || 1);
const perPage = 50 const perPage = 50;
const [errors, total] = await Promise.all([ const [errors, total] = await Promise.all([
prisma.emulatorErrors.findMany({ prisma.emulatorErrors.findMany({
orderBy: { id: 'desc' }, orderBy: { id: "desc" },
skip: (page - 1) * perPage, skip: (page - 1) * perPage,
take: perPage, take: perPage,
}), }),
prisma.emulatorErrors.count(), prisma.emulatorErrors.count(),
]) ]);
const lastPage = Math.ceil(total / perPage) const lastPage = Math.ceil(total / perPage);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -52,7 +52,7 @@ export default async function EmulatorErrorsPage({
</span> </span>
</div> </div>
<pre className="text-xs whitespace-pre-wrap break-all font-mono bg-muted p-2 rounded"> <pre className="text-xs whitespace-pre-wrap break-all font-mono bg-muted p-2 rounded">
{Buffer.from(error.stacktrace).toString('utf-8')} {Buffer.from(error.stacktrace).toString("utf-8")}
</pre> </pre>
</div> </div>
))} ))}
@@ -79,5 +79,5 @@ export default async function EmulatorErrorsPage({
</div> </div>
)} )}
</div> </div>
) );
} }
+22 -32
View File
@@ -1,55 +1,45 @@
'use client' "use client";
import { RefreshCw } from 'lucide-react' import { RefreshCw } from "lucide-react";
import { useEffect, useState } from 'react' import { useEffect, useState } from "react";
import { Badge } from '@/components/ui/badge' import { Badge } from "@/components/ui/badge";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
export function HealthCheckClient() { export function HealthCheckClient() {
const [status, setStatus] = useState<'checking' | 'online' | 'offline'>('checking') const [status, setStatus] = useState<"checking" | "online" | "offline">("checking");
const [checking, setChecking] = useState(false) const [checking, setChecking] = useState(false);
async function checkEmulator() { async function checkEmulator() {
setChecking(true) setChecking(true);
setStatus('checking') setStatus("checking");
try { try {
const res = await fetch('/api/admin/devops/health') const res = await fetch("/api/admin/devops/health");
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json();
setStatus(data.emulator ? 'online' : 'offline') setStatus(data.emulator ? "online" : "offline");
} else { } else {
setStatus('offline') setStatus("offline");
} }
} catch { } catch {
setStatus('offline') setStatus("offline");
} finally { } finally {
setChecking(false) setChecking(false);
} }
} }
// biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code // biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code
useEffect(() => { useEffect(() => {
checkEmulator() checkEmulator();
}, []) }, []);
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge <Badge variant={status === "online" ? "default" : status === "offline" ? "destructive" : "secondary"}>
variant={ {status === "checking" ? "Checking..." : status === "online" ? "Online" : "Offline"}
status === 'online' ? 'default' : status === 'offline' ? 'destructive' : 'secondary'
}
>
{status === 'checking' ? 'Checking...' : status === 'online' ? 'Online' : 'Offline'}
</Badge> </Badge>
<Button <Button variant="ghost" size="icon" className="h-6 w-6" onClick={checkEmulator} disabled={checking}>
variant="ghost" <RefreshCw className={`h-3 w-3 ${checking ? "animate-spin" : ""}`} />
size="icon"
className="h-6 w-6"
onClick={checkEmulator}
disabled={checking}
>
<RefreshCw className={`h-3 w-3 ${checking ? 'animate-spin' : ''}`} />
</Button> </Button>
</div> </div>
) );
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,16 +1,16 @@
'use client' "use client";
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface Registration { interface Registration {
id: number id: number;
userId: number userId: number;
username: string username: string;
createdAt: Date createdAt: Date;
} }
interface Props { interface Props {
registrations: Registration[] registrations: Registration[];
} }
export function EventRegistrations({ registrations }: Props) { export function EventRegistrations({ registrations }: Props) {
@@ -36,5 +36,5 @@ export function EventRegistrations({ registrations }: Props) {
)} )}
</CardContent> </CardContent>
</Card> </Card>
) );
} }
+27 -27
View File
@@ -1,43 +1,43 @@
'use client' "use client";
import { Plus } from 'lucide-react' import { Plus } from "lucide-react";
import { useState } from 'react' import { useState } from "react";
import { addEventWinner } from '@/actions/events' import { addEventWinner } from "@/actions/events";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
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";
interface Winner { interface Winner {
id: number id: number;
userId: number userId: number;
position: number position: number;
username: string username: string;
createdAt: Date createdAt: Date;
} }
interface Props { interface Props {
eventId: number eventId: number;
winners: Winner[] winners: Winner[];
} }
export function EventWinnersManager({ eventId, winners }: Props) { export function EventWinnersManager({ eventId, winners }: Props) {
const { run, isPending } = useServerAction() const { run, isPending } = useServerAction();
const [showForm, setShowForm] = useState(false) const [showForm, setShowForm] = useState(false);
const [userId, setUserId] = useState('') const [userId, setUserId] = useState("");
const [position, setPosition] = useState(winners.length + 1) const [position, setPosition] = useState(winners.length + 1);
function handleAdd(e: React.FormEvent) { function handleAdd(e: React.FormEvent) {
e.preventDefault() e.preventDefault();
run(() => addEventWinner({ eventId, userId: Number(userId), position }), { run(() => addEventWinner({ eventId, userId: Number(userId), position }), {
successMessage: 'Winner added!', successMessage: "Winner added!",
onSuccess: () => { onSuccess: () => {
setShowForm(false) setShowForm(false);
setUserId('') setUserId("");
setPosition(position + 1) setPosition(position + 1);
}, },
}) });
} }
return ( return (
@@ -95,5 +95,5 @@ export function EventWinnersManager({ eventId, winners }: Props) {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
) );
} }
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -1,18 +1,18 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from '@/lib/prisma' import { prisma } from "@/lib/prisma";
import { EventForm } from '../event-form' import { EventForm } from "../event-form";
export default async function CreateEventPage() { export default async function CreateEventPage() {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.EVENTS_EDIT, session.user.rank)) { if (!canAccess(permissions, PERMS.EVENTS_EDIT, session.user.rank)) {
redirect('/admin/events') redirect("/admin/events");
} }
const eventTypes = await prisma.websiteEventType.findMany({ const eventTypes = await prisma.websiteEventType.findMany({
where: { isActive: 1 }, where: { isActive: 1 },
orderBy: { name: 'asc' }, orderBy: { name: "asc" },
}) });
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -23,5 +23,5 @@ export default async function CreateEventPage() {
<EventForm eventTypes={eventTypes} /> <EventForm eventTypes={eventTypes} />
</div> </div>
) );
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+20 -20
View File
@@ -1,32 +1,32 @@
import Link from 'next/link' import Link from "next/link";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { fetchAdminList } from '@/lib/admin-list' import { fetchAdminList } from "@/lib/admin-list";
import { PERMS } from '@/lib/permissions' import { PERMS } from "@/lib/permissions";
import { EventsTable } from './events-table' import { EventsTable } from "./events-table";
export default async function EventsPage({ export default async function EventsPage({
params: paramsPromise, params: paramsPromise,
searchParams, searchParams,
}: { }: {
params: Promise<{ locale: string }> params: Promise<{ locale: string }>;
searchParams: Promise<Record<string, string>> searchParams: Promise<Record<string, string>>;
}) { }) {
const result = await fetchAdminList( const result = await fetchAdminList(
{ {
permission: PERMS.EVENTS_VIEW, permission: PERMS.EVENTS_VIEW,
model: 'websiteEvent', model: "websiteEvent",
searchFields: ['title'], searchFields: ["title"],
defaultSort: { startsAt: 'desc' }, defaultSort: { startsAt: "desc" },
include: { type: { select: { name: true, color: true } } }, include: { type: { select: { name: true, color: true } } },
mapRow: (e: { mapRow: (e: {
id: number id: number;
title: string title: string;
type: { name: string; color: string } type: { name: string; color: string };
startsAt: Date startsAt: Date;
endsAt: Date | null endsAt: Date | null;
status: string status: string;
hostUserId: number hostUserId: number;
_count?: { registrations: number } _count?: { registrations: number };
}) => ({ }) => ({
id: e.id, id: e.id,
title: e.title, title: e.title,
@@ -40,7 +40,7 @@ export default async function EventsPage({
}, },
paramsPromise, paramsPromise,
searchParams, searchParams,
) );
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -64,5 +64,5 @@ export default async function EventsPage({
}} }}
/> />
</div> </div>
) );
} }
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -1,17 +1,17 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from '@/lib/prisma' import { prisma } from "@/lib/prisma";
import { EventTypesManager } from './event-types-manager' import { EventTypesManager } from "./event-types-manager";
export default async function EventTypesPage() { export default async function EventTypesPage() {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.EVENTS_EDIT, session.user.rank)) { if (!canAccess(permissions, PERMS.EVENTS_EDIT, session.user.rank)) {
redirect('/admin/events') redirect("/admin/events");
} }
const eventTypes = await prisma.websiteEventType.findMany({ const eventTypes = await prisma.websiteEventType.findMany({
orderBy: { name: 'asc' }, orderBy: { name: "asc" },
}) });
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -22,5 +22,5 @@ export default async function EventTypesPage() {
<EventTypesManager eventTypes={eventTypes} /> <EventTypesManager eventTypes={eventTypes} />
</div> </div>
) );
} }
+3 -1
View File
@@ -60,7 +60,9 @@ export default async function AdminHelpQuestions() {
<tbody> <tbody>
{entries.map((e) => ( {entries.map((e) => (
<tr key={String(e.id)}> <tr key={String(e.id)}>
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">{e.position}</td> <td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
{e.position}
</td>
<td> <td>
<Link href={`/admin/help-questions/${e.id}`}>{e.name}</Link> <Link href={`/admin/help-questions/${e.id}`}>{e.name}</Link>
</td> </td>
@@ -1,62 +1,56 @@
'use client' "use client";
import { Loader2 } from 'lucide-react' import { Loader2 } from "lucide-react";
import { useEffect, useState } from 'react' import { useEffect, useState } from "react";
import { toast } from 'sonner' import { toast } from "sonner";
import { getBadgeData, updateBadge } from '@/actions/badges' import { getBadgeData, updateBadge } from "@/actions/badges";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
Dialog, import { Input } from "@/components/ui/input";
DialogContent, import { Label } from "@/components/ui/label";
DialogFooter, import { useServerAction } from "@/hooks/use-server-action";
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useServerAction } from '@/hooks/use-server-action'
interface BadgeEditorDialogProps { interface BadgeEditorDialogProps {
code: string code: string;
open: boolean open: boolean;
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void;
/** Called after a successful save so the parent can refresh state */ /** Called after a successful save so the parent can refresh state */
onSaved?: () => void onSaved?: () => void;
} }
export function BadgeEditorDialog({ code, open, onOpenChange, onSaved }: BadgeEditorDialogProps) { export function BadgeEditorDialog({ code, open, onOpenChange, onSaved }: BadgeEditorDialogProps) {
const [name, setName] = useState('') const [name, setName] = useState("");
const [desc, setDesc] = useState('') const [desc, setDesc] = useState("");
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const { run, isPending: saving } = useServerAction() const { run, isPending: saving } = useServerAction();
// Fetch current values when dialog opens // Fetch current values when dialog opens
useEffect(() => { useEffect(() => {
if (!open || !code) return if (!open || !code) return;
setLoading(true) setLoading(true);
getBadgeData({ code }) getBadgeData({ code })
.then((result) => { .then((result) => {
if (result.ok) { if (result.ok) {
const data = result.data as { name: string; desc: string } const data = result.data as { name: string; desc: string };
setName(data.name ?? '') setName(data.name ?? "");
setDesc(data.desc ?? '') setDesc(data.desc ?? "");
} }
}) })
.catch(() => { .catch(() => {
toast.error('Failed to load badge data') toast.error("Failed to load badge data");
}) })
.finally(() => setLoading(false)) .finally(() => setLoading(false));
}, [open, code]) }, [open, code]);
function handleSave() { function handleSave() {
run(() => updateBadge({ code, name, desc }), { run(() => updateBadge({ code, name, desc }), {
successMessage: `Badge ${code} updated`, successMessage: `Badge ${code} updated`,
errorMessage: 'Save failed', errorMessage: "Save failed",
onSuccess: () => { onSuccess: () => {
onSaved?.() onSaved?.();
onOpenChange(false) onOpenChange(false);
}, },
}) });
} }
return ( return (
@@ -104,5 +98,5 @@ export function BadgeEditorDialog({ code, open, onOpenChange, onSaved }: BadgeEd
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,4 +1,4 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from "@/components/ui/skeleton";
export default function ImportBadgesLoading() { export default function ImportBadgesLoading() {
return ( return (
@@ -17,5 +17,5 @@ export default function ImportBadgesLoading() {
<Skeleton className="h-10 w-24 mt-4" /> <Skeleton className="h-10 w-24 mt-4" />
</div> </div>
</div> </div>
) );
} }
+6 -6
View File
@@ -1,12 +1,12 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { ImportBadgesClient } from './import-badges-client' import { ImportBadgesClient } from "./import-badges-client";
export default async function ImportBadgesPage(_props: { params: Promise<{ locale: string }> }) { export default async function ImportBadgesPage(_props: { params: Promise<{ locale: string }> }) {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
return <ImportBadgesClient /> return <ImportBadgesClient />;
} }
File diff suppressed because it is too large. Load diff
+6 -6
View File
@@ -1,11 +1,11 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { ImportCloneClient } from './import-clone-client' import { ImportCloneClient } from "./import-clone-client";
export default async function ImportClonePage(_props: { params: Promise<{ locale: string }> }) { export default async function ImportClonePage(_props: { params: Promise<{ locale: string }> }) {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
return <ImportCloneClient /> return <ImportCloneClient />;
} }
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -9,5 +9,5 @@ export default function Loading() {
</div> </div>
))} ))}
</div> </div>
) );
} }
+6 -6
View File
@@ -1,11 +1,11 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { ImportClothingClient } from './import-clothing-client' import { ImportClothingClient } from "./import-clothing-client";
export default async function ImportClothingPage(_props: { params: Promise<{ locale: string }> }) { export default async function ImportClothingPage(_props: { params: Promise<{ locale: string }> }) {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
return <ImportClothingClient /> return <ImportClothingClient />;
} }
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -10,5 +10,5 @@ export default function Loading() {
</div> </div>
))} ))}
</div> </div>
) );
} }
+6 -6
View File
@@ -1,12 +1,12 @@
// src/app/[locale]/admin/import/effects/page.tsx // src/app/[locale]/admin/import/effects/page.tsx
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { ImportEffectsClient } from './import-effects-client' import { ImportEffectsClient } from "./import-effects-client";
export default async function ImportEffectsPage(_props: { params: Promise<{ locale: string }> }) { export default async function ImportEffectsPage(_props: { params: Promise<{ locale: string }> }) {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
return <ImportEffectsClient /> return <ImportEffectsClient />;
} }
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,4 +1,4 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from "@/components/ui/skeleton";
export default function ImportFurniLoading() { export default function ImportFurniLoading() {
return ( return (
@@ -17,5 +17,5 @@ export default function ImportFurniLoading() {
<Skeleton className="h-10 w-24 mt-4" /> <Skeleton className="h-10 w-24 mt-4" />
</div> </div>
</div> </div>
) );
} }
File diff suppressed because it is too large. Load diff
+6 -6
View File
@@ -1,12 +1,12 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { ImportFurniClient } from './import-furni-client' import { ImportFurniClient } from "./import-furni-client";
export default async function ImportFurniPage(_props: { params: Promise<{ locale: string }> }) { export default async function ImportFurniPage(_props: { params: Promise<{ locale: string }> }) {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
return <ImportFurniClient /> return <ImportFurniClient />;
} }
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -9,5 +9,5 @@ export default function Loading() {
</div> </div>
))} ))}
</div> </div>
) );
} }
+6 -6
View File
@@ -1,11 +1,11 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { ImportPetsClient } from './import-pets-client' import { ImportPetsClient } from "./import-pets-client";
export default async function ImportPetsPage(_props: { params: Promise<{ locale: string }> }) { export default async function ImportPetsPage(_props: { params: Promise<{ locale: string }> }) {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
return <ImportPetsClient /> return <ImportPetsClient />;
} }
+6 -6
View File
@@ -1,12 +1,12 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { RepairClient } from './repair-client' import { RepairClient } from "./repair-client";
export default async function RepairImportedPage() { export default async function RepairImportedPage() {
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
return <RepairClient /> return <RepairClient />;
} }
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -188,7 +188,9 @@ async function Sidebar({ staff }: { staff: { id: number; username: string; rank:
{initial} {initial}
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="font-semibold text-sm text-[var(--admin-sidebar-text-readable)] truncate">{staff.username}</div> <div className="font-semibold text-sm text-[var(--admin-sidebar-text-readable)] truncate">
{staff.username}
</div>
<span className="inline-block mt-0.5 text-[0.6rem] font-bold uppercase tracking-widest text-[var(--admin-sidebar-muted-readable)]"> <span className="inline-block mt-0.5 text-[0.6rem] font-bold uppercase tracking-widest text-[var(--admin-sidebar-muted-readable)]">
{t("rankLabel", { rank: staff.rank })} {t("rankLabel", { rank: staff.rank })}
</span> </span>
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,4 +1,4 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from "@/components/ui/skeleton";
export default function AuditLogsLoading() { export default function AuditLogsLoading() {
return ( return (
@@ -24,5 +24,5 @@ export default function AuditLogsLoading() {
))} ))}
</div> </div>
</div> </div>
) );
} }
+17 -17
View File
@@ -1,29 +1,29 @@
import { redirect } from 'next/navigation' import { redirect } from "next/navigation";
import { calcPagination, parseListParams } from '@/lib/admin-helpers' import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { canAccess, getAdminContext, PERMS } from '@/lib/permissions' import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { getAuditLogs } from '@/lib/services/audit' import { getAuditLogs } from "@/lib/services/audit";
import { AuditTable } from './audit-table' import { AuditTable } from "./audit-table";
export default async function AuditLogsPage({ export default async function AuditLogsPage({
params: paramsPromise, params: paramsPromise,
searchParams, searchParams,
}: { }: {
params: Promise<{ locale: string }> params: Promise<{ locale: string }>;
searchParams: Promise<Record<string, string>> searchParams: Promise<Record<string, string>>;
}) { }) {
await paramsPromise await paramsPromise;
const { session, permissions } = await getAdminContext() const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.LOGS_VIEW, session.user.rank)) { if (!canAccess(permissions, PERMS.LOGS_VIEW, session.user.rank)) {
redirect('/admin') redirect("/admin");
} }
const params = await searchParams const params = await searchParams;
const sp = new URLSearchParams(params) const sp = new URLSearchParams(params);
const { search, perPage, page } = parseListParams(sp) const { search, perPage, page } = parseListParams(sp);
const result = await getAuditLogs({ search, page, perPage }) const result = await getAuditLogs({ search, page, perPage });
const pagination = calcPagination(result.total, result.page, result.perPage) const pagination = calcPagination(result.total, result.page, result.perPage);
const rows = result.rows.map((r) => ({ const rows = result.rows.map((r) => ({
id: r.id, id: r.id,
@@ -33,7 +33,7 @@ export default async function AuditLogsPage({
targetId: r.targetId, targetId: r.targetId,
diff: r.diff, diff: r.diff,
createdAt: r.createdAt, createdAt: r.createdAt,
})) }));
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -44,5 +44,5 @@ export default async function AuditLogsPage({
<AuditTable data={{ rows, ...pagination }} /> <AuditTable data={{ rows, ...pagination }} />
</div> </div>
) );
} }
+19 -19
View File
@@ -1,36 +1,36 @@
'use client' "use client";
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";
interface ChatLogRow { interface ChatLogRow {
id: number id: number;
username: string username: string;
message: string message: string;
timestamp: number timestamp: number;
roomId: number roomId: number;
} }
const columns: DataTableColumn<ChatLogRow>[] = [ const columns: DataTableColumn<ChatLogRow>[] = [
{ key: 'id', label: 'ID', sortable: true }, { key: "id", label: "ID", sortable: true },
{ key: 'username', label: 'User' }, { key: "username", label: "User" },
{ {
key: 'message', key: "message",
label: 'Message', label: "Message",
render: (value) => <span className="max-w-[300px] truncate block">{String(value)}</span>, render: (value) => <span className="max-w-[300px] truncate block">{String(value)}</span>,
}, },
{ {
key: 'timestamp', key: "timestamp",
label: 'Time', label: "Time",
sortable: true, sortable: true,
render: (value) => <span>{formatTimestamp(Number(value))}</span>, render: (value) => <span>{formatTimestamp(Number(value))}</span>,
}, },
{ key: 'roomId', label: 'Room ID', sortable: true }, { 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) {
@@ -41,5 +41,5 @@ export function ChatTable({ data }: ChatTableProps) {
searchPlaceholder="Search by username or message..." searchPlaceholder="Search by username or message..."
exportUrl="/api/admin/export?type=chatlogs" exportUrl="/api/admin/export?type=chatlogs"
/> />
) );
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from "@/components/ui/skeleton";
export default function ChatLogsLoading() { export default function ChatLogsLoading() {
return ( return (
@@ -24,5 +24,5 @@ export default function ChatLogsLoading() {
))} ))}
</div> </div>
</div> </div>
) );
} }
+9 -9
View File
@@ -1,19 +1,19 @@
import { fetchAdminList } from '@/lib/admin-list' import { fetchAdminList } from "@/lib/admin-list";
import { PERMS } from '@/lib/permissions' import { PERMS } from "@/lib/permissions";
import { ChatTable } from './chat-table' import { ChatTable } from "./chat-table";
export default async function ChatLogsPage({ export default async function ChatLogsPage({
params: paramsPromise, params: paramsPromise,
searchParams, searchParams,
}: { }: {
params: Promise<{ locale: string }> params: Promise<{ locale: string }>;
searchParams: Promise<Record<string, string>> searchParams: Promise<Record<string, string>>;
}) { }) {
const result = await fetchAdminList( const result = await fetchAdminList(
{ {
permission: PERMS.LOGS_VIEW, permission: PERMS.LOGS_VIEW,
model: 'chatLog', model: "chatLog",
searchFields: ['message', 'user.username'], searchFields: ["message", "user.username"],
defaultPerPage: 50, defaultPerPage: 50,
include: { user: { select: { username: true } } }, include: { user: { select: { username: true } } },
mapRow: (l) => ({ mapRow: (l) => ({
@@ -26,7 +26,7 @@ export default async function ChatLogsPage({
}, },
paramsPromise, paramsPromise,
searchParams, searchParams,
) );
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -45,5 +45,5 @@ export default async function ChatLogsPage({
}} }}
/> />
</div> </div>
) );
} }
+20 -20
View File
@@ -1,38 +1,38 @@
'use client' "use client";
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";
interface CommandLogRow { interface CommandLogRow {
id: number id: number;
username: string username: string;
command: string command: string;
params: string params: string;
timestamp: number timestamp: number;
} }
const columns: DataTableColumn<CommandLogRow>[] = [ const columns: DataTableColumn<CommandLogRow>[] = [
{ key: 'id', label: 'ID', sortable: true }, { key: "id", label: "ID", sortable: true },
{ key: 'username', label: 'User' }, { key: "username", label: "User" },
{ key: 'command', label: 'Command', sortable: true }, { key: "command", label: "Command", sortable: true },
{ {
key: 'params', key: "params",
label: 'Params', label: "Params",
render: (value) => ( render: (value) => (
<span className="max-w-[200px] truncate block font-mono text-sm">{String(value) || '-'}</span> <span className="max-w-[200px] truncate block font-mono text-sm">{String(value) || "-"}</span>
), ),
}, },
{ {
key: 'timestamp', key: "timestamp",
label: 'Time', label: "Time",
sortable: true, sortable: true,
render: (value) => <span>{formatTimestamp(Number(value))}</span>, 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) {
@@ -43,5 +43,5 @@ export function CommandsTable({ data }: CommandsTableProps) {
searchPlaceholder="Search by username, command, or params..." searchPlaceholder="Search by username, command, or params..."
exportUrl="/api/admin/export?type=commands" exportUrl="/api/admin/export?type=commands"
/> />
) );
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from "@/components/ui/skeleton";
export default function CommandLogsLoading() { export default function CommandLogsLoading() {
return ( return (
@@ -24,5 +24,5 @@ export default function CommandLogsLoading() {
))} ))}
</div> </div>
</div> </div>
) );
} }
+9 -9
View File
@@ -1,19 +1,19 @@
import { fetchAdminList } from '@/lib/admin-list' import { fetchAdminList } from "@/lib/admin-list";
import { PERMS } from '@/lib/permissions' import { PERMS } from "@/lib/permissions";
import { CommandsTable } from './commands-table' import { CommandsTable } from "./commands-table";
export default async function CommandLogsPage({ export default async function CommandLogsPage({
params: paramsPromise, params: paramsPromise,
searchParams, searchParams,
}: { }: {
params: Promise<{ locale: string }> params: Promise<{ locale: string }>;
searchParams: Promise<Record<string, string>> searchParams: Promise<Record<string, string>>;
}) { }) {
const result = await fetchAdminList( const result = await fetchAdminList(
{ {
permission: PERMS.LOGS_VIEW, permission: PERMS.LOGS_VIEW,
model: 'commandLog', model: "commandLog",
searchFields: ['command', 'params', 'user.username'], searchFields: ["command", "params", "user.username"],
defaultPerPage: 50, defaultPerPage: 50,
include: { user: { select: { username: true } } }, include: { user: { select: { username: true } } },
mapRow: (l) => ({ mapRow: (l) => ({
@@ -26,7 +26,7 @@ export default async function CommandLogsPage({
}, },
paramsPromise, paramsPromise,
searchParams, searchParams,
) );
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -45,5 +45,5 @@ export default async function CommandLogsPage({
}} }}
/> />
</div> </div>
) );
} }
+7 -1
View File
@@ -15,7 +15,13 @@ function fromDate(d: Date | null | undefined): string {
export default async function AdminStaffActivities({ export default async function AdminStaffActivities({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ q?: string; page?: string; staffId?: string; action?: string; authorization?: string }>; searchParams: Promise<{
q?: string;
page?: string;
staffId?: string;
action?: string;
authorization?: string;
}>;
}) { }) {
const t = await getTranslations("pages.admin.logs"); const t = await getTranslations("pages.admin.logs");
+2 -2
View File
@@ -1,4 +1,4 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from "@/components/ui/skeleton";
export default function TradeLogsLoading() { export default function TradeLogsLoading() {
return ( return (
@@ -24,5 +24,5 @@ export default function TradeLogsLoading() {
))} ))}
</div> </div>
</div> </div>
) );
} }
+9 -9
View File
@@ -1,19 +1,19 @@
import { fetchAdminList } from '@/lib/admin-list' import { fetchAdminList } from "@/lib/admin-list";
import { PERMS } from '@/lib/permissions' import { PERMS } from "@/lib/permissions";
import { TradesTable } from './trades-table' import { TradesTable } from "./trades-table";
export default async function TradeLogsPage({ export default async function TradeLogsPage({
params: paramsPromise, params: paramsPromise,
searchParams, searchParams,
}: { }: {
params: Promise<{ locale: string }> params: Promise<{ locale: string }>;
searchParams: Promise<Record<string, string>> searchParams: Promise<Record<string, string>>;
}) { }) {
const result = await fetchAdminList( const result = await fetchAdminList(
{ {
permission: PERMS.LOGS_VIEW, permission: PERMS.LOGS_VIEW,
model: 'trade', model: "trade",
searchFields: ['userOne.username', 'userTwo.username'], searchFields: ["userOne.username", "userTwo.username"],
defaultPerPage: 50, defaultPerPage: 50,
include: { include: {
userOne: { select: { username: true } }, userOne: { select: { username: true } },
@@ -29,7 +29,7 @@ export default async function TradeLogsPage({
}, },
paramsPromise, paramsPromise,
searchParams, searchParams,
) );
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -48,5 +48,5 @@ export default async function TradeLogsPage({
}} }}
/> />
</div> </div>
) );
} }
Loaded 100 of 376 files, more files were not shown because too many files have changed in this diff. Show more