revert fb8e77bb68
Local Build and Deploy / deploy (push) Successful in 1m11s
Local Build and Deploy / deploy (push) Successful in 1m11s
revert style: clean up code with prettier and eslint
This commit is contained in:
1 parent
27de078f54
commit
e85e4d74ea
377 files changed
+20697
-22415
No files matched your search
Generated
+3394
-5872
File diff suppressed because it is too large.
Load diff
@@ -3,9 +3,7 @@ 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(
|
const result = mysqlConnectionUrl("mysql://user:pass@localhost:3306/cms?connection_limit=20&pool_timeout=30&charset=utf8mb4");
|
||||||
"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,5 +1,7 @@
|
|||||||
export function splitSqlStatements(sql: string): string[] {
|
export function splitSqlStatements(sql: string): string[] {
|
||||||
const withoutComments = sql.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*--.*$/gm, "");
|
const withoutComments = sql
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||||
|
.replace(/^\s*--.*$/gm, "");
|
||||||
|
|
||||||
return withoutComments
|
return withoutComments
|
||||||
.split(";")
|
.split(";")
|
||||||
|
|||||||
@@ -19,13 +19,7 @@ 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: {
|
create: { badgeKey: code, badgeName: name, badgeDescription: desc, createdAt: new Date(), updatedAt: new Date() },
|
||||||
badgeKey: code,
|
|
||||||
badgeName: name,
|
|
||||||
badgeDescription: desc,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
revalidatePath("/admin/import/badges");
|
revalidatePath("/admin/import/badges");
|
||||||
}
|
}
|
||||||
+34
-34
File diff suppressed because it is too large.
Load diff
@@ -7,11 +7,7 @@ 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({
|
export async function bulkUnban({ userIds }: { userIds: number[] }): Promise<ActionResult<{ unbanned: number; total: number }>> {
|
||||||
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({
|
||||||
@@ -23,15 +19,7 @@ export async function bulkUnban({
|
|||||||
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({
|
export async function bulkBan({ userIds, reason, duration }: { userIds: number[]; reason: string; duration: number }): Promise<ActionResult<{ banned: number }>> {
|
||||||
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;
|
||||||
@@ -65,17 +53,7 @@ export async function bulkBan({
|
|||||||
return { ok: true as const, data: { banned } };
|
return { ok: true as const, data: { banned } };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bulkGiveCurrency({
|
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 }> }>> {
|
||||||
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 }> = [];
|
||||||
@@ -115,15 +93,7 @@ export async function bulkGiveCurrency({
|
|||||||
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({
|
export async function bulkGiveBadge({ userIds, badgeCode }: { userIds: number[]; badgeCode: string }): Promise<ActionResult<{ given: number; total: number; failedIds: Array<{ userId: number; reason: string }> }>> {
|
||||||
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 }> = [];
|
||||||
|
|||||||
@@ -33,16 +33,7 @@ export async function deleteBcItem({ id }: { id: number }) {
|
|||||||
revalidatePath("/admin/catalog/builder-club");
|
revalidatePath("/admin/catalog/builder-club");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateBcItem({
|
export async function updateBcItem({ id, ...data }: { id: number; itemIds?: string; catalogName?: string; orderNumber?: number; extradata?: string }) {
|
||||||
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({
|
||||||
@@ -55,16 +46,7 @@ export async function updateBcItem({
|
|||||||
revalidatePath("/admin/catalog/builder-club");
|
revalidatePath("/admin/catalog/builder-club");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBcItem({
|
export async function createBcItem({ pageId, ...data }: { pageId: number; itemIds: string; catalogName: string; orderNumber: number; extradata: string }) {
|
||||||
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 },
|
||||||
@@ -81,10 +63,7 @@ export async function createBcItem({
|
|||||||
|
|
||||||
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({
|
const page = await prisma.catalogPagesBc.findUnique({ where: { id }, select: { enabled: true, visible: true } });
|
||||||
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 },
|
||||||
|
|||||||
@@ -62,11 +62,7 @@ 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({
|
export async function reorderCatalogItems({ orders }: { orders: Array<{ id: number; orderNumber: number }> }) {
|
||||||
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 } });
|
||||||
@@ -76,15 +72,7 @@ export async function reorderCatalogItems({
|
|||||||
return { ok: true as const, data: {} };
|
return { ok: true as const, data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCatalogItem({
|
export async function updateCatalogItem({ id, catalogFields, baseItem }: { id: number; catalogFields: Record<string, unknown>; baseItem?: { id: number; fields: Record<string, unknown> } }) {
|
||||||
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) {
|
||||||
@@ -102,22 +90,15 @@ export async function updateCatalogItem({
|
|||||||
return { ok: true as const, data: {} };
|
return { ok: true as const, data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function translateCatalogItems({
|
export async function translateCatalogItems({ items }: { items: Array<{ id: number; publicName: string; description: string }> }) {
|
||||||
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;
|
||||||
const furniDataInserted = 0;
|
let furniDataInserted = 0;
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const existing = await prisma.catalogItems.findUnique({
|
const existing = await prisma.catalogItems.findUnique({ where: { id: item.id }, select: { catalogName: true } });
|
||||||
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) {
|
||||||
@@ -146,14 +127,5 @@ export async function translateCatalogItems({
|
|||||||
|
|
||||||
await rcon.updateCatalog();
|
await rcon.updateCatalog();
|
||||||
revalidatePath("/admin/catalog");
|
revalidatePath("/admin/catalog");
|
||||||
return {
|
return { ok: true as const, data: { namesUpdated, descriptionsUpdated, furniDataUpdated, furniDataInserted: 0, updated: items.length } };
|
||||||
ok: true as const,
|
|
||||||
data: {
|
|
||||||
namesUpdated,
|
|
||||||
descriptionsUpdated,
|
|
||||||
furniDataUpdated,
|
|
||||||
furniDataInserted: 0,
|
|
||||||
updated: items.length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
+9
-27
@@ -7,10 +7,7 @@ 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({
|
export async function updateCatalogPage({ id, ...fields }: { id: number } & Record<string, unknown>): Promise<ActionResult> {
|
||||||
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();
|
||||||
@@ -40,18 +37,9 @@ export async function deleteCatalogPage({ id }: { id: number }) {
|
|||||||
return { ok: true as const, data: {} };
|
return { ok: true as const, data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleCatalogPage({
|
export async function toggleCatalogPage({ id, action }: { id: number; action: "toggleEnabled" | "toggleVisible" }) {
|
||||||
id,
|
|
||||||
action,
|
|
||||||
}: {
|
|
||||||
id: number;
|
|
||||||
action: "toggleEnabled" | "toggleVisible";
|
|
||||||
}) {
|
|
||||||
await requireStaff();
|
await requireStaff();
|
||||||
const page = await prisma.catalogPages.findUnique({
|
const page = await prisma.catalogPages.findUnique({ where: { id }, select: { enabled: true, visible: true } });
|
||||||
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;
|
||||||
@@ -63,17 +51,7 @@ export async function toggleCatalogPage({
|
|||||||
return { ok: true as const, data: {} };
|
return { ok: true as const, data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCatalogPage(input: {
|
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 }>> {
|
||||||
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: {
|
||||||
@@ -105,7 +83,11 @@ export async function createCatalogPage(input: {
|
|||||||
return { ok: true as const, data: { id: created.id } };
|
return { ok: true as const, data: { id: created.id } };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function reorderTreePage(input: { pageId: number; newParentId?: number; newOrderNum: number }) {
|
export async function reorderTreePage(input: {
|
||||||
|
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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -3,27 +3,13 @@
|
|||||||
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({
|
export async function importBadgeFromRemote({ code, name, description }: { code: string; name: string; description: string }) {
|
||||||
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: {
|
create: { badgeKey: code, badgeName: name, badgeDescription: description, createdAt: new Date(), updatedAt: new Date() },
|
||||||
badgeKey: code,
|
|
||||||
badgeName: name,
|
|
||||||
badgeDescription: description,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return { ok: true as const };
|
return { ok: true as const };
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+12
-13
@@ -1,20 +1,19 @@
|
|||||||
"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) =>
|
async (ctx) => actionOk((await deleteImportedItem(ctx.data.classname)) as unknown as Record<string, unknown>),
|
||||||
actionOk((await deleteImportedItem(ctx.data.classname)) as unknown as Record<string, unknown>),
|
)
|
||||||
);
|
|
||||||
+75
-75
File diff suppressed because it is too large.
Load diff
+52
-90
File diff suppressed because it is too large.
Load diff
+45
-45
File diff suppressed because it is too large.
Load diff
+45
-45
File diff suppressed because it is too large.
Load diff
+1
-10
@@ -71,16 +71,7 @@ export async function deleteRoom({ id }: { id: number }) {
|
|||||||
revalidatePath("/admin/rooms");
|
revalidatePath("/admin/rooms");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateRoom({
|
export async function updateRoom({ id, ...data }: { id: number; name?: string; description?: string; state?: string; usersMax?: number }) {
|
||||||
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({
|
||||||
|
|||||||
@@ -10,19 +10,7 @@ export async function deleteSoundtrack({ id }: { id: number }) {
|
|||||||
revalidatePath("/admin/sounds");
|
revalidatePath("/admin/sounds");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSoundtrack({
|
export async function updateSoundtrack({ id, name, author, track, length }: { id: number; name: string; author: string; track: string; length: number }) {
|
||||||
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 },
|
||||||
|
|||||||
@@ -1,45 +1,47 @@
|
|||||||
"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.partial().extend({ id: z.coerce.number().int().positive() });
|
const updateTemplateInput = templateSchema
|
||||||
|
.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
File diff suppressed because it is too large.
Load diff
+56
-53
File diff suppressed because it is too large.
Load diff
+213
-192
File diff suppressed because it is too large.
Load diff
+19
-19
@@ -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
@@ -39,11 +39,7 @@ export default async function EditArticle({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error ? (
|
{error ? <p className="text-xs text-[var(--admin-error)] dark:text-[var(--admin-error)] mb-4">Error: {error}</p> : null}
|
||||||
<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";
|
||||||
|
|||||||
@@ -21,11 +21,7 @@ export default async function NewArticle({ searchParams }: { searchParams: Promi
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error ? (
|
{error ? <p className="text-xs text-[var(--admin-error)] dark:text-[var(--admin-error)] mb-4">Error: {error}</p> : null}
|
||||||
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,11 +35,7 @@ export default async function AdminArticles({ searchParams }: { searchParams: Pr
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{error ? <p className="text-xs text-[var(--admin-error)] dark:text-[var(--admin-error)] mb-4">Error: {error}</p> : null}
|
||||||
<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
@@ -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,53 +1,59 @@
|
|||||||
"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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import {
|
||||||
|
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
|
||||||
@@ -75,12 +81,14 @@ 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">{formatLength(opt.length)}</span>
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
|
{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
@@ -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
@@ -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
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
+146
-141
File diff suppressed because it is too large.
Load diff
@@ -616,7 +616,7 @@ export default async function CommandoCentrum() {
|
|||||||
{e.version}
|
{e.version}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<pre className="text-xs p-2 bg-[var(--admin-canvas)] text-[var(--color-text-readable)] rounded overflow-auto max-h-[160px]">
|
<pre className="text-xs p-2 bg-[var(--admin-canvas)] text-[var(--color-text-readable)] rounded overflow-auto max-h-[160px]">
|
||||||
{snippet}
|
{snippet}
|
||||||
</pre>
|
</pre>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,55 @@
|
|||||||
"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 variant={status === "online" ? "default" : status === "offline" ? "destructive" : "secondary"}>
|
<Badge
|
||||||
{status === "checking" ? "Checking..." : status === "online" ? "Online" : "Offline"}
|
variant={
|
||||||
|
status === 'online' ? 'default' : status === 'offline' ? 'destructive' : 'secondary'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{status === 'checking' ? 'Checking...' : status === 'online' ? 'Online' : 'Offline'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={checkEmulator} disabled={checking}>
|
<Button
|
||||||
<RefreshCw className={`h-3 w-3 ${checking ? "animate-spin" : ""}`} />
|
variant="ghost"
|
||||||
|
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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -60,9 +60,7 @@ 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">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">{e.position}</td>
|
||||||
{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,56 +1,62 @@
|
|||||||
"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 { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import {
|
||||||
import { Input } from "@/components/ui/input";
|
Dialog,
|
||||||
import { Label } from "@/components/ui/label";
|
DialogContent,
|
||||||
import { useServerAction } from "@/hooks/use-server-action";
|
DialogFooter,
|
||||||
|
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 (
|
||||||
@@ -98,5 +104,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
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -9,5 +9,5 @@ export default function Loading() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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
@@ -10,5 +10,5 @@ export default function Loading() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -9,5 +9,5 @@ export default function Loading() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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 />
|
||||||
}
|
}
|
||||||
@@ -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
@@ -188,9 +188,7 @@ 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">
|
<div className="font-semibold text-sm text-[var(--admin-sidebar-text-readable)] truncate">{staff.username}</div>
|
||||||
{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
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -15,13 +15,7 @@ function fromDate(d: Date | null | undefined): string {
|
|||||||
export default async function AdminStaffActivities({
|
export default async function AdminStaffActivities({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{ q?: string; page?: string; staffId?: string; action?: string; authorization?: string }>;
|
||||||
q?: string;
|
|
||||||
page?: string;
|
|
||||||
staffId?: string;
|
|
||||||
action?: string;
|
|
||||||
authorization?: string;
|
|
||||||
}>;
|
|
||||||
}) {
|
}) {
|
||||||
const t = await getTranslations("pages.admin.logs");
|
const t = await getTranslations("pages.admin.logs");
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
Loaded 100 of 377 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user