refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (3)
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
2cd0863cb8
commit
580972c0a0
40 files changed
+1075
-794
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,10 +1,11 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { deleteHelpQuestion, updateHelpQuestion } from "@/actions/admin-help";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function EditHelpQuestion({
|
||||
params,
|
||||
@@ -20,13 +21,14 @@ export default async function EditHelpQuestion({
|
||||
|
||||
const { id } = await params;
|
||||
const { error } = await searchParams;
|
||||
let entry: Awaited<
|
||||
ReturnType<typeof prisma.websiteHelpCenterCategories.findUnique>
|
||||
> = null;
|
||||
let entry: typeof WebsiteHelpCenterCategories.$inferSelect | null = null;
|
||||
try {
|
||||
entry = await prisma.websiteHelpCenterCategories.findUnique({
|
||||
where: { id: BigInt(id) },
|
||||
});
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(WebsiteHelpCenterCategories)
|
||||
.where(eq(WebsiteHelpCenterCategories.id, BigInt(id)))
|
||||
.limit(1);
|
||||
entry = row ?? null;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function AdminHelpQuestions() {
|
||||
const { session, permissions } = await getAdminContext();
|
||||
@@ -13,14 +14,16 @@ export default async function AdminHelpQuestions() {
|
||||
}
|
||||
|
||||
const t = await getTranslations("pages.admin.helpCenter");
|
||||
let entries: Awaited<
|
||||
ReturnType<typeof prisma.websiteHelpCenterCategories.findMany>
|
||||
> = [];
|
||||
let entries: (typeof WebsiteHelpCenterCategories.$inferSelect)[] = [];
|
||||
try {
|
||||
entries = await prisma.websiteHelpCenterCategories.findMany({
|
||||
orderBy: [{ position: "asc" }, { id: "asc" }],
|
||||
take: 500,
|
||||
});
|
||||
entries = await db
|
||||
.select()
|
||||
.from(WebsiteHelpCenterCategories)
|
||||
.orderBy(
|
||||
asc(WebsiteHelpCenterCategories.position),
|
||||
asc(WebsiteHelpCenterCategories.id),
|
||||
)
|
||||
.limit(500);
|
||||
} catch {
|
||||
entries = [];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,15 @@
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
AclModelPermission,
|
||||
AclPermission,
|
||||
AclRole,
|
||||
db,
|
||||
WebsiteHousekeepingPermissions,
|
||||
} from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getAuditLogs } from "@/lib/services/audit";
|
||||
import { AuditSection, ExportSection } from "./client";
|
||||
|
||||
@@ -31,12 +38,14 @@ export default async function AdminHousekeeping({
|
||||
const sp = await searchParams;
|
||||
const tab = sp.tab ?? "overview";
|
||||
|
||||
type HKPerm = Awaited<
|
||||
ReturnType<typeof prisma.websiteHousekeepingPermissions.findMany>
|
||||
>[0];
|
||||
const allPermissions: HKPerm[] = await prisma.websiteHousekeepingPermissions
|
||||
.findMany({ orderBy: [{ groupName: "asc" }, { permission: "asc" }] })
|
||||
.catch(() => []);
|
||||
const allPermissions = await db
|
||||
.select()
|
||||
.from(WebsiteHousekeepingPermissions)
|
||||
.orderBy(
|
||||
asc(WebsiteHousekeepingPermissions.groupName),
|
||||
asc(WebsiteHousekeepingPermissions.permission),
|
||||
)
|
||||
.catch((): (typeof WebsiteHousekeepingPermissions.$inferSelect)[] => []);
|
||||
|
||||
const ranks = allPermissions.map((p) => p.minRank);
|
||||
const maxRank = ranks.length ? Math.max(...ranks) : 0;
|
||||
@@ -48,34 +57,43 @@ export default async function AdminHousekeeping({
|
||||
groupCounts[g] = (groupCounts[g] || 0) + 1;
|
||||
}
|
||||
|
||||
const rankGroups: Record<string, typeof allPermissions> = {};
|
||||
const rankGroups: Record<
|
||||
string,
|
||||
(typeof WebsiteHousekeepingPermissions.$inferSelect)[]
|
||||
> = {};
|
||||
for (const p of allPermissions) {
|
||||
if (!rankGroups[p.minRank]) rankGroups[p.minRank] = [];
|
||||
rankGroups[p.minRank].push(p);
|
||||
const rk = String(p.minRank);
|
||||
if (!rankGroups[rk]) rankGroups[rk] = [];
|
||||
rankGroups[rk].push(p);
|
||||
}
|
||||
|
||||
const aclSlugs = await prisma.aclPermission
|
||||
.findMany({ select: { slug: true } })
|
||||
const aclSlugs = await db
|
||||
.select({ slug: AclPermission.slug })
|
||||
.from(AclPermission)
|
||||
.catch(() => [] as { slug: string }[]);
|
||||
const aclSlugSet = new Set(aclSlugs.map((p) => p.slug));
|
||||
|
||||
const aclRolePerms = await prisma.aclModelPermission
|
||||
.findMany({
|
||||
where: { modelType: "Role" },
|
||||
include: {
|
||||
permission: { select: { slug: true } },
|
||||
role: { select: { slug: true } },
|
||||
},
|
||||
const aclRolePerms = await db
|
||||
.select({
|
||||
roleSlug: AclRole.slug,
|
||||
permissionSlug: AclPermission.slug,
|
||||
})
|
||||
.catch(() => []);
|
||||
.from(AclModelPermission)
|
||||
.innerJoin(
|
||||
AclPermission,
|
||||
eq(AclModelPermission.permissionId, AclPermission.id),
|
||||
)
|
||||
.innerJoin(AclRole, eq(AclModelPermission.modelId, AclRole.id))
|
||||
.where(eq(AclModelPermission.modelType, "Role"))
|
||||
.catch((): { roleSlug: string; permissionSlug: string }[] => []);
|
||||
|
||||
const aclMinRankBySlug = new Map<string, number>();
|
||||
for (const row of aclRolePerms) {
|
||||
const roleSlug = row.role.slug;
|
||||
const roleSlug = row.roleSlug;
|
||||
if (!roleSlug.startsWith("rank_")) continue;
|
||||
const rank = Number(roleSlug.slice(5));
|
||||
if (!Number.isFinite(rank)) continue;
|
||||
const slug = row.permission.slug;
|
||||
const slug = row.permissionSlug;
|
||||
const prev = aclMinRankBySlug.get(slug);
|
||||
if (prev === undefined || rank < prev) aclMinRankBySlug.set(slug, rank);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
@@ -8,9 +9,9 @@ import {
|
||||
} from "@/actions/admin-ip";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteIpBlacklist, WebsiteIpWhitelist } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type IpRow = {
|
||||
id: bigint;
|
||||
@@ -30,19 +31,21 @@ export default async function AdminIp() {
|
||||
let blacklist: IpRow[] = [];
|
||||
|
||||
try {
|
||||
whitelist = await prisma.websiteIpWhitelist.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 200,
|
||||
});
|
||||
whitelist = await db
|
||||
.select()
|
||||
.from(WebsiteIpWhitelist)
|
||||
.orderBy(desc(WebsiteIpWhitelist.id))
|
||||
.limit(200);
|
||||
} catch {
|
||||
whitelist = [];
|
||||
}
|
||||
|
||||
try {
|
||||
blacklist = await prisma.websiteIpBlacklist.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 200,
|
||||
});
|
||||
blacklist = await db
|
||||
.select()
|
||||
.from(WebsiteIpBlacklist)
|
||||
.orderBy(desc(WebsiteIpBlacklist.id))
|
||||
.limit(200);
|
||||
} catch {
|
||||
blacklist = [];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Package } from "lucide-react";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { db, ItemsBase } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { ItemsBaseEditForm } from "./items-base-edit-form";
|
||||
|
||||
export default async function ItemsBaseDetailPage({
|
||||
@@ -21,7 +22,11 @@ export default async function ItemsBaseDetailPage({
|
||||
const id = Number(rawId);
|
||||
if (!Number.isFinite(id) || id <= 0) notFound();
|
||||
|
||||
const item = await prisma.itemsBase.findUnique({ where: { id } });
|
||||
const [item] = await db
|
||||
.select()
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.id, id))
|
||||
.limit(1);
|
||||
if (!item) notFound();
|
||||
|
||||
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { LogOut } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -15,9 +16,9 @@ import {
|
||||
ADMIN_NAV_CONFIG_KEY,
|
||||
parseAdminNavConfig,
|
||||
} from "@/lib/admin-nav-config";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { setCsrfCookie } from "@/lib/foundation/security";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
// Request-time auth: requireStaff, CSRF cookie, and optional 2FA gate cannot be
|
||||
@@ -37,11 +38,12 @@ export default async function AdminLayout({
|
||||
csrfToken = "";
|
||||
}
|
||||
if (await siteSettings.getBool("force_staff_2fa", false)) {
|
||||
const u = await prisma.user
|
||||
.findUnique({
|
||||
where: { id: staff.id },
|
||||
select: { twoFactorConfirmedAt: true },
|
||||
})
|
||||
const u = await db
|
||||
.select({ twoFactorConfirmedAt: User.twoFactorConfirmedAt })
|
||||
.from(User)
|
||||
.where(eq(User.id, staff.id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0] ?? null)
|
||||
.catch(() => null);
|
||||
if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired");
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+57
-36
File diff suppressed because it is too large.
Load diff
@@ -1,10 +1,11 @@
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { saveMaintenance } from "@/actions/admin-maintenance";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteSetting } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const KEYS = [
|
||||
"maintenance_enabled",
|
||||
@@ -22,10 +23,10 @@ export default async function AdminMaintenance() {
|
||||
|
||||
let rows: { key: string; value: string }[] = [];
|
||||
try {
|
||||
rows = await prisma.websiteSetting.findMany({
|
||||
where: { key: { in: [...KEYS] } },
|
||||
select: { key: true, value: true },
|
||||
});
|
||||
rows = await db
|
||||
.select({ key: WebsiteSetting.key, value: WebsiteSetting.value })
|
||||
.from(WebsiteSetting)
|
||||
.where(inArray(WebsiteSetting.key, [...KEYS]));
|
||||
} catch {
|
||||
rows = [];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { count, desc, eq, inArray } from "drizzle-orm";
|
||||
import { Store } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -8,9 +9,9 @@ import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, MarketplaceItems, User } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function AdminMarketplacePage({
|
||||
searchParams,
|
||||
@@ -28,27 +29,37 @@ export default async function AdminMarketplacePage({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const filter = raw.filter === "sold" ? "sold" : "active";
|
||||
|
||||
const where = filter === "sold" ? { state: 2 } : { state: 1 };
|
||||
const where = eq(MarketplaceItems.state, filter === "sold" ? 2 : 1);
|
||||
|
||||
let total = 0;
|
||||
let rows: Awaited<ReturnType<typeof prisma.marketplaceItems.findMany>> = [];
|
||||
let rows: (typeof MarketplaceItems.$inferSelect)[] = [];
|
||||
let activeCount = 0;
|
||||
let soldCount = 0;
|
||||
let loadError = false;
|
||||
|
||||
try {
|
||||
[total, activeCount, soldCount] = await Promise.all([
|
||||
prisma.marketplaceItems.count({ where }),
|
||||
prisma.marketplaceItems.count({ where: { state: 1 } }),
|
||||
prisma.marketplaceItems.count({ where: { state: 2 } }),
|
||||
const [totalRows, activeRows, soldRows] = await Promise.all([
|
||||
db.select({ total: count() }).from(MarketplaceItems).where(where),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(MarketplaceItems)
|
||||
.where(eq(MarketplaceItems.state, 1)),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(MarketplaceItems)
|
||||
.where(eq(MarketplaceItems.state, 2)),
|
||||
]);
|
||||
total = Number(totalRows[0]?.total ?? 0);
|
||||
activeCount = Number(activeRows[0]?.total ?? 0);
|
||||
soldCount = Number(soldRows[0]?.total ?? 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
rows = await prisma.marketplaceItems.findMany({
|
||||
where,
|
||||
orderBy: { timestamp: "desc" },
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
});
|
||||
rows = await db
|
||||
.select()
|
||||
.from(MarketplaceItems)
|
||||
.where(where)
|
||||
.orderBy(desc(MarketplaceItems.timestamp))
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset);
|
||||
} catch {
|
||||
loadError = true;
|
||||
}
|
||||
@@ -59,11 +70,10 @@ export default async function AdminMarketplacePage({
|
||||
];
|
||||
const users =
|
||||
userIds.length > 0
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
.catch(() => [])
|
||||
: [];
|
||||
const usernameById = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { count, desc, eq, inArray } from "drizzle-orm";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { Ban, db, SupportTickets, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { CfhDetail } from "./cfh-detail";
|
||||
|
||||
export default async function CfhDetailPage({
|
||||
@@ -16,9 +17,11 @@ export default async function CfhDetailPage({
|
||||
const ticketId = Number(id);
|
||||
if (Number.isNaN(ticketId)) notFound();
|
||||
|
||||
const ticket = await prisma.supportTickets.findUnique({
|
||||
where: { id: ticketId },
|
||||
});
|
||||
const [ticket] = await db
|
||||
.select()
|
||||
.from(SupportTickets)
|
||||
.where(eq(SupportTickets.id, ticketId))
|
||||
.limit(1);
|
||||
if (!ticket) notFound();
|
||||
|
||||
const canEdit = canAccess(
|
||||
@@ -30,36 +33,42 @@ export default async function CfhDetailPage({
|
||||
// Fetch sender, reported, mod info
|
||||
const userIds = [ticket.senderId, ticket.reportedId, ticket.modId].filter(
|
||||
Boolean,
|
||||
);
|
||||
) as number[];
|
||||
const users =
|
||||
userIds.length > 0
|
||||
? await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
rank: true,
|
||||
online: true,
|
||||
ipRegister: true,
|
||||
mail: true,
|
||||
},
|
||||
? await db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
rank: User.rank,
|
||||
online: User.online,
|
||||
ipRegister: User.ipRegister,
|
||||
mail: User.mail,
|
||||
})
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
: [];
|
||||
const userMap = Object.fromEntries(users.map((u) => [u.id, u]));
|
||||
|
||||
// Get ban/mute history for reported user
|
||||
const reportedId = ticket.reportedId;
|
||||
const [banCount, recentBans] = reportedId
|
||||
const [banCountRows, recentBans] = reportedId
|
||||
? await Promise.all([
|
||||
prisma.ban.count({ where: { userId: reportedId } }),
|
||||
prisma.ban.findMany({
|
||||
where: { userId: reportedId },
|
||||
orderBy: { id: "desc" },
|
||||
take: 5,
|
||||
}),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(Ban)
|
||||
.where(eq(Ban.userId, reportedId)),
|
||||
db
|
||||
.select()
|
||||
.from(Ban)
|
||||
.where(eq(Ban.userId, reportedId))
|
||||
.orderBy(desc(Ban.id))
|
||||
.limit(5),
|
||||
])
|
||||
: [0, []];
|
||||
: [[{ total: 0 }], []];
|
||||
|
||||
const banCount = Number(banCountRows[0]?.total ?? 0);
|
||||
|
||||
return (
|
||||
<CfhDetail
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+39
-27
@@ -1,3 +1,4 @@
|
||||
import { count, desc, eq, gt, inArray, or } from "drizzle-orm";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
@@ -9,57 +10,68 @@ import {
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { activeBanWhere } from "@/lib/bans";
|
||||
import { unixNow } from "@/lib/bans";
|
||||
import {
|
||||
Ban as BanTable,
|
||||
db,
|
||||
StaffActivities,
|
||||
User,
|
||||
WebsiteArticles,
|
||||
} from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type RankCount = { rank: number; count: number };
|
||||
|
||||
export default async function AdminDashboard() {
|
||||
const t = await getTranslations("pages.admin.dashboard");
|
||||
const now = unixNow();
|
||||
let users = -1;
|
||||
let online = 0;
|
||||
let articles = 0;
|
||||
let bans = 0;
|
||||
try {
|
||||
[users, online, articles, bans] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.user.count({ where: { online: "1" } }),
|
||||
prisma.websiteArticles.count(),
|
||||
prisma.ban.count({
|
||||
where: activeBanWhere(),
|
||||
}),
|
||||
const [usersRow, onlineRow, articlesRow, bansRow] = await Promise.all([
|
||||
db.select({ total: count() }).from(User),
|
||||
db.select({ total: count() }).from(User).where(eq(User.online, "1")),
|
||||
db.select({ total: count() }).from(WebsiteArticles),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(BanTable)
|
||||
.where(or(eq(BanTable.banExpire, 0), gt(BanTable.banExpire, now))),
|
||||
]);
|
||||
users = usersRow[0]?.total ?? 0;
|
||||
online = onlineRow[0]?.total ?? 0;
|
||||
articles = articlesRow[0]?.total ?? 0;
|
||||
bans = bansRow[0]?.total ?? 0;
|
||||
} catch {}
|
||||
const dbOk = users >= 0;
|
||||
|
||||
const rankGroups = await prisma.user
|
||||
.groupBy({
|
||||
by: ["rank"],
|
||||
_count: { _all: true },
|
||||
orderBy: { rank: "desc" },
|
||||
})
|
||||
.catch(() => [] as Array<{ rank: number; _count: { _all: number } }>);
|
||||
const rankGroups = await db
|
||||
.select({ rank: User.rank, total: count() })
|
||||
.from(User)
|
||||
.groupBy(User.rank)
|
||||
.orderBy(desc(User.rank))
|
||||
.catch(() => [] as Array<{ rank: number; total: number }>);
|
||||
const rankCounts: RankCount[] = rankGroups
|
||||
.map((g) => ({ rank: g.rank, count: g._count._all }))
|
||||
.map((g) => ({ rank: g.rank, count: Number(g.total) }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
const maxRankCount = rankCounts.reduce((m, r) => Math.max(m, r.count), 0);
|
||||
|
||||
const activity = await prisma.staffActivities
|
||||
.findMany({ orderBy: { createdAt: "desc" }, take: 8 })
|
||||
.catch(
|
||||
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
|
||||
);
|
||||
const activity = await db
|
||||
.select()
|
||||
.from(StaffActivities)
|
||||
.orderBy(desc(StaffActivities.createdAt))
|
||||
.limit(8)
|
||||
.catch(() => [] as (typeof StaffActivities.$inferSelect)[]);
|
||||
const staffIds = Array.from(
|
||||
new Set(activity.map((a) => Number(a.userId))),
|
||||
).filter((n) => n > 0);
|
||||
let nameById = new Map<number, string>();
|
||||
if (staffIds.length) {
|
||||
const u = await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: staffIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
const u = await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, staffIds))
|
||||
.catch(() => []);
|
||||
nameById = new Map(u.map((x) => [x.id, x.username]));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { count, desc, inArray } from "drizzle-orm";
|
||||
import { Camera } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -7,8 +8,8 @@ import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { CameraWeb, db, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function AdminPhotos({
|
||||
searchParams,
|
||||
@@ -23,16 +24,21 @@ export default async function AdminPhotos({
|
||||
const t = await getTranslations("pages.admin.photos");
|
||||
const parsed = parseListParams(new URLSearchParams(await searchParams));
|
||||
|
||||
const total = await prisma.cameraWeb.count().catch(() => 0);
|
||||
const [totalRow] = await db
|
||||
.select({ total: count() })
|
||||
.from(CameraWeb)
|
||||
.catch(() => [{ total: 0 }]);
|
||||
const total = Number(totalRow?.total ?? 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage || 24);
|
||||
|
||||
let photos: Awaited<ReturnType<typeof prisma.cameraWeb.findMany>> = [];
|
||||
let photos: (typeof CameraWeb.$inferSelect)[] = [];
|
||||
try {
|
||||
photos = await prisma.cameraWeb.findMany({
|
||||
orderBy: { timestamp: "desc" },
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
});
|
||||
photos = await db
|
||||
.select()
|
||||
.from(CameraWeb)
|
||||
.orderBy(desc(CameraWeb.timestamp))
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset);
|
||||
} catch {
|
||||
photos = [];
|
||||
}
|
||||
@@ -40,11 +46,10 @@ export default async function AdminPhotos({
|
||||
const userIds = [...new Set(photos.map((p) => p.userId))];
|
||||
const users =
|
||||
userIds.length > 0
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
.catch(() => [])
|
||||
: [];
|
||||
const usernameById = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { db, WebsitePoll } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { PollForm } from "../poll-form";
|
||||
import { PollQuestionsManager } from "./poll-questions";
|
||||
import { PollResultsView } from "./poll-results";
|
||||
@@ -26,12 +27,12 @@ export default async function PollDetailPage({
|
||||
const t = await getTranslations("pages.admin.polls");
|
||||
const tHubs = await getTranslations("pages.admin.hubs");
|
||||
|
||||
const poll = await prisma.websitePoll.findUnique({
|
||||
where: { id: pollId },
|
||||
include: {
|
||||
const poll = await db.query.WebsitePoll.findFirst({
|
||||
where: eq(WebsitePoll.id, pollId),
|
||||
with: {
|
||||
questions: {
|
||||
orderBy: { sortOrder: "asc" },
|
||||
include: {
|
||||
orderBy: (q, { asc }) => [asc(q.sortOrder)],
|
||||
with: {
|
||||
votes: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
createApiKey,
|
||||
@@ -6,8 +7,8 @@ import {
|
||||
} from "@/actions/admin-radio-api-keys";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioApiKeys } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const metadata = { title: "Radio API Keys" };
|
||||
|
||||
@@ -34,22 +35,21 @@ export default async function AdminRadioApiKeysPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
keys = await prisma.radioApiKeys
|
||||
.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
key: true,
|
||||
allowedIps: true,
|
||||
rateLimit: true,
|
||||
lastUsedAt: true,
|
||||
expiresAt: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: [{ isActive: "desc" }, { id: "desc" }],
|
||||
take: 500,
|
||||
keys = await db
|
||||
.select({
|
||||
id: RadioApiKeys.id,
|
||||
name: RadioApiKeys.name,
|
||||
key: RadioApiKeys.key,
|
||||
allowedIps: RadioApiKeys.allowedIps,
|
||||
rateLimit: RadioApiKeys.rateLimit,
|
||||
lastUsedAt: RadioApiKeys.lastUsedAt,
|
||||
expiresAt: RadioApiKeys.expiresAt,
|
||||
isActive: RadioApiKeys.isActive,
|
||||
createdAt: RadioApiKeys.createdAt,
|
||||
})
|
||||
.from(RadioApiKeys)
|
||||
.orderBy(desc(RadioApiKeys.isActive), desc(RadioApiKeys.id))
|
||||
.limit(500)
|
||||
.catch(() => {
|
||||
dbError = true;
|
||||
return [];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import Image from "next/image";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
@@ -7,8 +8,8 @@ import {
|
||||
} from "@/actions/admin-radio-autodj";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioAutoDjPlaylist } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const metadata = { title: "Radio AutoDJ" };
|
||||
|
||||
@@ -39,22 +40,24 @@ export default async function AdminRadioAutoDjPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
tracks = await prisma.radioAutoDjPlaylist
|
||||
.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
artist: true,
|
||||
album: true,
|
||||
artworkUrl: true,
|
||||
duration: true,
|
||||
playCount: true,
|
||||
lastPlayedAt: true,
|
||||
isActive: true,
|
||||
sortOrder: true,
|
||||
},
|
||||
orderBy: [{ sortOrder: "asc" }, { title: "asc" }],
|
||||
tracks = await db
|
||||
.select({
|
||||
id: RadioAutoDjPlaylist.id,
|
||||
title: RadioAutoDjPlaylist.title,
|
||||
artist: RadioAutoDjPlaylist.artist,
|
||||
album: RadioAutoDjPlaylist.album,
|
||||
artworkUrl: RadioAutoDjPlaylist.artworkUrl,
|
||||
duration: RadioAutoDjPlaylist.duration,
|
||||
playCount: RadioAutoDjPlaylist.playCount,
|
||||
lastPlayedAt: RadioAutoDjPlaylist.lastPlayedAt,
|
||||
isActive: RadioAutoDjPlaylist.isActive,
|
||||
sortOrder: RadioAutoDjPlaylist.sortOrder,
|
||||
})
|
||||
.from(RadioAutoDjPlaylist)
|
||||
.orderBy(
|
||||
asc(RadioAutoDjPlaylist.sortOrder),
|
||||
asc(RadioAutoDjPlaylist.title),
|
||||
)
|
||||
.catch(() => {
|
||||
dbError = true;
|
||||
return [];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import NextImage from "next/image";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
@@ -7,8 +8,8 @@ import {
|
||||
} from "@/actions/admin-radio-extra";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioBanners } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type Banner = {
|
||||
id: bigint;
|
||||
@@ -28,19 +29,19 @@ export default async function AdminRadioBannersPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
banners = await prisma.radioBanners.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
imagePath: true,
|
||||
title: true,
|
||||
description: true,
|
||||
sortOrder: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: [{ sortOrder: "asc" }, { id: "asc" }],
|
||||
});
|
||||
banners = await db
|
||||
.select({
|
||||
id: RadioBanners.id,
|
||||
userId: RadioBanners.userId,
|
||||
imagePath: RadioBanners.imagePath,
|
||||
title: RadioBanners.title,
|
||||
description: RadioBanners.description,
|
||||
sortOrder: RadioBanners.sortOrder,
|
||||
isActive: RadioBanners.isActive,
|
||||
createdAt: RadioBanners.createdAt,
|
||||
})
|
||||
.from(RadioBanners)
|
||||
.orderBy(asc(RadioBanners.sortOrder), asc(RadioBanners.id));
|
||||
} catch {
|
||||
dbError = true;
|
||||
banners = [];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { desc, inArray } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { db, RadioHistory, User } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type History = {
|
||||
id: bigint;
|
||||
@@ -31,28 +32,28 @@ export default async function AdminRadioHistoryPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
history = await prisma.radioHistory.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
showName: true,
|
||||
startedAt: true,
|
||||
endedAt: true,
|
||||
listenersCount: true,
|
||||
notes: true,
|
||||
},
|
||||
orderBy: { startedAt: "desc" },
|
||||
take: 100,
|
||||
});
|
||||
history = await db
|
||||
.select({
|
||||
id: RadioHistory.id,
|
||||
userId: RadioHistory.userId,
|
||||
showName: RadioHistory.showName,
|
||||
startedAt: RadioHistory.startedAt,
|
||||
endedAt: RadioHistory.endedAt,
|
||||
listenersCount: RadioHistory.listenersCount,
|
||||
notes: RadioHistory.notes,
|
||||
})
|
||||
.from(RadioHistory)
|
||||
.orderBy(desc(RadioHistory.startedAt))
|
||||
.limit(100);
|
||||
|
||||
const ids = Array.from(
|
||||
new Set(history.map((h) => Number(h.userId))),
|
||||
).filter((n) => Number.isFinite(n) && n > 0);
|
||||
if (ids.length > 0) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: { id: true, username: true },
|
||||
});
|
||||
const users = await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, ids));
|
||||
names = new Map(users.map((u) => [String(u.id), u.username]));
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { deleteShout } from "@/actions/admin-radio-moderation";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioShouts } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type Shout = {
|
||||
id: bigint;
|
||||
@@ -18,17 +19,17 @@ export default async function AdminRadioModerationPage() {
|
||||
|
||||
let shouts: Shout[] = [];
|
||||
try {
|
||||
shouts = await prisma.radioShouts.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
message: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 50,
|
||||
});
|
||||
shouts = await db
|
||||
.select({
|
||||
id: RadioShouts.id,
|
||||
userId: RadioShouts.userId,
|
||||
message: RadioShouts.message,
|
||||
createdAt: RadioShouts.createdAt,
|
||||
updatedAt: RadioShouts.updatedAt,
|
||||
})
|
||||
.from(RadioShouts)
|
||||
.orderBy(desc(RadioShouts.id))
|
||||
.limit(50);
|
||||
} catch {
|
||||
shouts = [];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { deleteShout } from "@/actions/admin-radio-moderation";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioApplications, RadioSchedules, RadioShouts } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type Shout = {
|
||||
id: bigint;
|
||||
@@ -52,48 +53,53 @@ export default async function AdminRadioPage() {
|
||||
let schedules: Schedule[] = [];
|
||||
|
||||
try {
|
||||
shouts = await prisma.radioShouts.findMany({
|
||||
select: { id: true, userId: true, message: true, createdAt: true },
|
||||
orderBy: { id: "desc" },
|
||||
take: 25,
|
||||
});
|
||||
shouts = await db
|
||||
.select({
|
||||
id: RadioShouts.id,
|
||||
userId: RadioShouts.userId,
|
||||
message: RadioShouts.message,
|
||||
createdAt: RadioShouts.createdAt,
|
||||
})
|
||||
.from(RadioShouts)
|
||||
.orderBy(desc(RadioShouts.id))
|
||||
.limit(25);
|
||||
} catch {
|
||||
shouts = [];
|
||||
}
|
||||
|
||||
try {
|
||||
applications = await prisma.radioApplications.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
realName: true,
|
||||
age: true,
|
||||
status: true,
|
||||
musicStyle: true,
|
||||
discordUsername: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 25,
|
||||
});
|
||||
applications = await db
|
||||
.select({
|
||||
id: RadioApplications.id,
|
||||
userId: RadioApplications.userId,
|
||||
realName: RadioApplications.realName,
|
||||
age: RadioApplications.age,
|
||||
status: RadioApplications.status,
|
||||
musicStyle: RadioApplications.musicStyle,
|
||||
discordUsername: RadioApplications.discordUsername,
|
||||
createdAt: RadioApplications.createdAt,
|
||||
})
|
||||
.from(RadioApplications)
|
||||
.orderBy(desc(RadioApplications.id))
|
||||
.limit(25);
|
||||
} catch {
|
||||
applications = [];
|
||||
}
|
||||
|
||||
try {
|
||||
schedules = await prisma.radioSchedules.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
day: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
showName: true,
|
||||
isActive: true,
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 25,
|
||||
});
|
||||
schedules = await db
|
||||
.select({
|
||||
id: RadioSchedules.id,
|
||||
userId: RadioSchedules.userId,
|
||||
day: RadioSchedules.day,
|
||||
startTime: RadioSchedules.startTime,
|
||||
endTime: RadioSchedules.endTime,
|
||||
showName: RadioSchedules.showName,
|
||||
isActive: RadioSchedules.isActive,
|
||||
})
|
||||
.from(RadioSchedules)
|
||||
.orderBy(desc(RadioSchedules.id))
|
||||
.limit(25);
|
||||
} catch {
|
||||
schedules = [];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
createRadioRank,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
} from "@/actions/admin-radio-extra";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, RadioRanks } from "@/lib/db";
|
||||
|
||||
type Rank = {
|
||||
id: bigint;
|
||||
@@ -23,16 +24,16 @@ export default async function AdminRadioRanksPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
ranks = await prisma.radioRanks.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
badgeCode: true,
|
||||
isActive: true,
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
ranks = await db
|
||||
.select({
|
||||
id: RadioRanks.id,
|
||||
name: RadioRanks.name,
|
||||
description: RadioRanks.description,
|
||||
badgeCode: RadioRanks.badgeCode,
|
||||
isActive: RadioRanks.isActive,
|
||||
})
|
||||
.from(RadioRanks)
|
||||
.orderBy(asc(RadioRanks.id));
|
||||
} catch {
|
||||
dbError = true;
|
||||
ranks = [];
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@/actions/admin-radio-extra";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, WebsiteSetting } from "@/lib/db";
|
||||
|
||||
type Field = { key: string; comment: string };
|
||||
type Group = { title: string; fields: Field[] };
|
||||
@@ -364,9 +364,13 @@ export default async function AdminRadioSettingsPage() {
|
||||
let extra: Field[] = [];
|
||||
|
||||
try {
|
||||
const rows = await prisma.websiteSetting.findMany({
|
||||
select: { key: true, value: true, comment: true },
|
||||
});
|
||||
const rows = await db
|
||||
.select({
|
||||
key: WebsiteSetting.key,
|
||||
value: WebsiteSetting.value,
|
||||
comment: WebsiteSetting.comment,
|
||||
})
|
||||
.from(WebsiteSetting);
|
||||
values = new Map(rows.map((r) => [r.key, r.value]));
|
||||
extra = rows
|
||||
.filter(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
@@ -8,8 +9,8 @@ import {
|
||||
} from "@/actions/admin-rare-values";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const CURRENCY_TYPES = ["diamonds", "credits", "duckets", "points"];
|
||||
|
||||
@@ -21,23 +22,25 @@ export default async function AdminRareValues() {
|
||||
|
||||
const t = await getTranslations("pages.admin.rareValues");
|
||||
|
||||
let categories: Awaited<
|
||||
ReturnType<typeof prisma.websiteRareValueCategories.findMany>
|
||||
> = [];
|
||||
let categories: (typeof WebsiteRareValueCategories.$inferSelect)[] = [];
|
||||
try {
|
||||
categories = await prisma.websiteRareValueCategories.findMany({
|
||||
orderBy: [{ priority: "asc" }, { name: "asc" }],
|
||||
});
|
||||
categories = await db
|
||||
.select()
|
||||
.from(WebsiteRareValueCategories)
|
||||
.orderBy(
|
||||
asc(WebsiteRareValueCategories.priority),
|
||||
asc(WebsiteRareValueCategories.name),
|
||||
);
|
||||
} catch {
|
||||
categories = [];
|
||||
}
|
||||
|
||||
let values: Awaited<ReturnType<typeof prisma.websiteRareValues.findMany>> =
|
||||
[];
|
||||
let values: (typeof WebsiteRareValues.$inferSelect)[] = [];
|
||||
try {
|
||||
values = await prisma.websiteRareValues.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
values = await db
|
||||
.select()
|
||||
.from(WebsiteRareValues)
|
||||
.orderBy(asc(WebsiteRareValues.name));
|
||||
} catch {
|
||||
values = [];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { desc, eq, inArray } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
||||
import { db, Items, ItemsBase, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { loadRoom } from "../../_lib/load-room";
|
||||
import { FurniClient } from "./furni-client";
|
||||
|
||||
@@ -18,10 +19,11 @@ export default async function RoomFurniPage({
|
||||
const room = await loadRoom(id);
|
||||
|
||||
// Load items in this room. items.item_id → items_base.id
|
||||
const items = await prisma.items.findMany({
|
||||
where: { roomId: room.id },
|
||||
orderBy: { id: "desc" },
|
||||
});
|
||||
const items = await db
|
||||
.select()
|
||||
.from(Items)
|
||||
.where(eq(Items.roomId, room.id))
|
||||
.orderBy(desc(Items.id));
|
||||
|
||||
// Resolve item bases
|
||||
const baseIds = Array.from(
|
||||
@@ -30,10 +32,15 @@ export default async function RoomFurniPage({
|
||||
),
|
||||
);
|
||||
const bases = baseIds.length
|
||||
? await prisma.itemsBase.findMany({
|
||||
where: { id: { in: baseIds } },
|
||||
select: { id: true, itemName: true, publicName: true, type: true },
|
||||
? await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
itemName: ItemsBase.itemName,
|
||||
publicName: ItemsBase.publicName,
|
||||
type: ItemsBase.type,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(inArray(ItemsBase.id, baseIds))
|
||||
: [];
|
||||
const baseMap = new Map(bases.map((b) => [b.id, b]));
|
||||
|
||||
@@ -42,10 +49,10 @@ export default async function RoomFurniPage({
|
||||
new Set(items.map((i) => i.userId).filter((u) => u > 0)),
|
||||
);
|
||||
const users = userIds.length
|
||||
? await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
: [];
|
||||
const userMap = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, Rooms, User } from "@/lib/db";
|
||||
|
||||
export async function loadRoom(id: string) {
|
||||
const roomId = parseInt(id, 10);
|
||||
if (Number.isNaN(roomId)) notFound();
|
||||
|
||||
const room = await prisma.rooms.findUnique({
|
||||
where: { id: roomId },
|
||||
});
|
||||
const [room] = await db
|
||||
.select()
|
||||
.from(Rooms)
|
||||
.where(eq(Rooms.id, roomId))
|
||||
.limit(1);
|
||||
|
||||
if (!room) notFound();
|
||||
const owner =
|
||||
room.ownerId > 0
|
||||
? await prisma.user.findUnique({
|
||||
where: { id: room.ownerId },
|
||||
select: { id: true, username: true, look: true },
|
||||
? ((
|
||||
await db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
})
|
||||
.from(User)
|
||||
.where(eq(User.id, room.ownerId))
|
||||
.limit(1)
|
||||
)[0] ?? null)
|
||||
: null;
|
||||
return { ...room, owner };
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { Settings } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { db, WebsiteSetting } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { AdvancedSettingsPanel } from "./advanced-settings-panel";
|
||||
import { FIELD_DEFAULTS, MANAGED_SETTING_KEYS } from "./cms-settings-config";
|
||||
import { CmsSettingsForm } from "./cms-settings-form";
|
||||
@@ -22,11 +23,12 @@ export default async function AdminSettings() {
|
||||
);
|
||||
const t = await getTranslations("pages.admin.settings");
|
||||
|
||||
let settings: Awaited<ReturnType<typeof prisma.websiteSetting.findMany>> = [];
|
||||
let settings: (typeof WebsiteSetting.$inferSelect)[] = [];
|
||||
try {
|
||||
settings = await prisma.websiteSetting.findMany({
|
||||
orderBy: { key: "asc" },
|
||||
});
|
||||
settings = await db
|
||||
.select()
|
||||
.from(WebsiteSetting)
|
||||
.orderBy(asc(WebsiteSetting.key));
|
||||
} catch {
|
||||
settings = [];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Package } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { deleteShopArticle, updateShopArticle } from "@/actions/admin-shop";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteShopArticles } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function EditShopArticle({
|
||||
params,
|
||||
@@ -20,12 +21,14 @@ export default async function EditShopArticle({
|
||||
const t = await getTranslations("pages.admin.shop");
|
||||
|
||||
const { id } = await params;
|
||||
let pkg: Awaited<ReturnType<typeof prisma.websiteShopArticles.findUnique>> =
|
||||
null;
|
||||
let pkg: typeof WebsiteShopArticles.$inferSelect | null = null;
|
||||
try {
|
||||
pkg = await prisma.websiteShopArticles.findUnique({
|
||||
where: { id: BigInt(id) },
|
||||
});
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(WebsiteShopArticles)
|
||||
.where(eq(WebsiteShopArticles.id, BigInt(id)))
|
||||
.limit(1);
|
||||
pkg = row ?? null;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
+19
-17
@@ -1,3 +1,4 @@
|
||||
import { asc, count, desc } from "drizzle-orm";
|
||||
import { ShoppingCart } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -8,9 +9,9 @@ import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, WebsitePaypalTransactions, WebsiteShopArticles } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const PAID_STATUS = new Set(["completed", "paid", "success", "approved"]);
|
||||
const RECENT_ORDERS = 25;
|
||||
@@ -65,22 +66,24 @@ export default async function AdminShop({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
|
||||
let packageTotal = 0;
|
||||
let packages: Awaited<
|
||||
ReturnType<typeof prisma.websiteShopArticles.findMany>
|
||||
> = [];
|
||||
let packages: (typeof WebsiteShopArticles.$inferSelect)[] = [];
|
||||
let loadError = false;
|
||||
try {
|
||||
packageTotal = await prisma.websiteShopArticles.count();
|
||||
const [totalRow] = await db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteShopArticles);
|
||||
packageTotal = Number(totalRow?.total ?? 0);
|
||||
const pagination = calcPagination(
|
||||
packageTotal,
|
||||
parsed.page,
|
||||
parsed.perPage,
|
||||
);
|
||||
packages = await prisma.websiteShopArticles.findMany({
|
||||
orderBy: [{ position: "asc" }, { name: "asc" }],
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
});
|
||||
packages = await db
|
||||
.select()
|
||||
.from(WebsiteShopArticles)
|
||||
.orderBy(asc(WebsiteShopArticles.position), asc(WebsiteShopArticles.name))
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset);
|
||||
} catch {
|
||||
packages = [];
|
||||
packageTotal = 0;
|
||||
@@ -93,14 +96,13 @@ export default async function AdminShop({
|
||||
parsed.perPage,
|
||||
);
|
||||
|
||||
let orders: Awaited<
|
||||
ReturnType<typeof prisma.websitePaypalTransactions.findMany>
|
||||
> = [];
|
||||
let orders: (typeof WebsitePaypalTransactions.$inferSelect)[] = [];
|
||||
try {
|
||||
orders = await prisma.websitePaypalTransactions.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: RECENT_ORDERS,
|
||||
});
|
||||
orders = await db
|
||||
.select()
|
||||
.from(WebsitePaypalTransactions)
|
||||
.orderBy(desc(WebsitePaypalTransactions.createdAt))
|
||||
.limit(RECENT_ORDERS);
|
||||
} catch {
|
||||
orders = [];
|
||||
loadError = true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { asc, count, gt } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { CatalogItems, db, Soundtracks } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import {
|
||||
DEFAULT_SOUNDTRACK_BASE_URL,
|
||||
@@ -18,28 +19,31 @@ export default async function SoundsPage() {
|
||||
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
|
||||
|
||||
const [rows, usage, baseUrlSetting] = await Promise.all([
|
||||
prisma.soundtracks.findMany({
|
||||
orderBy: { id: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
author: true,
|
||||
length: true,
|
||||
track: true,
|
||||
},
|
||||
}),
|
||||
prisma.catalogItems.groupBy({
|
||||
by: ["songId"],
|
||||
where: { songId: { gt: 0 } },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
db
|
||||
.select({
|
||||
id: Soundtracks.id,
|
||||
code: Soundtracks.code,
|
||||
name: Soundtracks.name,
|
||||
author: Soundtracks.author,
|
||||
length: Soundtracks.length,
|
||||
track: Soundtracks.track,
|
||||
})
|
||||
.from(Soundtracks)
|
||||
.orderBy(asc(Soundtracks.id)),
|
||||
db
|
||||
.select({
|
||||
songId: CatalogItems.songId,
|
||||
total: count(),
|
||||
})
|
||||
.from(CatalogItems)
|
||||
.where(gt(CatalogItems.songId, 0))
|
||||
.groupBy(CatalogItems.songId),
|
||||
siteSettings.get("soundtrack_base_url", DEFAULT_SOUNDTRACK_BASE_URL),
|
||||
]);
|
||||
|
||||
const usageMap: Record<number, number> = {};
|
||||
for (const row of usage) {
|
||||
if (row.songId != null) usageMap[row.songId] = row._count._all;
|
||||
if (row.songId != null) usageMap[row.songId] = Number(row.total);
|
||||
}
|
||||
|
||||
const soundtracks = rows.map((r) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type SubRow = {
|
||||
id: number;
|
||||
@@ -14,6 +15,19 @@ type SubRow = {
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
type CountRow = { c: number | bigint };
|
||||
type SubSqlRow = {
|
||||
id: number;
|
||||
user_id: number | null;
|
||||
subscription_type: string | null;
|
||||
expires_at: number;
|
||||
};
|
||||
|
||||
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
}
|
||||
|
||||
export default async function AdminSubscriptions({
|
||||
searchParams,
|
||||
}: {
|
||||
@@ -36,39 +50,32 @@ export default async function AdminSubscriptions({
|
||||
let loadError = false;
|
||||
try {
|
||||
const [countRows, typeRows, soonRows] = await Promise.all([
|
||||
prisma.$queryRaw<{ c: bigint }[]>`
|
||||
rawRows<CountRow>(sql`
|
||||
SELECT COUNT(*) AS c
|
||||
FROM users_subscriptions
|
||||
WHERE active = 1
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
|
||||
`,
|
||||
prisma.$queryRaw<{ c: bigint }[]>`
|
||||
`),
|
||||
rawRows<CountRow>(sql`
|
||||
SELECT COUNT(DISTINCT subscription_type) AS c
|
||||
FROM users_subscriptions
|
||||
WHERE active = 1
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
|
||||
`,
|
||||
prisma.$queryRaw<{ c: bigint }[]>`
|
||||
`),
|
||||
rawRows<CountRow>(sql`
|
||||
SELECT COUNT(*) AS c
|
||||
FROM users_subscriptions
|
||||
WHERE active = 1
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) <= ${now + 7 * 24 * 60 * 60}
|
||||
`,
|
||||
`),
|
||||
]);
|
||||
total = Number(countRows[0]?.c ?? 0);
|
||||
types = Number(typeRows[0]?.c ?? 0);
|
||||
expiringSoon = Number(soonRows[0]?.c ?? 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
|
||||
const rows = await prisma.$queryRaw<
|
||||
{
|
||||
id: number;
|
||||
user_id: number | null;
|
||||
subscription_type: string | null;
|
||||
expires_at: number;
|
||||
}[]
|
||||
>`
|
||||
const rows = await rawRows<SubSqlRow>(sql`
|
||||
SELECT id,
|
||||
user_id,
|
||||
subscription_type,
|
||||
@@ -78,7 +85,7 @@ export default async function AdminSubscriptions({
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
|
||||
ORDER BY expires_at ASC
|
||||
LIMIT ${pagination.perPage} OFFSET ${pagination.offset}
|
||||
`;
|
||||
`);
|
||||
|
||||
active = rows.map((r) => ({
|
||||
id: Number(r.id),
|
||||
|
||||
+20
-11
@@ -1,11 +1,12 @@
|
||||
import { asc, count } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { createTag, deleteTag, updateTag } from "@/actions/admin-tags";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, Taggables, Tags } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type Tag = {
|
||||
id: bigint;
|
||||
@@ -26,18 +27,26 @@ export default async function AdminTagsPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
tags = await prisma.tags.findMany({
|
||||
select: { id: true, name: true, backgroundColor: true, createdAt: true },
|
||||
orderBy: { name: "asc" },
|
||||
take: 500,
|
||||
});
|
||||
tags = await db
|
||||
.select({
|
||||
id: Tags.id,
|
||||
name: Tags.name,
|
||||
backgroundColor: Tags.backgroundColor,
|
||||
createdAt: Tags.createdAt,
|
||||
})
|
||||
.from(Tags)
|
||||
.orderBy(asc(Tags.name))
|
||||
.limit(500);
|
||||
|
||||
// How many things each tag is attached to (taggables join table).
|
||||
const counts = await prisma.taggables.groupBy({
|
||||
by: ["tagId"],
|
||||
_count: { tagId: true },
|
||||
});
|
||||
usage = new Map(counts.map((c) => [String(c.tagId), c._count.tagId]));
|
||||
const counts = await db
|
||||
.select({
|
||||
tagId: Taggables.tagId,
|
||||
total: count(),
|
||||
})
|
||||
.from(Taggables)
|
||||
.groupBy(Taggables.tagId);
|
||||
usage = new Map(counts.map((c) => [String(c.tagId), Number(c.total)]));
|
||||
} catch {
|
||||
dbError = true;
|
||||
tags = [];
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { createTeam, deleteTeam } from "@/actions/admin-teams";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteTeams } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type TeamRow = {
|
||||
id: bigint;
|
||||
@@ -24,17 +25,17 @@ export default async function AdminTeams() {
|
||||
const t = await getTranslations("pages.admin.teams");
|
||||
let teams: TeamRow[] = [];
|
||||
try {
|
||||
teams = await prisma.websiteTeams.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
rankName: true,
|
||||
badge: true,
|
||||
jobDescription: true,
|
||||
staffColor: true,
|
||||
hiddenRank: true,
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
teams = await db
|
||||
.select({
|
||||
id: WebsiteTeams.id,
|
||||
rankName: WebsiteTeams.rankName,
|
||||
badge: WebsiteTeams.badge,
|
||||
jobDescription: WebsiteTeams.jobDescription,
|
||||
staffColor: WebsiteTeams.staffColor,
|
||||
hiddenRank: WebsiteTeams.hiddenRank,
|
||||
})
|
||||
.from(WebsiteTeams)
|
||||
.orderBy(asc(WebsiteTeams.id));
|
||||
} catch {
|
||||
teams = [];
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user