refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (2)
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
7c39aef5d9
commit
2cd0863cb8
40 files changed
+1069
-726
No files matched your search
@@ -1,7 +1,8 @@
|
|||||||
|
import { asc, count } from "drizzle-orm";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
export const revalidate = 300;
|
export const revalidate = 300;
|
||||||
@@ -19,10 +20,18 @@ export default async function RareValuesPage() {
|
|||||||
const t = await getTranslations("pages.rares");
|
const t = await getTranslations("pages.rares");
|
||||||
let categories: CategoryRow[] = [];
|
let categories: CategoryRow[] = [];
|
||||||
try {
|
try {
|
||||||
categories = await prisma.websiteRareValueCategories.findMany({
|
categories = await db
|
||||||
orderBy: [{ priority: "asc" }, { name: "asc" }],
|
.select({
|
||||||
select: { id: true, name: true, badge: true, priority: true },
|
id: WebsiteRareValueCategories.id,
|
||||||
});
|
name: WebsiteRareValueCategories.name,
|
||||||
|
badge: WebsiteRareValueCategories.badge,
|
||||||
|
priority: WebsiteRareValueCategories.priority,
|
||||||
|
})
|
||||||
|
.from(WebsiteRareValueCategories)
|
||||||
|
.orderBy(
|
||||||
|
asc(WebsiteRareValueCategories.priority),
|
||||||
|
asc(WebsiteRareValueCategories.name),
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
categories = [];
|
categories = [];
|
||||||
}
|
}
|
||||||
@@ -30,12 +39,15 @@ export default async function RareValuesPage() {
|
|||||||
// Per-category rare counts (grouped so we avoid N queries).
|
// Per-category rare counts (grouped so we avoid N queries).
|
||||||
const counts = new Map<string, number>();
|
const counts = new Map<string, number>();
|
||||||
try {
|
try {
|
||||||
const grouped = await prisma.websiteRareValues.groupBy({
|
const grouped = await db
|
||||||
by: ["categoryId"],
|
.select({
|
||||||
_count: { _all: true },
|
categoryId: WebsiteRareValues.categoryId,
|
||||||
});
|
value: count(),
|
||||||
|
})
|
||||||
|
.from(WebsiteRareValues)
|
||||||
|
.groupBy(WebsiteRareValues.categoryId);
|
||||||
for (const g of grouped) {
|
for (const g of grouped) {
|
||||||
counts.set(String(g.categoryId), g._count._all);
|
counts.set(String(g.categoryId), Number(g.value));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// leave counts empty -> shown as 0
|
// leave counts empty -> shown as 0
|
||||||
@@ -55,7 +67,7 @@ export default async function RareValuesPage() {
|
|||||||
<div className="card-grid sm-2 lg-3">
|
<div className="card-grid sm-2 lg-3">
|
||||||
{categories.map((c) => {
|
{categories.map((c) => {
|
||||||
const id = String(c.id);
|
const id = String(c.id);
|
||||||
const count = counts.get(id) ?? 0;
|
const rareCount = counts.get(id) ?? 0;
|
||||||
const badgeSrc =
|
const badgeSrc =
|
||||||
badgesBase && c.badge ? `${badgesBase}/${c.badge}.gif` : "";
|
badgesBase && c.badge ? `${badgesBase}/${c.badge}.gif` : "";
|
||||||
return (
|
return (
|
||||||
@@ -83,7 +95,7 @@ export default async function RareValuesPage() {
|
|||||||
<div className="content-card-head-text">
|
<div className="content-card-head-text">
|
||||||
<p className="content-card-title">{c.name}</p>
|
<p className="content-card-title">{c.name}</p>
|
||||||
<p className="content-card-subtitle">
|
<p className="content-card-subtitle">
|
||||||
{t("rareCount", { count })}
|
{t("rareCount", { count: rareCount })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { ContentCard, StatBlock } from "@/components/public/ui";
|
import { ContentCard, StatBlock } from "@/components/public/ui";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, User } from "@/lib/db";
|
||||||
import RedeemForm from "./RedeemForm";
|
import RedeemForm from "./RedeemForm";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -17,10 +18,12 @@ export default async function RedeemPage() {
|
|||||||
// Show the signed-in user's current credit balance for context (fail soft).
|
// Show the signed-in user's current credit balance for context (fail soft).
|
||||||
let user: { username: string; credits: number } | null = null;
|
let user: { username: string; credits: number } | null = null;
|
||||||
try {
|
try {
|
||||||
user = await prisma.user.findUnique({
|
const [row] = await db
|
||||||
where: { id },
|
.select({ username: User.username, credits: User.credits })
|
||||||
select: { username: true, credits: true },
|
.from(User)
|
||||||
});
|
.where(eq(User.id, id))
|
||||||
|
.limit(1);
|
||||||
|
user = row ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
user = null;
|
user = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { count, desc, eq } from "drizzle-orm";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -8,9 +9,9 @@ import { LanguageSwitcher } from "@/components/language-switcher";
|
|||||||
import { Reveal } from "@/components/motion-reveal";
|
import { Reveal } from "@/components/motion-reveal";
|
||||||
import { ThemeSwitcher } from "@/components/theme-switcher";
|
import { ThemeSwitcher } from "@/components/theme-switcher";
|
||||||
import { cached } from "@/lib/cache";
|
import { cached } from "@/lib/cache";
|
||||||
|
import { db, User } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { captchaConfig } from "@/lib/services/captcha";
|
import { captchaConfig } from "@/lib/services/captcha";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
@@ -32,21 +33,23 @@ export default async function RegisterPage() {
|
|||||||
|
|
||||||
const [online, recentUsers, latestUsers] = await Promise.all([
|
const [online, recentUsers, latestUsers] = await Promise.all([
|
||||||
cached("online_count", 10_000, () =>
|
cached("online_count", 10_000, () =>
|
||||||
prisma.user.count({ where: { online: "1" } }),
|
db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(User)
|
||||||
|
.where(eq(User.online, "1"))
|
||||||
|
.then((rows) => rows[0]?.total ?? 0),
|
||||||
).catch(() => 0),
|
).catch(() => 0),
|
||||||
prisma.user
|
db
|
||||||
.findMany({
|
.select({ username: User.username, look: User.look })
|
||||||
where: { online: "1" },
|
.from(User)
|
||||||
select: { username: true, look: true },
|
.where(eq(User.online, "1"))
|
||||||
take: 8,
|
.limit(8)
|
||||||
})
|
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
prisma.user
|
db
|
||||||
.findMany({
|
.select({ username: User.username, look: User.look })
|
||||||
orderBy: { accountCreated: "desc" },
|
.from(User)
|
||||||
select: { username: true, look: true },
|
.orderBy(desc(User.accountCreated))
|
||||||
take: 8,
|
.limit(8)
|
||||||
})
|
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
|
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, Rooms, User } from "@/lib/db";
|
||||||
|
|
||||||
export const revalidate = 300;
|
export const revalidate = 300;
|
||||||
|
|
||||||
@@ -31,24 +32,25 @@ function parseRoomId(raw: string): number | null {
|
|||||||
async function getRoom(raw: string) {
|
async function getRoom(raw: string) {
|
||||||
const id = parseRoomId(raw);
|
const id = parseRoomId(raw);
|
||||||
if (id === null) return null;
|
if (id === null) return null;
|
||||||
return prisma.rooms
|
const [room] = await db
|
||||||
.findUnique({
|
.select({
|
||||||
where: { id },
|
id: Rooms.id,
|
||||||
select: {
|
ownerId: Rooms.ownerId,
|
||||||
id: true,
|
ownerName: Rooms.ownerName,
|
||||||
ownerId: true,
|
name: Rooms.name,
|
||||||
ownerName: true,
|
description: Rooms.description,
|
||||||
name: true,
|
state: Rooms.state,
|
||||||
description: true,
|
users: Rooms.users,
|
||||||
state: true,
|
usersMax: Rooms.usersMax,
|
||||||
users: true,
|
category: Rooms.category,
|
||||||
usersMax: true,
|
score: Rooms.score,
|
||||||
category: true,
|
tags: Rooms.tags,
|
||||||
score: true,
|
|
||||||
tags: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.catch(() => null);
|
.from(Rooms)
|
||||||
|
.where(eq(Rooms.id, id))
|
||||||
|
.limit(1)
|
||||||
|
.catch(() => []);
|
||||||
|
return room ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
@@ -83,12 +85,14 @@ export default async function RoomPage({
|
|||||||
// owner_name column if the account row is missing or the query degrades.
|
// owner_name column if the account row is missing or the query degrades.
|
||||||
const owner =
|
const owner =
|
||||||
room.ownerId > 0
|
room.ownerId > 0
|
||||||
? await prisma.user
|
? (
|
||||||
.findUnique({
|
await db
|
||||||
where: { id: room.ownerId },
|
.select({ username: User.username })
|
||||||
select: { username: true },
|
.from(User)
|
||||||
})
|
.where(eq(User.id, room.ownerId))
|
||||||
.catch(() => null)
|
.limit(1)
|
||||||
|
.catch(() => [])
|
||||||
|
)[0]
|
||||||
: null;
|
: null;
|
||||||
const ownerName = owner?.username || room.ownerName || "Unknown";
|
const ownerName = owner?.username || room.ownerName || "Unknown";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { and, eq, like } from "drizzle-orm";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { db, Rooms, User } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
export const metadata: Metadata = { title: "Search" };
|
export const metadata: Metadata = { title: "Search" };
|
||||||
@@ -43,38 +44,31 @@ export default async function SearchPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await prisma.user
|
const users = await db
|
||||||
.findMany({
|
.select({
|
||||||
where: {
|
id: User.id,
|
||||||
username: { contains: query },
|
username: User.username,
|
||||||
},
|
look: User.look,
|
||||||
select: {
|
motto: User.motto,
|
||||||
id: true,
|
online: User.online,
|
||||||
username: true,
|
rank: User.rank,
|
||||||
look: true,
|
|
||||||
motto: true,
|
|
||||||
online: true,
|
|
||||||
rank: true,
|
|
||||||
},
|
|
||||||
take: 30,
|
|
||||||
})
|
})
|
||||||
|
.from(User)
|
||||||
|
.where(like(User.username, `%${query}%`))
|
||||||
|
.limit(30)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
const rooms = await prisma.rooms
|
const rooms = await db
|
||||||
.findMany({
|
.select({
|
||||||
where: {
|
id: Rooms.id,
|
||||||
name: { contains: query },
|
name: Rooms.name,
|
||||||
state: "open",
|
ownerName: Rooms.ownerName,
|
||||||
},
|
users: Rooms.users,
|
||||||
select: {
|
usersMax: Rooms.usersMax,
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
ownerName: true,
|
|
||||||
users: true,
|
|
||||||
usersMax: true,
|
|
||||||
},
|
|
||||||
take: 12,
|
|
||||||
})
|
})
|
||||||
|
.from(Rooms)
|
||||||
|
.where(and(like(Rooms.name, `%${query}%`), eq(Rooms.state, "open")))
|
||||||
|
.limit(12)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
@@ -11,8 +12,8 @@ import { env } from "@/env";
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { LaravelEncrypter } from "@/lib/auth/laravel-encrypter";
|
import { LaravelEncrypter } from "@/lib/auth/laravel-encrypter";
|
||||||
import { totpKeyUri } from "@/lib/auth/totp";
|
import { totpKeyUri } from "@/lib/auth/totp";
|
||||||
|
import { db, User } from "@/lib/db";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -37,14 +38,16 @@ export default async function TwoFactorPage({
|
|||||||
twoFactorRecoveryCodes: string | null;
|
twoFactorRecoveryCodes: string | null;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
try {
|
try {
|
||||||
user = await prisma.user.findUnique({
|
const [row] = await db
|
||||||
where: { id },
|
.select({
|
||||||
select: {
|
twoFactorSecret: User.twoFactorSecret,
|
||||||
twoFactorSecret: true,
|
twoFactorConfirmedAt: User.twoFactorConfirmedAt,
|
||||||
twoFactorConfirmedAt: true,
|
twoFactorRecoveryCodes: User.twoFactorRecoveryCodes,
|
||||||
twoFactorRecoveryCodes: true,
|
})
|
||||||
},
|
.from(User)
|
||||||
});
|
.where(eq(User.id, id))
|
||||||
|
.limit(1);
|
||||||
|
user = row ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
user = null;
|
user = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
@@ -5,8 +6,8 @@ import { getTranslations } from "next-intl/server";
|
|||||||
import { updateMotto } from "@/actions/user-settings";
|
import { updateMotto } from "@/actions/user-settings";
|
||||||
import { Reveal } from "@/components/motion-reveal";
|
import { Reveal } from "@/components/motion-reveal";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import { db, User } from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -25,10 +26,17 @@ export default async function SettingsPage() {
|
|||||||
mail: string | null;
|
mail: string | null;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
try {
|
try {
|
||||||
user = await prisma.user.findUnique({
|
const [row] = await db
|
||||||
where: { id },
|
.select({
|
||||||
select: { username: true, motto: true, look: true, mail: true },
|
username: User.username,
|
||||||
});
|
motto: User.motto,
|
||||||
|
look: User.look,
|
||||||
|
mail: User.mail,
|
||||||
|
})
|
||||||
|
.from(User)
|
||||||
|
.where(eq(User.id, id))
|
||||||
|
.limit(1);
|
||||||
|
user = row ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
user = null;
|
user = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { desc, eq, sql } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { signOutEverywhere } from "@/actions/sessions";
|
import { signOutEverywhere } from "@/actions/sessions";
|
||||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import { db, WebsiteLoginLogs } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -54,20 +55,30 @@ export default async function SessionsPage({
|
|||||||
const userId = Number(session.user.id);
|
const userId = Number(session.user.id);
|
||||||
const { signedOutAll } = await searchParams;
|
const { signedOutAll } = await searchParams;
|
||||||
|
|
||||||
const logins = await prisma.websiteLoginLogs
|
const logins = await db
|
||||||
.findMany({
|
.select({
|
||||||
where: { userId },
|
id: WebsiteLoginLogs.id,
|
||||||
orderBy: { id: "desc" },
|
ip: WebsiteLoginLogs.ip,
|
||||||
take: 30,
|
userAgent: WebsiteLoginLogs.userAgent,
|
||||||
select: { id: true, ip: true, userAgent: true, createdAt: true },
|
createdAt: WebsiteLoginLogs.createdAt,
|
||||||
})
|
})
|
||||||
|
.from(WebsiteLoginLogs)
|
||||||
|
.where(eq(WebsiteLoginLogs.userId, userId))
|
||||||
|
.orderBy(desc(WebsiteLoginLogs.id))
|
||||||
|
.limit(30)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
type Failed = { id: number; ip_address: string; attempted_at: Date | string };
|
type Failed = { id: number; ip_address: string; attempted_at: Date | string };
|
||||||
const failed = await prisma.$queryRaw<Failed[]>`
|
let failed: Failed[] = [];
|
||||||
SELECT id, ip_address, attempted_at FROM failed_logins
|
try {
|
||||||
WHERE username = ${session.user.name} ORDER BY id DESC LIMIT 15
|
const [rows] = (await db.execute(sql`
|
||||||
`.catch(() => [] as Failed[]);
|
SELECT id, ip_address, attempted_at FROM failed_logins
|
||||||
|
WHERE username = ${session.user.name} ORDER BY id DESC LIMIT 15
|
||||||
|
`)) as unknown as [Failed[], unknown];
|
||||||
|
failed = rows ?? [];
|
||||||
|
} catch {
|
||||||
|
failed = [];
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { asc, eq } from "drizzle-orm";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { buyShopArticle } from "@/actions/shop";
|
import { buyShopArticle } from "@/actions/shop";
|
||||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import { db, WebsiteShopArticles, WebsiteShopCategories } from "@/lib/db";
|
||||||
import { excerpt } from "@/lib/format";
|
import { excerpt } from "@/lib/format";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { creditsPerUnit } from "@/lib/services/paypal";
|
import { creditsPerUnit } from "@/lib/services/paypal";
|
||||||
|
|
||||||
export const revalidate = 300;
|
export const revalidate = 300;
|
||||||
@@ -62,18 +63,18 @@ type ShopCategory = {
|
|||||||
icon: string | null;
|
icon: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ARTICLE_SELECT = {
|
const articleSelect = {
|
||||||
id: true,
|
id: WebsiteShopArticles.id,
|
||||||
name: true,
|
name: WebsiteShopArticles.name,
|
||||||
info: true,
|
info: WebsiteShopArticles.info,
|
||||||
iconUrl: true,
|
iconUrl: WebsiteShopArticles.iconUrl,
|
||||||
color: true,
|
color: WebsiteShopArticles.color,
|
||||||
costs: true,
|
costs: WebsiteShopArticles.costs,
|
||||||
credits: true,
|
credits: WebsiteShopArticles.credits,
|
||||||
duckets: true,
|
duckets: WebsiteShopArticles.duckets,
|
||||||
diamonds: true,
|
diamonds: WebsiteShopArticles.diamonds,
|
||||||
badges: true,
|
badges: WebsiteShopArticles.badges,
|
||||||
position: true,
|
position: WebsiteShopArticles.position,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default async function ShopPage({
|
export default async function ShopPage({
|
||||||
@@ -115,43 +116,31 @@ export default async function ShopPage({
|
|||||||
// Categories for the filter bar.
|
// Categories for the filter bar.
|
||||||
let categories: ShopCategory[] = [];
|
let categories: ShopCategory[] = [];
|
||||||
try {
|
try {
|
||||||
categories = await prisma.websiteShopCategories.findMany({
|
categories = await db
|
||||||
select: { id: true, name: true, icon: true },
|
.select({
|
||||||
orderBy: { name: "asc" },
|
id: WebsiteShopCategories.id,
|
||||||
});
|
name: WebsiteShopCategories.name,
|
||||||
|
icon: WebsiteShopCategories.icon,
|
||||||
|
})
|
||||||
|
.from(WebsiteShopCategories)
|
||||||
|
.orderBy(asc(WebsiteShopCategories.name));
|
||||||
} catch {
|
} catch {
|
||||||
categories = [];
|
categories = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Articles. The category foreign key (website_shop_category_id) is not modelled
|
|
||||||
// in the typed Prisma client, so when filtering we resolve the matching ids via
|
|
||||||
// a guarded raw query, then load the rows through the typed client.
|
|
||||||
let articles: ShopArticle[] = [];
|
let articles: ShopArticle[] = [];
|
||||||
try {
|
try {
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
let ids: bigint[] = [];
|
articles = await db
|
||||||
try {
|
.select(articleSelect)
|
||||||
const rows = await prisma.$queryRawUnsafe<{ id: bigint }[]>(
|
.from(WebsiteShopArticles)
|
||||||
"SELECT id FROM website_shop_articles WHERE website_shop_category_id = ?",
|
.where(eq(WebsiteShopArticles.categoryId, BigInt(categoryId)))
|
||||||
BigInt(categoryId),
|
.orderBy(asc(WebsiteShopArticles.position));
|
||||||
);
|
|
||||||
ids = rows.map((r) => r.id);
|
|
||||||
} catch {
|
|
||||||
ids = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
articles = ids.length
|
|
||||||
? await prisma.websiteShopArticles.findMany({
|
|
||||||
where: { id: { in: ids } },
|
|
||||||
select: ARTICLE_SELECT,
|
|
||||||
orderBy: { position: "asc" },
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
} else {
|
} else {
|
||||||
articles = await prisma.websiteShopArticles.findMany({
|
articles = await db
|
||||||
select: ARTICLE_SELECT,
|
.select(articleSelect)
|
||||||
orderBy: { position: "asc" },
|
.from(WebsiteShopArticles)
|
||||||
});
|
.orderBy(asc(WebsiteShopArticles.position));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
articles = [];
|
articles = [];
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { ContentCard, StatBlock } from "@/components/public/ui";
|
import { ContentCard, StatBlock } from "@/components/public/ui";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, User } from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
creditsPerUnit,
|
creditsPerUnit,
|
||||||
isPayPalConfigured,
|
isPayPalConfigured,
|
||||||
@@ -27,10 +28,12 @@ export default async function TopUpPage({
|
|||||||
// Current credits balance for context (fail soft).
|
// Current credits balance for context (fail soft).
|
||||||
let user: { username: string; credits: number } | null = null;
|
let user: { username: string; credits: number } | null = null;
|
||||||
try {
|
try {
|
||||||
user = await prisma.user.findUnique({
|
const [row] = await db
|
||||||
where: { id },
|
.select({ username: User.username, credits: User.credits })
|
||||||
select: { username: true, credits: true },
|
.from(User)
|
||||||
});
|
.where(eq(User.id, id))
|
||||||
|
.limit(1);
|
||||||
|
user = row ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
user = null;
|
user = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { asc, desc, eq, gte } from "drizzle-orm";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, User, WebsiteTeams } from "@/lib/db";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
export const revalidate = 300;
|
export const revalidate = 300;
|
||||||
@@ -30,30 +31,35 @@ export default async function StaffPage() {
|
|||||||
|
|
||||||
let staff: StaffMember[] = [];
|
let staff: StaffMember[] = [];
|
||||||
try {
|
try {
|
||||||
staff = await prisma.user.findMany({
|
staff = await db
|
||||||
where: { rank: { gte: minStaffRank } },
|
.select({
|
||||||
select: { username: true, look: true, rank: true, motto: true },
|
username: User.username,
|
||||||
orderBy: [{ rank: "desc" }, { username: "asc" }],
|
look: User.look,
|
||||||
take: 100,
|
rank: User.rank,
|
||||||
});
|
motto: User.motto,
|
||||||
|
})
|
||||||
|
.from(User)
|
||||||
|
.where(gte(User.rank, minStaffRank))
|
||||||
|
.orderBy(desc(User.rank), asc(User.username))
|
||||||
|
.limit(100);
|
||||||
} catch {
|
} catch {
|
||||||
staff = [];
|
staff = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let teams: Team[] = [];
|
let teams: Team[] = [];
|
||||||
try {
|
try {
|
||||||
const rows = await prisma.websiteTeams.findMany({
|
const rows = await db
|
||||||
where: { hiddenRank: false },
|
.select({
|
||||||
select: {
|
id: WebsiteTeams.id,
|
||||||
id: true,
|
rankName: WebsiteTeams.rankName,
|
||||||
rankName: true,
|
badge: WebsiteTeams.badge,
|
||||||
badge: true,
|
jobDescription: WebsiteTeams.jobDescription,
|
||||||
jobDescription: true,
|
staffColor: WebsiteTeams.staffColor,
|
||||||
staffColor: true,
|
})
|
||||||
},
|
.from(WebsiteTeams)
|
||||||
orderBy: { id: "asc" },
|
.where(eq(WebsiteTeams.hiddenRank, false))
|
||||||
});
|
.orderBy(asc(WebsiteTeams.id));
|
||||||
teams = rows.map((t) => ({ ...t, id: String(t.id) }));
|
teams = rows.map((row) => ({ ...row, id: String(row.id) }));
|
||||||
} catch {
|
} catch {
|
||||||
teams = [];
|
teams = [];
|
||||||
}
|
}
|
||||||
@@ -69,14 +75,14 @@ export default async function StaffPage() {
|
|||||||
subtitle={t("ranksSubtitle")}
|
subtitle={t("ranksSubtitle")}
|
||||||
>
|
>
|
||||||
<div className="card-grid sm-2 lg-3">
|
<div className="card-grid sm-2 lg-3">
|
||||||
{teams.map((t) => (
|
{teams.map((team) => (
|
||||||
<div key={t.id} className="card">
|
<div key={team.id} className="card">
|
||||||
<h3 style={{ margin: "0 0 0.25rem", color: t.staffColor }}>
|
<h3 style={{ margin: "0 0 0.25rem", color: team.staffColor }}>
|
||||||
{t.rankName}
|
{team.rankName}
|
||||||
</h3>
|
</h3>
|
||||||
{t.jobDescription ? (
|
{team.jobDescription ? (
|
||||||
<p className="muted" style={{ margin: 0 }}>
|
<p className="muted" style={{ margin: 0 }}>
|
||||||
{t.jobDescription}
|
{team.jobDescription}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { and, asc, desc, eq, inArray, or } from "drizzle-orm";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
@@ -12,9 +13,19 @@ import {
|
|||||||
StatBlock,
|
StatBlock,
|
||||||
} from "@/components/public/ui";
|
} from "@/components/public/ui";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import {
|
||||||
|
CameraWeb,
|
||||||
|
db,
|
||||||
|
MessengerFriendrequests,
|
||||||
|
MessengerFriendships,
|
||||||
|
Rooms,
|
||||||
|
User,
|
||||||
|
UsersBadges,
|
||||||
|
UsersCurrency,
|
||||||
|
WebsiteUserGuestbooks,
|
||||||
|
} from "@/lib/db";
|
||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -47,12 +58,12 @@ export async function generateMetadata({
|
|||||||
params: Promise<{ username: string }>;
|
params: Promise<{ username: string }>;
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { username } = await params;
|
const { username } = await params;
|
||||||
const user = await prisma.user
|
const [user] = await db
|
||||||
.findUnique({
|
.select({ username: User.username, motto: User.motto })
|
||||||
where: { username },
|
.from(User)
|
||||||
select: { username: true, motto: true },
|
.where(eq(User.username, username))
|
||||||
})
|
.limit(1)
|
||||||
.catch(() => null);
|
.catch(() => []);
|
||||||
if (!user) return { title: "Profile" };
|
if (!user) return { title: "Profile" };
|
||||||
return {
|
return {
|
||||||
title: user.username,
|
title: user.username,
|
||||||
@@ -78,26 +89,28 @@ export default async function ProfilePage({
|
|||||||
const { username } = await params;
|
const { username } = await params;
|
||||||
const { friend, guestbook: guestbookStatus, error } = await searchParams;
|
const { friend, guestbook: guestbookStatus, error } = await searchParams;
|
||||||
|
|
||||||
const [user, imagerBase, session] = await Promise.all([
|
const [userRows, imagerBase, session] = await Promise.all([
|
||||||
prisma.user.findUnique({
|
db
|
||||||
where: { username },
|
.select({
|
||||||
select: {
|
id: User.id,
|
||||||
id: true,
|
username: User.username,
|
||||||
username: true,
|
motto: User.motto,
|
||||||
motto: true,
|
look: User.look,
|
||||||
look: true,
|
rank: User.rank,
|
||||||
rank: true,
|
credits: User.credits,
|
||||||
credits: true,
|
online: User.online,
|
||||||
online: true,
|
accountCreated: User.accountCreated,
|
||||||
accountCreated: true,
|
})
|
||||||
},
|
.from(User)
|
||||||
}),
|
.where(eq(User.username, username))
|
||||||
|
.limit(1),
|
||||||
siteSettings.get(
|
siteSettings.get(
|
||||||
"habbo_imaging_url",
|
"habbo_imaging_url",
|
||||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||||
),
|
),
|
||||||
auth(),
|
auth(),
|
||||||
]);
|
]);
|
||||||
|
const user = userRows[0] ?? null;
|
||||||
|
|
||||||
if (!user) notFound();
|
if (!user) notFound();
|
||||||
|
|
||||||
@@ -112,34 +125,50 @@ export default async function ProfilePage({
|
|||||||
let outgoingPending = false;
|
let outgoingPending = false;
|
||||||
let incomingPending = false;
|
let incomingPending = false;
|
||||||
if (isLoggedIn && !isSelf) {
|
if (isLoggedIn && !isSelf) {
|
||||||
const [friendship, outgoing, incoming] = await Promise.all([
|
const [friendshipRows, outgoingRows, incomingRows] = await Promise.all([
|
||||||
prisma.messengerFriendships
|
db
|
||||||
.findFirst({
|
.select({ id: MessengerFriendships.id })
|
||||||
where: {
|
.from(MessengerFriendships)
|
||||||
OR: [
|
.where(
|
||||||
{ userOneId: meId, userTwoId: user.id },
|
or(
|
||||||
{ userOneId: user.id, userTwoId: meId },
|
and(
|
||||||
],
|
eq(MessengerFriendships.userOneId, meId),
|
||||||
},
|
eq(MessengerFriendships.userTwoId, user.id),
|
||||||
select: { id: true },
|
),
|
||||||
})
|
and(
|
||||||
.catch(() => null),
|
eq(MessengerFriendships.userOneId, user.id),
|
||||||
prisma.messengerFriendrequests
|
eq(MessengerFriendships.userTwoId, meId),
|
||||||
.findFirst({
|
),
|
||||||
where: { userFromId: meId, userToId: user.id },
|
),
|
||||||
select: { id: true },
|
)
|
||||||
})
|
.limit(1)
|
||||||
.catch(() => null),
|
.catch(() => []),
|
||||||
prisma.messengerFriendrequests
|
db
|
||||||
.findFirst({
|
.select({ id: MessengerFriendrequests.id })
|
||||||
where: { userFromId: user.id, userToId: meId },
|
.from(MessengerFriendrequests)
|
||||||
select: { id: true },
|
.where(
|
||||||
})
|
and(
|
||||||
.catch(() => null),
|
eq(MessengerFriendrequests.userFromId, meId),
|
||||||
|
eq(MessengerFriendrequests.userToId, user.id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.catch(() => []),
|
||||||
|
db
|
||||||
|
.select({ id: MessengerFriendrequests.id })
|
||||||
|
.from(MessengerFriendrequests)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(MessengerFriendrequests.userFromId, user.id),
|
||||||
|
eq(MessengerFriendrequests.userToId, meId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.catch(() => []),
|
||||||
]);
|
]);
|
||||||
isFriend = Boolean(friendship);
|
isFriend = Boolean(friendshipRows[0]);
|
||||||
outgoingPending = Boolean(outgoing);
|
outgoingPending = Boolean(outgoingRows[0]);
|
||||||
incomingPending = Boolean(incoming);
|
incomingPending = Boolean(incomingRows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const feedbackErrorMessage =
|
const feedbackErrorMessage =
|
||||||
@@ -168,14 +197,18 @@ export default async function ProfilePage({
|
|||||||
|
|
||||||
// Currency ledger rows (duckets/diamonds). Credits live on the users row
|
// Currency ledger rows (duckets/diamonds). Credits live on the users row
|
||||||
// itself; the alt-currencies sit in the users_currency ledger keyed by type.
|
// itself; the alt-currencies sit in the users_currency ledger keyed by type.
|
||||||
const currencyRows = await prisma.usersCurrency
|
const currencyRows = await db
|
||||||
.findMany({
|
.select({
|
||||||
where: {
|
type: UsersCurrency.type,
|
||||||
userId: user.id,
|
amount: UsersCurrency.amount,
|
||||||
type: { in: [CURRENCY_DUCKETS, CURRENCY_DIAMONDS] },
|
|
||||||
},
|
|
||||||
select: { type: true, amount: true },
|
|
||||||
})
|
})
|
||||||
|
.from(UsersCurrency)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(UsersCurrency.userId, user.id),
|
||||||
|
inArray(UsersCurrency.type, [CURRENCY_DUCKETS, CURRENCY_DIAMONDS]),
|
||||||
|
),
|
||||||
|
)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
const ducketsAmount =
|
const ducketsAmount =
|
||||||
currencyRows.find((c) => c.type === CURRENCY_DUCKETS)?.amount ?? 0;
|
currencyRows.find((c) => c.type === CURRENCY_DUCKETS)?.amount ?? 0;
|
||||||
@@ -207,21 +240,23 @@ export default async function ProfilePage({
|
|||||||
|
|
||||||
// Friends. messenger_friendships stores one directed row per pair keyed on
|
// Friends. messenger_friendships stores one directed row per pair keyed on
|
||||||
// user_one_id; resolve the user_two_id side to usernames/looks for the grid.
|
// user_one_id; resolve the user_two_id side to usernames/looks for the grid.
|
||||||
const friendships = await prisma.messengerFriendships
|
const friendships = await db
|
||||||
.findMany({
|
.select({ userTwoId: MessengerFriendships.userTwoId })
|
||||||
where: { userOneId: user.id },
|
.from(MessengerFriendships)
|
||||||
orderBy: { friendsSince: "desc" },
|
.where(eq(MessengerFriendships.userOneId, user.id))
|
||||||
take: 18,
|
.orderBy(desc(MessengerFriendships.friendsSince))
|
||||||
select: { userTwoId: true },
|
.limit(18)
|
||||||
})
|
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
const friendIds = Array.from(new Set(friendships.map((f) => f.userTwoId)));
|
const friendIds = Array.from(new Set(friendships.map((f) => f.userTwoId)));
|
||||||
const friendUsers = friendIds.length
|
const friendUsers = friendIds.length
|
||||||
? await prisma.user
|
? await db
|
||||||
.findMany({
|
.select({
|
||||||
where: { id: { in: friendIds } },
|
id: User.id,
|
||||||
select: { id: true, username: true, look: true },
|
username: User.username,
|
||||||
|
look: User.look,
|
||||||
})
|
})
|
||||||
|
.from(User)
|
||||||
|
.where(inArray(User.id, friendIds))
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
: [];
|
: [];
|
||||||
const friendById = new Map(friendUsers.map((f) => [f.id, f]));
|
const friendById = new Map(friendUsers.map((f) => [f.id, f]));
|
||||||
@@ -230,52 +265,54 @@ export default async function ProfilePage({
|
|||||||
.filter((f): f is NonNullable<typeof f> => Boolean(f));
|
.filter((f): f is NonNullable<typeof f> => Boolean(f));
|
||||||
|
|
||||||
// Rooms owned by this user.
|
// Rooms owned by this user.
|
||||||
const rooms = await prisma.rooms
|
const rooms = await db
|
||||||
.findMany({
|
.select({
|
||||||
where: { ownerId: user.id },
|
id: Rooms.id,
|
||||||
orderBy: { users: "desc" },
|
name: Rooms.name,
|
||||||
take: 8,
|
users: Rooms.users,
|
||||||
select: {
|
usersMax: Rooms.usersMax,
|
||||||
id: true,
|
state: Rooms.state,
|
||||||
name: true,
|
|
||||||
users: true,
|
|
||||||
usersMax: true,
|
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
.from(Rooms)
|
||||||
|
.where(eq(Rooms.ownerId, user.id))
|
||||||
|
.orderBy(desc(Rooms.users))
|
||||||
|
.limit(8)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
const badges = await prisma.usersBadges
|
const badges = await db
|
||||||
.findMany({
|
.select()
|
||||||
where: { userId: user.id },
|
.from(UsersBadges)
|
||||||
orderBy: { slotId: "asc" },
|
.where(eq(UsersBadges.userId, user.id))
|
||||||
})
|
.orderBy(asc(UsersBadges.slotId))
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
const photos = await prisma.cameraWeb
|
const photos = await db
|
||||||
.findMany({
|
.select()
|
||||||
where: { userId: user.id },
|
.from(CameraWeb)
|
||||||
orderBy: { timestamp: "desc" },
|
.where(eq(CameraWeb.userId, user.id))
|
||||||
take: 6,
|
.orderBy(desc(CameraWeb.timestamp))
|
||||||
})
|
.limit(6)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
const guestbook = await prisma.websiteUserGuestbooks
|
const guestbook = await db
|
||||||
.findMany({
|
.select()
|
||||||
where: { profileId: user.id },
|
.from(WebsiteUserGuestbooks)
|
||||||
orderBy: { createdAt: "desc" },
|
.where(eq(WebsiteUserGuestbooks.profileId, user.id))
|
||||||
take: 30,
|
.orderBy(desc(WebsiteUserGuestbooks.createdAt))
|
||||||
})
|
.limit(30)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
// Resolve author usernames/looks for the guestbook entries in one query.
|
// Resolve author usernames/looks for the guestbook entries in one query.
|
||||||
const authorIds = Array.from(new Set(guestbook.map((g) => g.userId)));
|
const authorIds = Array.from(new Set(guestbook.map((g) => g.userId)));
|
||||||
const authors = authorIds.length
|
const authors = authorIds.length
|
||||||
? await prisma.user
|
? await db
|
||||||
.findMany({
|
.select({
|
||||||
where: { id: { in: authorIds } },
|
id: User.id,
|
||||||
select: { id: true, username: true, look: true },
|
username: User.username,
|
||||||
|
look: User.look,
|
||||||
})
|
})
|
||||||
|
.from(User)
|
||||||
|
.where(inArray(User.id, authorIds))
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
: [];
|
: [];
|
||||||
const authorById = new Map(authors.map((a) => [a.id, a]));
|
const authorById = new Map(authors.map((a) => [a.id, a]));
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { CheckCircle2, Clock, MailX } from "lucide-react";
|
import { CheckCircle2, Clock, MailX } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { isValidVerificationToken } from "@/actions/email-verify";
|
import { isValidVerificationToken } from "@/actions/email-verify";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, User } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -88,19 +89,20 @@ export default async function VerifyPage({
|
|||||||
const ok = await isValidVerificationToken(normalisedEmail, token);
|
const ok = await isValidVerificationToken(normalisedEmail, token);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
try {
|
try {
|
||||||
const user = await prisma.user.findFirst({
|
const [user] = await db
|
||||||
where: { mail: normalisedEmail },
|
.select({ id: User.id, mailVerified: User.mailVerified })
|
||||||
select: { id: true, mailVerified: true },
|
.from(User)
|
||||||
});
|
.where(eq(User.mail, normalisedEmail))
|
||||||
|
.limit(1);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
status = "invalid";
|
status = "invalid";
|
||||||
} else if (user.mailVerified === "1") {
|
} else if (user.mailVerified === "1") {
|
||||||
status = "already";
|
status = "already";
|
||||||
} else {
|
} else {
|
||||||
await prisma.user.update({
|
await db
|
||||||
where: { id: user.id },
|
.update(User)
|
||||||
data: { mailVerified: "1" },
|
.set({ mailVerified: "1" })
|
||||||
});
|
.where(eq(User.id, user.id));
|
||||||
status = "verified";
|
status = "verified";
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { asc } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
|
import { Achievements, db } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
type Achievement = {
|
type Achievement = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -24,18 +25,22 @@ export default async function AdminAchievements() {
|
|||||||
|
|
||||||
let achievements: Achievement[];
|
let achievements: Achievement[];
|
||||||
try {
|
try {
|
||||||
achievements = await prisma.achievements.findMany({
|
achievements = await db
|
||||||
select: {
|
.select({
|
||||||
name: true,
|
name: Achievements.name,
|
||||||
category: true,
|
category: Achievements.category,
|
||||||
level: true,
|
level: Achievements.level,
|
||||||
rewardAmount: true,
|
rewardAmount: Achievements.rewardAmount,
|
||||||
rewardType: true,
|
rewardType: Achievements.rewardType,
|
||||||
points: true,
|
points: Achievements.points,
|
||||||
progressNeeded: true,
|
progressNeeded: Achievements.progressNeeded,
|
||||||
},
|
})
|
||||||
orderBy: [{ category: "asc" }, { name: "asc" }, { level: "asc" }],
|
.from(Achievements)
|
||||||
});
|
.orderBy(
|
||||||
|
asc(Achievements.category),
|
||||||
|
asc(Achievements.name),
|
||||||
|
asc(Achievements.level),
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
achievements = [];
|
achievements = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { Megaphone } from "lucide-react";
|
import { Megaphone } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { updateAd } from "@/actions/admin-ads";
|
import { updateAd } from "@/actions/admin-ads";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { db, WebsiteAds } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { EditAdDeleteButton } from "../ads-table";
|
import { EditAdDeleteButton } from "../ads-table";
|
||||||
|
|
||||||
export default async function EditAd({
|
export default async function EditAd({
|
||||||
@@ -19,9 +20,14 @@ export default async function EditAd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
let ad: Awaited<ReturnType<typeof prisma.websiteAds.findUnique>> = null;
|
let ad: typeof WebsiteAds.$inferSelect | null = null;
|
||||||
try {
|
try {
|
||||||
ad = await prisma.websiteAds.findUnique({ where: { id: BigInt(id) } });
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(WebsiteAds)
|
||||||
|
.where(eq(WebsiteAds.id, BigInt(id)))
|
||||||
|
.limit(1);
|
||||||
|
ad = row ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { desc } from "drizzle-orm";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { db, WebsiteAds } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { AdsTable } from "./ads-table";
|
import { AdsTable } from "./ads-table";
|
||||||
|
|
||||||
export default async function AdminAds() {
|
export default async function AdminAds() {
|
||||||
@@ -15,12 +16,13 @@ export default async function AdminAds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const t = await getTranslations("pages.admin.ads");
|
const t = await getTranslations("pages.admin.ads");
|
||||||
let ads: Awaited<ReturnType<typeof prisma.websiteAds.findMany>>;
|
let ads: (typeof WebsiteAds.$inferSelect)[] = [];
|
||||||
try {
|
try {
|
||||||
ads = await prisma.websiteAds.findMany({
|
ads = await db
|
||||||
orderBy: { id: "desc" },
|
.select()
|
||||||
take: 500,
|
.from(WebsiteAds)
|
||||||
});
|
.orderBy(desc(WebsiteAds.id))
|
||||||
|
.limit(500);
|
||||||
} catch {
|
} catch {
|
||||||
ads = [];
|
ads = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { desc } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { sendHotelAlert } from "@/actions/admin-alerts";
|
import { sendHotelAlert } from "@/actions/admin-alerts";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { AlertLogs, db } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
const CRITICAL = new Set(["critical", "error", "danger"]);
|
const CRITICAL = new Set(["critical", "error", "danger"]);
|
||||||
const WARNING = new Set(["warning", "warn"]);
|
const WARNING = new Set(["warning", "warn"]);
|
||||||
@@ -58,12 +59,13 @@ export default async function AdminAlerts() {
|
|||||||
|
|
||||||
const t = await getTranslations("pages.admin.alerts");
|
const t = await getTranslations("pages.admin.alerts");
|
||||||
|
|
||||||
let alerts: Awaited<ReturnType<typeof prisma.alertLogs.findMany>>;
|
let alerts: (typeof AlertLogs.$inferSelect)[] = [];
|
||||||
try {
|
try {
|
||||||
alerts = await prisma.alertLogs.findMany({
|
alerts = await db
|
||||||
orderBy: { id: "desc" },
|
.select()
|
||||||
take: 100,
|
.from(AlertLogs)
|
||||||
});
|
.orderBy(desc(AlertLogs.id))
|
||||||
|
.limit(100);
|
||||||
} catch {
|
} catch {
|
||||||
alerts = [];
|
alerts = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { count, gte, inArray, sql } from "drizzle-orm";
|
||||||
import { MessageSquare, Shield, Terminal, Users } from "lucide-react";
|
import { MessageSquare, Shield, Terminal, Users } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { StatsCard } from "@/components/admin/stats-card";
|
import { StatsCard } from "@/components/admin/stats-card";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Ban, db, User } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { redisCache } from "@/lib/redis-cache";
|
import { redisCache } from "@/lib/redis-cache";
|
||||||
|
|
||||||
type DayRow = {
|
type DayRow = {
|
||||||
@@ -34,11 +35,16 @@ async function safeCount(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||||
|
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||||
|
return rows ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
async function safeRawCount(
|
async function safeRawCount(
|
||||||
sql: Promise<{ count: bigint }[]>,
|
query: ReturnType<typeof sql>,
|
||||||
): Promise<{ value: number; ok: boolean }> {
|
): Promise<{ value: number; ok: boolean }> {
|
||||||
try {
|
try {
|
||||||
const rows = await sql;
|
const rows = await rawRows<{ count: bigint }>(query);
|
||||||
return { value: Number(rows[0]?.count ?? 0), ok: true };
|
return { value: Number(rows[0]?.count ?? 0), ok: true };
|
||||||
} catch {
|
} catch {
|
||||||
return { value: 0, ok: false };
|
return { value: 0, ok: false };
|
||||||
@@ -81,24 +87,24 @@ async function getActivityData(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [chatRows, cmdRows, banRows, regRows] = await Promise.all([
|
const [chatRows, cmdRows, banRows, regRows] = await Promise.all([
|
||||||
prisma.$queryRaw<{ day_bucket: bigint; count: bigint }[]>`
|
rawRows<{ day_bucket: bigint; count: bigint }>(sql`
|
||||||
SELECT FLOOR(timestamp / 86400) AS day_bucket, COUNT(*) AS count
|
SELECT FLOOR(timestamp / 86400) AS day_bucket, COUNT(*) AS count
|
||||||
FROM chatlogs_room WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`.catch(
|
FROM chatlogs_room WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`).catch(
|
||||||
() => [],
|
() => [],
|
||||||
),
|
),
|
||||||
prisma.$queryRaw<{ day_bucket: bigint; count: bigint }[]>`
|
rawRows<{ day_bucket: bigint; count: bigint }>(sql`
|
||||||
SELECT FLOOR(timestamp / 86400) AS day_bucket, COUNT(*) AS count
|
SELECT FLOOR(timestamp / 86400) AS day_bucket, COUNT(*) AS count
|
||||||
FROM commandlogs WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`.catch(
|
FROM commandlogs WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`).catch(
|
||||||
() => [],
|
() => [],
|
||||||
),
|
),
|
||||||
prisma.$queryRaw<{ day_bucket: bigint; count: bigint }[]>`
|
rawRows<{ day_bucket: bigint; count: bigint }>(sql`
|
||||||
SELECT FLOOR(timestamp / 86400) AS day_bucket, COUNT(*) AS count
|
SELECT FLOOR(timestamp / 86400) AS day_bucket, COUNT(*) AS count
|
||||||
FROM bans WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`.catch(
|
FROM bans WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`).catch(
|
||||||
() => [],
|
() => [],
|
||||||
),
|
),
|
||||||
prisma.$queryRaw<{ day_bucket: bigint; count: bigint }[]>`
|
rawRows<{ day_bucket: bigint; count: bigint }>(sql`
|
||||||
SELECT FLOOR(account_created / 86400) AS day_bucket, COUNT(*) AS count
|
SELECT FLOOR(account_created / 86400) AS day_bucket, COUNT(*) AS count
|
||||||
FROM users WHERE account_created >= ${rangeStart} GROUP BY day_bucket`.catch(
|
FROM users WHERE account_created >= ${rangeStart} GROUP BY day_bucket`).catch(
|
||||||
() => [],
|
() => [],
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
@@ -124,19 +130,29 @@ async function getActivityData(
|
|||||||
|
|
||||||
const [weekChats, weekCommands, weekBans, weekRegistrations] =
|
const [weekChats, weekCommands, weekBans, weekRegistrations] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
safeRawCount(
|
safeRawCount(sql`
|
||||||
prisma.$queryRaw<{ count: bigint }[]>`
|
SELECT COUNT(*) AS count FROM chatlogs_room WHERE timestamp >= ${weekAgo}`),
|
||||||
SELECT COUNT(*) AS count FROM chatlogs_room WHERE timestamp >= ${weekAgo}`,
|
safeRawCount(sql`
|
||||||
|
SELECT COUNT(*) AS count FROM commandlogs WHERE timestamp >= ${weekAgo}`),
|
||||||
|
safeCount(async () =>
|
||||||
|
Number(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(Ban)
|
||||||
|
.where(gte(Ban.timestamp, weekAgo))
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
safeRawCount(
|
safeCount(async () =>
|
||||||
prisma.$queryRaw<{ count: bigint }[]>`
|
Number(
|
||||||
SELECT COUNT(*) AS count FROM commandlogs WHERE timestamp >= ${weekAgo}`,
|
(
|
||||||
),
|
await db
|
||||||
safeCount(() =>
|
.select({ total: count() })
|
||||||
prisma.ban.count({ where: { timestamp: { gte: weekAgo } } }),
|
.from(User)
|
||||||
),
|
.where(gte(User.accountCreated, weekAgo))
|
||||||
safeCount(() =>
|
)[0]?.total ?? 0,
|
||||||
prisma.user.count({ where: { accountCreated: { gte: weekAgo } } }),
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -146,18 +162,18 @@ async function getActivityData(
|
|||||||
|
|
||||||
let topChatters: ActivityData["topChatters"] = [];
|
let topChatters: ActivityData["topChatters"] = [];
|
||||||
try {
|
try {
|
||||||
const rows = await prisma.$queryRaw<{ userId: number; messages: bigint }[]>`
|
const rows = await rawRows<{ userId: number; messages: bigint }>(sql`
|
||||||
SELECT user_from_id AS userId, COUNT(*) AS messages
|
SELECT user_from_id AS userId, COUNT(*) AS messages
|
||||||
FROM chatlogs_room WHERE timestamp >= ${weekAgo}
|
FROM chatlogs_room WHERE timestamp >= ${weekAgo}
|
||||||
GROUP BY user_from_id ORDER BY messages DESC LIMIT 10
|
GROUP BY user_from_id ORDER BY messages DESC LIMIT 10
|
||||||
`;
|
`);
|
||||||
const userIds = rows.map((c) => Number(c.userId)).filter((n) => n > 0);
|
const userIds = rows.map((c) => Number(c.userId)).filter((n) => n > 0);
|
||||||
const users =
|
const users =
|
||||||
userIds.length > 0
|
userIds.length > 0
|
||||||
? await prisma.user.findMany({
|
? await db
|
||||||
where: { id: { in: userIds } },
|
.select({ id: User.id, username: User.username })
|
||||||
select: { id: true, username: true },
|
.from(User)
|
||||||
})
|
.where(inArray(User.id, userIds))
|
||||||
: [];
|
: [];
|
||||||
const userMap = new Map(users.map((u) => [u.id, u.username]));
|
const userMap = new Map(users.map((u) => [u.id, u.username]));
|
||||||
topChatters = rows.map((c) => {
|
topChatters = rows.map((c) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { and, count, eq, gte, sql, sum } from "drizzle-orm";
|
||||||
import { ArrowLeftRight } from "lucide-react";
|
import { ArrowLeftRight } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
@@ -5,8 +6,14 @@ import { RevenueChart } from "@/components/admin/revenue-chart";
|
|||||||
import { StatsCard } from "@/components/admin/stats-card";
|
import { StatsCard } from "@/components/admin/stats-card";
|
||||||
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
LogsShopPurchases,
|
||||||
|
MarketplaceItems,
|
||||||
|
RoomTradeLog,
|
||||||
|
User,
|
||||||
|
} from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { redisCache } from "@/lib/redis-cache";
|
import { redisCache } from "@/lib/redis-cache";
|
||||||
|
|
||||||
type EconomyData = {
|
type EconomyData = {
|
||||||
@@ -43,6 +50,11 @@ async function safeCount(fn: () => Promise<number>): Promise<number> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||||
|
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||||
|
return rows ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
async function getEconomyData(
|
async function getEconomyData(
|
||||||
locale: string,
|
locale: string,
|
||||||
): Promise<{ data: EconomyData; degraded: boolean }> {
|
): Promise<{ data: EconomyData; degraded: boolean }> {
|
||||||
@@ -54,49 +66,86 @@ async function getEconomyData(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [creditsAgg, pixelsAgg, pointsAgg] = await Promise.all([
|
const [creditsAgg, pixelsAgg, pointsAgg] = await Promise.all([
|
||||||
prisma.user.aggregate({ _sum: { credits: true } }).catch(() => {
|
db
|
||||||
degraded = true;
|
.select({ total: sum(User.credits) })
|
||||||
return null;
|
.from(User)
|
||||||
}),
|
.catch(() => {
|
||||||
prisma.user.aggregate({ _sum: { pixels: true } }).catch(() => {
|
degraded = true;
|
||||||
degraded = true;
|
return [{ total: null }];
|
||||||
return null;
|
}),
|
||||||
}),
|
db
|
||||||
prisma.user.aggregate({ _sum: { points: true } }).catch(() => {
|
.select({ total: sum(User.pixels) })
|
||||||
degraded = true;
|
.from(User)
|
||||||
return null;
|
.catch(() => {
|
||||||
}),
|
degraded = true;
|
||||||
|
return [{ total: null }];
|
||||||
|
}),
|
||||||
|
db
|
||||||
|
.select({ total: sum(User.points) })
|
||||||
|
.from(User)
|
||||||
|
.catch(() => {
|
||||||
|
degraded = true;
|
||||||
|
return [{ total: null }];
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
const totalCredits = creditsAgg?._sum.credits ?? 0;
|
const totalCredits = Number(creditsAgg[0]?.total ?? 0);
|
||||||
const totalPixels = pixelsAgg?._sum.pixels ?? 0;
|
const totalPixels = Number(pixelsAgg[0]?.total ?? 0);
|
||||||
const totalPoints = pointsAgg?._sum.points ?? 0;
|
const totalPoints = Number(pointsAgg[0]?.total ?? 0);
|
||||||
|
|
||||||
let shopPurchases = 0;
|
let shopPurchases = 0;
|
||||||
let shopCreditsSpent = 0;
|
let shopCreditsSpent = 0;
|
||||||
let shopPointsSpent = 0;
|
let shopPointsSpent = 0;
|
||||||
try {
|
try {
|
||||||
const shopRevenue = await prisma.logsShopPurchases.aggregate({
|
const [shopRevenue] = await db
|
||||||
where: { timestamp: { gte: weekAgo } },
|
.select({
|
||||||
_sum: { costCredits: true, costPoints: true },
|
purchases: count(),
|
||||||
_count: true,
|
credits: sum(LogsShopPurchases.costCredits),
|
||||||
});
|
points: sum(LogsShopPurchases.costPoints),
|
||||||
shopPurchases =
|
})
|
||||||
typeof shopRevenue._count === "number" ? shopRevenue._count : 0;
|
.from(LogsShopPurchases)
|
||||||
shopCreditsSpent = shopRevenue._sum?.costCredits ?? 0;
|
.where(gte(LogsShopPurchases.timestamp, weekAgo));
|
||||||
shopPointsSpent = shopRevenue._sum?.costPoints ?? 0;
|
shopPurchases = Number(shopRevenue?.purchases ?? 0);
|
||||||
|
shopCreditsSpent = Number(shopRevenue?.credits ?? 0);
|
||||||
|
shopPointsSpent = Number(shopRevenue?.points ?? 0);
|
||||||
} catch {
|
} catch {
|
||||||
degraded = true;
|
degraded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [marketplaceActive, marketplaceSold, weekTrades] = await Promise.all([
|
const [marketplaceActive, marketplaceSold, weekTrades] = await Promise.all([
|
||||||
safeCount(() => prisma.marketplaceItems.count({ where: { state: 1 } })),
|
safeCount(async () =>
|
||||||
safeCount(() =>
|
Number(
|
||||||
prisma.marketplaceItems.count({
|
(
|
||||||
where: { state: 2, soldTimestamp: { gte: weekAgo } },
|
await db
|
||||||
}),
|
.select({ total: count() })
|
||||||
|
.from(MarketplaceItems)
|
||||||
|
.where(eq(MarketplaceItems.state, 1))
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
safeCount(() =>
|
safeCount(async () =>
|
||||||
prisma.roomTradeLog.count({ where: { timestamp: { gte: weekAgo } } }),
|
Number(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(MarketplaceItems)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(MarketplaceItems.state, 2),
|
||||||
|
gte(MarketplaceItems.soldTimestamp, weekAgo),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
safeCount(async () =>
|
||||||
|
Number(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(RoomTradeLog)
|
||||||
|
.where(gte(RoomTradeLog.timestamp, weekAgo))
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -112,20 +161,18 @@ async function getEconomyData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const revRows = await prisma.$queryRaw<
|
const revRows = await rawRows<{
|
||||||
{
|
day_bucket: bigint;
|
||||||
day_bucket: bigint;
|
credits: number | bigint;
|
||||||
credits: number | bigint;
|
points: number | bigint;
|
||||||
points: number | bigint;
|
}>(sql`
|
||||||
}[]
|
|
||||||
>`
|
|
||||||
SELECT FLOOR(timestamp / 86400) AS day_bucket,
|
SELECT FLOOR(timestamp / 86400) AS day_bucket,
|
||||||
COALESCE(SUM(cost_credits), 0) AS credits,
|
COALESCE(SUM(cost_credits), 0) AS credits,
|
||||||
COALESCE(SUM(cost_points), 0) AS points
|
COALESCE(SUM(cost_points), 0) AS points
|
||||||
FROM logs_shop_purchases
|
FROM logs_shop_purchases
|
||||||
WHERE timestamp >= ${rangeStart}
|
WHERE timestamp >= ${rangeStart}
|
||||||
GROUP BY day_bucket ORDER BY day_bucket
|
GROUP BY day_bucket ORDER BY day_bucket
|
||||||
`;
|
`);
|
||||||
const revMap = new Map(
|
const revMap = new Map(
|
||||||
revRows.map((r) => [
|
revRows.map((r) => [
|
||||||
Number(r.day_bucket),
|
Number(r.day_bucket),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { count, eq, gt, gte, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
ArrowLeftRight,
|
ArrowLeftRight,
|
||||||
Clock,
|
Clock,
|
||||||
@@ -12,8 +13,8 @@ import { DashboardChart } from "@/components/admin/dashboard-chart";
|
|||||||
import { PeakHoursHeatmap } from "@/components/admin/peak-hours-heatmap";
|
import { PeakHoursHeatmap } from "@/components/admin/peak-hours-heatmap";
|
||||||
import { StatsCard } from "@/components/admin/stats-card";
|
import { StatsCard } from "@/components/admin/stats-card";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Ban, db, Rooms, RoomTradeLog, User } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { redisCache } from "@/lib/redis-cache";
|
import { redisCache } from "@/lib/redis-cache";
|
||||||
|
|
||||||
type AnalyticsData = {
|
type AnalyticsData = {
|
||||||
@@ -57,6 +58,11 @@ async function safeCount(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||||
|
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||||
|
return rows ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
async function getAnalyticsData(
|
async function getAnalyticsData(
|
||||||
locale: string,
|
locale: string,
|
||||||
): Promise<{ data: AnalyticsData; degraded: boolean }> {
|
): Promise<{ data: AnalyticsData; degraded: boolean }> {
|
||||||
@@ -78,30 +84,82 @@ async function getAnalyticsData(
|
|||||||
weekTrades,
|
weekTrades,
|
||||||
weekBans,
|
weekBans,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
safeCount(() => prisma.user.count()),
|
safeCount(async () =>
|
||||||
safeCount(() => prisma.user.count({ where: { online: "1" } })),
|
Number((await db.select({ total: count() }).from(User))[0]?.total ?? 0),
|
||||||
safeCount(() =>
|
|
||||||
prisma.user.count({ where: { accountCreated: { gte: weekAgo } } }),
|
|
||||||
),
|
),
|
||||||
safeCount(() =>
|
safeCount(async () =>
|
||||||
prisma.user.count({ where: { accountCreated: { gte: monthAgo } } }),
|
Number(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(User)
|
||||||
|
.where(eq(User.online, "1"))
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
safeCount(async () =>
|
||||||
|
Number(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(User)
|
||||||
|
.where(gte(User.accountCreated, weekAgo))
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
safeCount(async () =>
|
||||||
|
Number(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(User)
|
||||||
|
.where(gte(User.accountCreated, monthAgo))
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
safeCount(async () =>
|
||||||
|
Number(
|
||||||
|
(await db.select({ total: count() }).from(Rooms))[0]?.total ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
safeCount(async () =>
|
||||||
|
Number(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(Rooms)
|
||||||
|
.where(gt(Rooms.users, 0))
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
safeCount(() => prisma.rooms.count()),
|
|
||||||
safeCount(() => prisma.rooms.count({ where: { users: { gt: 0 } } })),
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const rows = await prisma.$queryRaw<{ count: bigint }[]>`
|
const rows = await rawRows<{ count: bigint }>(sql`
|
||||||
SELECT COUNT(*) AS count FROM chatlogs_room WHERE timestamp >= ${weekAgo}`;
|
SELECT COUNT(*) AS count FROM chatlogs_room WHERE timestamp >= ${weekAgo}`);
|
||||||
return { value: Number(rows[0]?.count ?? 0), ok: true };
|
return { value: Number(rows[0]?.count ?? 0), ok: true };
|
||||||
} catch {
|
} catch {
|
||||||
return { value: 0, ok: false };
|
return { value: 0, ok: false };
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
safeCount(() =>
|
safeCount(async () =>
|
||||||
prisma.roomTradeLog.count({ where: { timestamp: { gte: weekAgo } } }),
|
Number(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(RoomTradeLog)
|
||||||
|
.where(gte(RoomTradeLog.timestamp, weekAgo))
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
safeCount(() =>
|
safeCount(async () =>
|
||||||
prisma.ban.count({ where: { timestamp: { gte: weekAgo } } }),
|
Number(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(Ban)
|
||||||
|
.where(gte(Ban.timestamp, weekAgo))
|
||||||
|
)[0]?.total ?? 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -121,14 +179,12 @@ async function getAnalyticsData(
|
|||||||
|
|
||||||
let registrationChart: { label: string; value: number }[] = [];
|
let registrationChart: { label: string; value: number }[] = [];
|
||||||
try {
|
try {
|
||||||
const regRows = await prisma.$queryRaw<
|
const regRows = await rawRows<{ day_bucket: bigint; count: bigint }>(sql`
|
||||||
{ day_bucket: bigint; count: bigint }[]
|
|
||||||
>`
|
|
||||||
SELECT FLOOR(account_created / 86400) AS day_bucket, COUNT(*) AS count
|
SELECT FLOOR(account_created / 86400) AS day_bucket, COUNT(*) AS count
|
||||||
FROM users
|
FROM users
|
||||||
WHERE account_created >= ${monthAgo}
|
WHERE account_created >= ${monthAgo}
|
||||||
GROUP BY day_bucket ORDER BY day_bucket
|
GROUP BY day_bucket ORDER BY day_bucket
|
||||||
`;
|
`);
|
||||||
const regCountMap = new Map(
|
const regCountMap = new Map(
|
||||||
regRows.map((r) => [Number(r.day_bucket), Number(r.count)]),
|
regRows.map((r) => [Number(r.day_bucket), Number(r.count)]),
|
||||||
);
|
);
|
||||||
@@ -152,23 +208,21 @@ async function getAnalyticsData(
|
|||||||
const grid: number[][] = Array.from({ length: 7 }, () => Array(24).fill(0));
|
const grid: number[][] = Array.from({ length: 7 }, () => Array(24).fill(0));
|
||||||
let max = 0;
|
let max = 0;
|
||||||
try {
|
try {
|
||||||
const peakRows = await prisma.$queryRaw<
|
const peakRows = await rawRows<{ d: number; h: number; c: bigint }>(sql`
|
||||||
{ d: number; h: number; c: bigint }[]
|
|
||||||
>`
|
|
||||||
SELECT ((DAYOFWEEK(FROM_UNIXTIME(last_online)) + 5) % 7) AS d,
|
SELECT ((DAYOFWEEK(FROM_UNIXTIME(last_online)) + 5) % 7) AS d,
|
||||||
HOUR(FROM_UNIXTIME(last_online)) AS h,
|
HOUR(FROM_UNIXTIME(last_online)) AS h,
|
||||||
COUNT(*) AS c
|
COUNT(*) AS c
|
||||||
FROM users
|
FROM users
|
||||||
WHERE last_online >= ${weekAgo}
|
WHERE last_online >= ${weekAgo}
|
||||||
GROUP BY d, h
|
GROUP BY d, h
|
||||||
`;
|
`);
|
||||||
for (const r of peakRows) {
|
for (const r of peakRows) {
|
||||||
const day = Number(r.d);
|
const day = Number(r.d);
|
||||||
const hour = Number(r.h);
|
const hour = Number(r.h);
|
||||||
if (day < 0 || day > 6 || hour < 0 || hour > 23) continue;
|
if (day < 0 || day > 6 || hour < 0 || hour > 23) continue;
|
||||||
const count = Number(r.c);
|
const cnt = Number(r.c);
|
||||||
grid[day][hour] = count;
|
grid[day][hour] = cnt;
|
||||||
if (count > max) max = count;
|
if (cnt > max) max = cnt;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
degraded = true;
|
degraded = true;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { desc, inArray } from "drizzle-orm";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { dismissApplication } from "@/actions/admin-applications";
|
import { dismissApplication } from "@/actions/admin-applications";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { db, User, WebsiteStaffApplications } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
type ApplicationRow = {
|
type ApplicationRow = {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
@@ -25,10 +26,11 @@ export default async function AdminApplications() {
|
|||||||
const t = await getTranslations("pages.admin.applications");
|
const t = await getTranslations("pages.admin.applications");
|
||||||
let applications: ApplicationRow[];
|
let applications: ApplicationRow[];
|
||||||
try {
|
try {
|
||||||
applications = await prisma.websiteStaffApplications.findMany({
|
applications = await db
|
||||||
orderBy: { createdAt: "desc" },
|
.select()
|
||||||
take: 100,
|
.from(WebsiteStaffApplications)
|
||||||
});
|
.orderBy(desc(WebsiteStaffApplications.createdAt))
|
||||||
|
.limit(100);
|
||||||
} catch {
|
} catch {
|
||||||
applications = [];
|
applications = [];
|
||||||
}
|
}
|
||||||
@@ -38,10 +40,10 @@ export default async function AdminApplications() {
|
|||||||
if (applications.length > 0) {
|
if (applications.length > 0) {
|
||||||
try {
|
try {
|
||||||
const ids = Array.from(new Set(applications.map((a) => a.userId)));
|
const ids = Array.from(new Set(applications.map((a) => a.userId)));
|
||||||
const users = await prisma.user.findMany({
|
const users = await db
|
||||||
where: { id: { in: ids } },
|
.select({ id: User.id, username: User.username })
|
||||||
select: { id: true, username: true },
|
.from(User)
|
||||||
});
|
.where(inArray(User.id, ids));
|
||||||
for (const u of users) userMap.set(u.id, u.username);
|
for (const u of users) userMap.set(u.id, u.username);
|
||||||
} catch {
|
} catch {
|
||||||
// no DB — fall back to raw ids
|
// no DB — fall back to raw ids
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { Newspaper } from "lucide-react";
|
import { Newspaper } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
@@ -5,8 +6,8 @@ import { getTranslations } from "next-intl/server";
|
|||||||
import { deleteArticle, updateArticle } from "@/actions/admin-articles";
|
import { deleteArticle, updateArticle } from "@/actions/admin-articles";
|
||||||
import { ArticleForm } from "@/components/admin/article-form";
|
import { ArticleForm } from "@/components/admin/article-form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { db, WebsiteArticles } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export default async function EditArticle({
|
export default async function EditArticle({
|
||||||
params,
|
params,
|
||||||
@@ -23,19 +24,19 @@ export default async function EditArticle({
|
|||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const { error } = await searchParams;
|
const { error } = await searchParams;
|
||||||
const t = await getTranslations("pages.admin.articles");
|
const t = await getTranslations("pages.admin.articles");
|
||||||
const article = await prisma.websiteArticles
|
const [article] = await db
|
||||||
.findUnique({
|
.select({
|
||||||
where: { id: BigInt(id) },
|
id: WebsiteArticles.id,
|
||||||
select: {
|
slug: WebsiteArticles.slug,
|
||||||
id: true,
|
title: WebsiteArticles.title,
|
||||||
slug: true,
|
shortStory: WebsiteArticles.shortStory,
|
||||||
title: true,
|
fullStory: WebsiteArticles.fullStory,
|
||||||
shortStory: true,
|
image: WebsiteArticles.image,
|
||||||
fullStory: true,
|
|
||||||
image: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.catch(() => null);
|
.from(WebsiteArticles)
|
||||||
|
.where(eq(WebsiteArticles.id, BigInt(id)))
|
||||||
|
.limit(1)
|
||||||
|
.catch(() => []);
|
||||||
if (!article) notFound();
|
if (!article) notFound();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { desc, inArray } from "drizzle-orm";
|
||||||
import { Newspaper } from "lucide-react";
|
import { Newspaper } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
@@ -6,9 +7,9 @@ import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
|||||||
import { ArticleCard } from "@/components/admin/article-card";
|
import { ArticleCard } from "@/components/admin/article-card";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { db, User, WebsiteArticles } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export default async function AdminArticles({
|
export default async function AdminArticles({
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -22,19 +23,18 @@ export default async function AdminArticles({
|
|||||||
|
|
||||||
const { error } = await searchParams;
|
const { error } = await searchParams;
|
||||||
const t = await getTranslations("pages.admin.articles");
|
const t = await getTranslations("pages.admin.articles");
|
||||||
const articles = await prisma.websiteArticles
|
const articles = await db
|
||||||
.findMany({
|
.select({
|
||||||
orderBy: { createdAt: "desc" },
|
id: WebsiteArticles.id,
|
||||||
select: {
|
title: WebsiteArticles.title,
|
||||||
id: true,
|
slug: WebsiteArticles.slug,
|
||||||
title: true,
|
image: WebsiteArticles.image,
|
||||||
slug: true,
|
createdAt: WebsiteArticles.createdAt,
|
||||||
image: true,
|
userId: WebsiteArticles.userId,
|
||||||
createdAt: true,
|
|
||||||
userId: true,
|
|
||||||
},
|
|
||||||
take: 50,
|
|
||||||
})
|
})
|
||||||
|
.from(WebsiteArticles)
|
||||||
|
.orderBy(desc(WebsiteArticles.createdAt))
|
||||||
|
.limit(50)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
const authorMap = new Map<number, string>();
|
const authorMap = new Map<number, string>();
|
||||||
@@ -42,11 +42,10 @@ export default async function AdminArticles({
|
|||||||
.map((a) => a.userId)
|
.map((a) => a.userId)
|
||||||
.filter((id): id is number => Boolean(id));
|
.filter((id): id is number => Boolean(id));
|
||||||
if (authorIds.length > 0) {
|
if (authorIds.length > 0) {
|
||||||
const authors = await prisma.user
|
const authors = await db
|
||||||
.findMany({
|
.select({ id: User.id, username: User.username })
|
||||||
where: { id: { in: authorIds } },
|
.from(User)
|
||||||
select: { id: true, username: true },
|
.where(inArray(User.id, authorIds))
|
||||||
})
|
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
for (const u of authors) authorMap.set(u.id, u.username);
|
for (const u of authors) authorMap.set(u.id, u.username);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { asc } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { uploadBadge } from "@/actions/admin-badge-upload";
|
import { uploadBadge } from "@/actions/admin-badge-upload";
|
||||||
import { giveBadge } from "@/actions/admin-badges";
|
import { giveBadge } from "@/actions/admin-badges";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { db, WebsiteBadges } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export const metadata = { title: "Badges" };
|
export const metadata = { title: "Badges" };
|
||||||
|
|
||||||
@@ -21,12 +22,13 @@ export default async function AdminBadges({
|
|||||||
const t = await getTranslations("pages.admin.badges");
|
const t = await getTranslations("pages.admin.badges");
|
||||||
const { uploaded, error } = await searchParams;
|
const { uploaded, error } = await searchParams;
|
||||||
|
|
||||||
let badges: Awaited<ReturnType<typeof prisma.websiteBadges.findMany>>;
|
let badges: (typeof WebsiteBadges.$inferSelect)[] = [];
|
||||||
try {
|
try {
|
||||||
badges = await prisma.websiteBadges.findMany({
|
badges = await db
|
||||||
orderBy: { badgeName: "asc" },
|
.select()
|
||||||
take: 500,
|
.from(WebsiteBadges)
|
||||||
});
|
.orderBy(asc(WebsiteBadges.badgeName))
|
||||||
|
.limit(500);
|
||||||
} catch {
|
} catch {
|
||||||
badges = [];
|
badges = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { asc } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { db, WebsiteBanner } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
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() {
|
||||||
@@ -9,9 +10,10 @@ export default async function BannersPage() {
|
|||||||
redirect("/admin");
|
redirect("/admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
const banners = await prisma.websiteBanner.findMany({
|
const banners = await db
|
||||||
orderBy: { sortOrder: "asc" },
|
.select()
|
||||||
});
|
.from(WebsiteBanner)
|
||||||
|
.orderBy(asc(WebsiteBanner.sortOrder));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { desc, eq, gt, or } from "drizzle-orm";
|
||||||
import { Ban } from "lucide-react";
|
import { Ban } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
@@ -5,10 +6,10 @@ import { createBan, liftBan } from "@/actions/admin-bans";
|
|||||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { activeBanWhere, unixNow } from "@/lib/bans";
|
import { unixNow } from "@/lib/bans";
|
||||||
|
import { Ban as BanTable, db } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export default async function AdminBans() {
|
export default async function AdminBans() {
|
||||||
const { session, permissions } = await getAdminContext();
|
const { session, permissions } = await getAdminContext();
|
||||||
@@ -18,13 +19,14 @@ export default async function AdminBans() {
|
|||||||
|
|
||||||
const t = await getTranslations("pages.admin.bans");
|
const t = await getTranslations("pages.admin.bans");
|
||||||
const now = unixNow();
|
const now = unixNow();
|
||||||
let bans: Awaited<ReturnType<typeof prisma.ban.findMany>> = [];
|
let bans: (typeof BanTable.$inferSelect)[] = [];
|
||||||
try {
|
try {
|
||||||
bans = await prisma.ban.findMany({
|
bans = await db
|
||||||
where: activeBanWhere(now),
|
.select()
|
||||||
orderBy: { timestamp: "desc" },
|
.from(BanTable)
|
||||||
take: 100,
|
.where(or(eq(BanTable.banExpire, 0), gt(BanTable.banExpire, now)))
|
||||||
});
|
.orderBy(desc(BanTable.timestamp))
|
||||||
|
.limit(100);
|
||||||
} catch {
|
} catch {
|
||||||
bans = [];
|
bans = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { asc, eq } from "drizzle-orm";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { CalendarCampaigns, CalendarRewards, db } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
type Campaign = {
|
type Campaign = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -48,18 +49,20 @@ export default async function AdminCalendarCampaign({
|
|||||||
|
|
||||||
let campaign: Campaign | null = null;
|
let campaign: Campaign | null = null;
|
||||||
try {
|
try {
|
||||||
campaign = await prisma.calendarCampaigns.findUnique({
|
const [row] = await db
|
||||||
where: { id: campaignId },
|
.select({
|
||||||
select: {
|
id: CalendarCampaigns.id,
|
||||||
id: true,
|
name: CalendarCampaigns.name,
|
||||||
name: true,
|
image: CalendarCampaigns.image,
|
||||||
image: true,
|
startTimestamp: CalendarCampaigns.startTimestamp,
|
||||||
startTimestamp: true,
|
totalDays: CalendarCampaigns.totalDays,
|
||||||
totalDays: true,
|
lockExpired: CalendarCampaigns.lockExpired,
|
||||||
lockExpired: true,
|
enabled: CalendarCampaigns.enabled,
|
||||||
enabled: true,
|
})
|
||||||
},
|
.from(CalendarCampaigns)
|
||||||
});
|
.where(eq(CalendarCampaigns.id, campaignId))
|
||||||
|
.limit(1);
|
||||||
|
campaign = row ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
campaign = null;
|
campaign = null;
|
||||||
}
|
}
|
||||||
@@ -67,25 +70,25 @@ export default async function AdminCalendarCampaign({
|
|||||||
|
|
||||||
let rewards: Reward[] = [];
|
let rewards: Reward[] = [];
|
||||||
try {
|
try {
|
||||||
rewards = await prisma.calendarRewards.findMany({
|
rewards = await db
|
||||||
where: { campaignId },
|
.select({
|
||||||
select: {
|
id: CalendarRewards.id,
|
||||||
id: true,
|
campaignId: CalendarRewards.campaignId,
|
||||||
campaignId: true,
|
productName: CalendarRewards.productName,
|
||||||
productName: true,
|
customImage: CalendarRewards.customImage,
|
||||||
customImage: true,
|
credits: CalendarRewards.credits,
|
||||||
credits: true,
|
pixels: CalendarRewards.pixels,
|
||||||
pixels: true,
|
points: CalendarRewards.points,
|
||||||
points: true,
|
pointsType: CalendarRewards.pointsType,
|
||||||
pointsType: true,
|
badge: CalendarRewards.badge,
|
||||||
badge: true,
|
itemId: CalendarRewards.itemId,
|
||||||
itemId: true,
|
subscriptionType: CalendarRewards.subscriptionType,
|
||||||
subscriptionType: true,
|
subscriptionDays: CalendarRewards.subscriptionDays,
|
||||||
subscriptionDays: true,
|
})
|
||||||
},
|
.from(CalendarRewards)
|
||||||
orderBy: { id: "asc" },
|
.where(eq(CalendarRewards.campaignId, campaignId))
|
||||||
take: 500,
|
.orderBy(asc(CalendarRewards.id))
|
||||||
});
|
.limit(500);
|
||||||
} catch {
|
} catch {
|
||||||
rewards = [];
|
rewards = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { count, desc } from "drizzle-orm";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
|
import { CalendarCampaigns, CalendarRewards, db } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
type Campaign = {
|
type Campaign = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -27,19 +28,19 @@ export default async function AdminCalendar() {
|
|||||||
let campaigns: Campaign[] = [];
|
let campaigns: Campaign[] = [];
|
||||||
let loadError = false;
|
let loadError = false;
|
||||||
try {
|
try {
|
||||||
campaigns = await prisma.calendarCampaigns.findMany({
|
campaigns = await db
|
||||||
select: {
|
.select({
|
||||||
id: true,
|
id: CalendarCampaigns.id,
|
||||||
name: true,
|
name: CalendarCampaigns.name,
|
||||||
image: true,
|
image: CalendarCampaigns.image,
|
||||||
startTimestamp: true,
|
startTimestamp: CalendarCampaigns.startTimestamp,
|
||||||
totalDays: true,
|
totalDays: CalendarCampaigns.totalDays,
|
||||||
lockExpired: true,
|
lockExpired: CalendarCampaigns.lockExpired,
|
||||||
enabled: true,
|
enabled: CalendarCampaigns.enabled,
|
||||||
},
|
})
|
||||||
orderBy: { id: "desc" },
|
.from(CalendarCampaigns)
|
||||||
take: 200,
|
.orderBy(desc(CalendarCampaigns.id))
|
||||||
});
|
.limit(200);
|
||||||
} catch {
|
} catch {
|
||||||
campaigns = [];
|
campaigns = [];
|
||||||
loadError = true;
|
loadError = true;
|
||||||
@@ -48,13 +49,16 @@ export default async function AdminCalendar() {
|
|||||||
const rewardCounts = new Map<number, number>();
|
const rewardCounts = new Map<number, number>();
|
||||||
let totalRewards = 0;
|
let totalRewards = 0;
|
||||||
try {
|
try {
|
||||||
const grouped = await prisma.calendarRewards.groupBy({
|
const grouped = await db
|
||||||
by: ["campaignId"],
|
.select({
|
||||||
_count: { _all: true },
|
campaignId: CalendarRewards.campaignId,
|
||||||
});
|
total: count(),
|
||||||
|
})
|
||||||
|
.from(CalendarRewards)
|
||||||
|
.groupBy(CalendarRewards.campaignId);
|
||||||
for (const g of grouped) {
|
for (const g of grouped) {
|
||||||
rewardCounts.set(g.campaignId, g._count._all);
|
rewardCounts.set(g.campaignId, Number(g.total));
|
||||||
totalRewards += g._count._all;
|
totalRewards += Number(g.total);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
loadError = true;
|
loadError = true;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
import { CatalogPages, db } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { loadCatalogItemsData } from "@/lib/services/catalog-items-loader";
|
import { loadCatalogItemsData } from "@/lib/services/catalog-items-loader";
|
||||||
import { getAncestors } from "@/lib/services/catalog-tree";
|
import { getAncestors } from "@/lib/services/catalog-tree";
|
||||||
import { CatalogDetailTabs } from "./catalog-detail-tabs";
|
import { CatalogDetailTabs } from "./catalog-detail-tabs";
|
||||||
@@ -22,18 +23,24 @@ export default async function CatalogEditPage({
|
|||||||
const pageId = parseInt(id, 10);
|
const pageId = parseInt(id, 10);
|
||||||
if (Number.isNaN(pageId)) notFound();
|
if (Number.isNaN(pageId)) notFound();
|
||||||
|
|
||||||
const catalogPage = await prisma.catalogPages.findUnique({
|
const [catalogPage] = await db
|
||||||
where: { id: pageId },
|
.select()
|
||||||
});
|
.from(CatalogPages)
|
||||||
|
.where(eq(CatalogPages.id, pageId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!catalogPage) notFound();
|
if (!catalogPage) notFound();
|
||||||
|
|
||||||
const [children, itemsData, ancestors] = await Promise.all([
|
const [children, itemsData, ancestors] = await Promise.all([
|
||||||
prisma.catalogPages.findMany({
|
db
|
||||||
where: { parentId: pageId },
|
.select({
|
||||||
orderBy: { orderNum: "asc" },
|
id: CatalogPages.id,
|
||||||
select: { id: true, caption: true, enabled: true },
|
caption: CatalogPages.caption,
|
||||||
}),
|
enabled: CatalogPages.enabled,
|
||||||
|
})
|
||||||
|
.from(CatalogPages)
|
||||||
|
.where(eq(CatalogPages.parentId, pageId))
|
||||||
|
.orderBy(asc(CatalogPages.orderNum)),
|
||||||
loadCatalogItemsData(pageId),
|
loadCatalogItemsData(pageId),
|
||||||
getAncestors(catalogPage.parentId),
|
getAncestors(catalogPage.parentId),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { CatalogItemsBc, CatalogPagesBc, db } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
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({
|
||||||
@@ -19,13 +20,18 @@ export default async function BcPageDetailPage({
|
|||||||
|
|
||||||
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 db
|
||||||
|
.select()
|
||||||
|
.from(CatalogPagesBc)
|
||||||
|
.where(eq(CatalogPagesBc.id, id))
|
||||||
|
.limit(1);
|
||||||
if (!bcPage) notFound();
|
if (!bcPage) notFound();
|
||||||
|
|
||||||
const bcItems = await prisma.catalogItemsBc.findMany({
|
const bcItems = await db
|
||||||
where: { pageId: id },
|
.select()
|
||||||
orderBy: { orderNumber: "asc" },
|
.from(CatalogItemsBc)
|
||||||
});
|
.where(eq(CatalogItemsBc.pageId, id))
|
||||||
|
.orderBy(asc(CatalogItemsBc.orderNumber));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BcPageDetail
|
<BcPageDetail
|
||||||
|
|||||||
+184
-106
@@ -1,16 +1,41 @@
|
|||||||
|
import {
|
||||||
|
and,
|
||||||
|
asc,
|
||||||
|
count,
|
||||||
|
desc,
|
||||||
|
eq,
|
||||||
|
inArray,
|
||||||
|
like,
|
||||||
|
or,
|
||||||
|
type SQL,
|
||||||
|
sql,
|
||||||
|
} from "drizzle-orm";
|
||||||
|
import type { AnyMySqlColumn } from "drizzle-orm/mysql-core";
|
||||||
import { Store } from "lucide-react";
|
import { Store } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||||
import { CatalogTree } from "@/components/admin/catalog-tree";
|
import { CatalogTree } from "@/components/admin/catalog-tree";
|
||||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||||
|
import {
|
||||||
|
CatalogItems,
|
||||||
|
CatalogItemsBc,
|
||||||
|
CatalogPages,
|
||||||
|
CatalogPagesBc,
|
||||||
|
db,
|
||||||
|
} from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { Prisma, prisma } from "@/lib/prisma";
|
|
||||||
import { getCatalogItemCounts } from "@/lib/services/catalog-tree";
|
import { getCatalogItemCounts } from "@/lib/services/catalog-tree";
|
||||||
import { CatalogActions } from "./catalog-actions";
|
import { CatalogActions } from "./catalog-actions";
|
||||||
import { CatalogManagerDialog } from "./catalog-manager-dialog-wrapper";
|
import { CatalogManagerDialog } from "./catalog-manager-dialog-wrapper";
|
||||||
import { CatalogTable } from "./catalog-table";
|
import { CatalogTable } from "./catalog-table";
|
||||||
import { CatalogViewToggle } from "./catalog-view-toggle";
|
import { CatalogViewToggle } from "./catalog-view-toggle";
|
||||||
|
|
||||||
|
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 CatalogPagesPage({
|
export default async function CatalogPagesPage({
|
||||||
params: paramsPromise,
|
params: paramsPromise,
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -33,7 +58,6 @@ export default async function CatalogPagesPage({
|
|||||||
const viewMode = sp.get("view") || "tree";
|
const viewMode = sp.get("view") || "tree";
|
||||||
const catalogType = sp.get("catalog") || "normal";
|
const catalogType = sp.get("catalog") || "normal";
|
||||||
|
|
||||||
// ── Builder Club mode ──────────────────────────────────────────
|
|
||||||
type TreeNodeShape = {
|
type TreeNodeShape = {
|
||||||
id: number;
|
id: number;
|
||||||
caption: string;
|
caption: string;
|
||||||
@@ -48,41 +72,56 @@ export default async function CatalogPagesPage({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (catalogType === "bc") {
|
if (catalogType === "bc") {
|
||||||
const [totalBcPages, totalBcItems, enabledBcPages] = await Promise.all([
|
const [totalBcPagesRows, totalBcItemsRows, enabledBcPagesRows] =
|
||||||
prisma.catalogPagesBc.count(),
|
await Promise.all([
|
||||||
prisma.catalogItemsBc.count(),
|
db.select({ total: count() }).from(CatalogPagesBc),
|
||||||
prisma.catalogPagesBc.count({ where: { enabled: "1" } }),
|
db.select({ total: count() }).from(CatalogItemsBc),
|
||||||
]);
|
db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(CatalogPagesBc)
|
||||||
|
.where(eq(CatalogPagesBc.enabled, "1")),
|
||||||
|
]);
|
||||||
|
const totalBcPages = Number(totalBcPagesRows[0]?.total ?? 0);
|
||||||
|
const totalBcItems = Number(totalBcItemsRows[0]?.total ?? 0);
|
||||||
|
const enabledBcPages = Number(enabledBcPagesRows[0]?.total ?? 0);
|
||||||
|
|
||||||
// ── BC tree view ──────────────────────────────────────────────
|
|
||||||
let bcRootPages: TreeNodeShape[] = [];
|
let bcRootPages: TreeNodeShape[] = [];
|
||||||
if (viewMode === "tree") {
|
if (viewMode === "tree") {
|
||||||
const bcRootRaw = await prisma.catalogPagesBc.findMany({
|
const bcRootRaw = await db
|
||||||
where: { OR: [{ parentId: -1 }, { parentId: 0 }] },
|
.select()
|
||||||
orderBy: { orderNum: "asc" },
|
.from(CatalogPagesBc)
|
||||||
});
|
.where(
|
||||||
|
or(eq(CatalogPagesBc.parentId, -1), eq(CatalogPagesBc.parentId, 0)),
|
||||||
|
)
|
||||||
|
.orderBy(asc(CatalogPagesBc.orderNum));
|
||||||
const bcRootIds = bcRootRaw.map((p) => p.id);
|
const bcRootIds = bcRootRaw.map((p) => p.id);
|
||||||
const [bcChildCounts, bcItemCounts] = await Promise.all([
|
const [bcChildCounts, bcItemCounts] = await Promise.all([
|
||||||
bcRootIds.length > 0
|
bcRootIds.length > 0
|
||||||
? prisma.catalogPagesBc.groupBy({
|
? db
|
||||||
by: ["parentId"],
|
.select({
|
||||||
where: { parentId: { in: bcRootIds } },
|
parentId: CatalogPagesBc.parentId,
|
||||||
_count: true,
|
total: count(),
|
||||||
})
|
})
|
||||||
|
.from(CatalogPagesBc)
|
||||||
|
.where(inArray(CatalogPagesBc.parentId, bcRootIds))
|
||||||
|
.groupBy(CatalogPagesBc.parentId)
|
||||||
: [],
|
: [],
|
||||||
bcRootIds.length > 0
|
bcRootIds.length > 0
|
||||||
? prisma.catalogItemsBc.groupBy({
|
? db
|
||||||
by: ["pageId"],
|
.select({
|
||||||
where: { pageId: { in: bcRootIds } },
|
pageId: CatalogItemsBc.pageId,
|
||||||
_count: true,
|
total: count(),
|
||||||
})
|
})
|
||||||
|
.from(CatalogItemsBc)
|
||||||
|
.where(inArray(CatalogItemsBc.pageId, bcRootIds))
|
||||||
|
.groupBy(CatalogItemsBc.pageId)
|
||||||
: [],
|
: [],
|
||||||
]);
|
]);
|
||||||
const bcChildMap = Object.fromEntries(
|
const bcChildMap = Object.fromEntries(
|
||||||
bcChildCounts.map((c) => [c.parentId, c._count]),
|
bcChildCounts.map((c) => [c.parentId, Number(c.total)]),
|
||||||
);
|
);
|
||||||
const bcItemMap = Object.fromEntries(
|
const bcItemMap = Object.fromEntries(
|
||||||
bcItemCounts.map((c) => [c.pageId, c._count]),
|
bcItemCounts.map((c) => [c.pageId, Number(c.total)]),
|
||||||
);
|
);
|
||||||
bcRootPages = bcRootRaw.map((p) => ({
|
bcRootPages = bcRootRaw.map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
@@ -98,7 +137,6 @@ export default async function CatalogPagesPage({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── BC table view ─────────────────────────────────────────────
|
|
||||||
let bcRows: {
|
let bcRows: {
|
||||||
id: number;
|
id: number;
|
||||||
caption: string;
|
caption: string;
|
||||||
@@ -119,32 +157,38 @@ export default async function CatalogPagesPage({
|
|||||||
const parentId =
|
const parentId =
|
||||||
parentFilter !== null ? parseInt(parentFilter, 10) : undefined;
|
parentFilter !== null ? parseInt(parentFilter, 10) : undefined;
|
||||||
|
|
||||||
const where: Record<string, unknown> = {};
|
const conditions: SQL[] = [];
|
||||||
const hasParent = parentId !== undefined && !Number.isNaN(parentId);
|
const hasParent = parentId !== undefined && !Number.isNaN(parentId);
|
||||||
if (search) {
|
if (search) {
|
||||||
const pattern = { contains: search };
|
conditions.push(like(CatalogPagesBc.caption, `%${search}%`));
|
||||||
where.OR = [{ caption: pattern }];
|
|
||||||
}
|
}
|
||||||
if (hasParent) where.parentId = parentId;
|
if (hasParent) conditions.push(eq(CatalogPagesBc.parentId, parentId));
|
||||||
|
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
|
|
||||||
// Prisma sort key is snake_case for BC model. Translate known keys.
|
const sortKeyMap: Record<string, AnyMySqlColumn> = {
|
||||||
const sortKeyMap: Record<string, string> = {
|
parentId: CatalogPagesBc.parentId,
|
||||||
parentId: "parentId",
|
pageLayout: CatalogPagesBc.pageLayout,
|
||||||
pageLayout: "pageLayout",
|
orderNum: CatalogPagesBc.orderNum,
|
||||||
orderNum: "orderNum",
|
caption: CatalogPagesBc.caption,
|
||||||
|
id: CatalogPagesBc.id,
|
||||||
};
|
};
|
||||||
const sortKey = sort ? (sortKeyMap[sort] ?? sort) : "orderNum";
|
const sortCol = sort
|
||||||
const orderBy = { [sortKey]: sort ? order : "asc" };
|
? (sortKeyMap[sort] ?? CatalogPagesBc.orderNum)
|
||||||
|
: CatalogPagesBc.orderNum;
|
||||||
|
const orderBy =
|
||||||
|
(sort ? order : "asc") === "desc" ? desc(sortCol) : asc(sortCol);
|
||||||
|
|
||||||
const [pages, total] = await Promise.all([
|
const [pages, totalRows] = await Promise.all([
|
||||||
prisma.catalogPagesBc.findMany({
|
db
|
||||||
where,
|
.select()
|
||||||
orderBy,
|
.from(CatalogPagesBc)
|
||||||
skip: (page - 1) * perPage,
|
.where(where)
|
||||||
take: perPage,
|
.orderBy(orderBy)
|
||||||
}),
|
.limit(perPage)
|
||||||
prisma.catalogPagesBc.count({ where }),
|
.offset((page - 1) * perPage),
|
||||||
|
db.select({ total: count() }).from(CatalogPagesBc).where(where),
|
||||||
]);
|
]);
|
||||||
|
const total = Number(totalRows[0]?.total ?? 0);
|
||||||
|
|
||||||
bcPagination = calcPagination(total, page, perPage);
|
bcPagination = calcPagination(total, page, perPage);
|
||||||
|
|
||||||
@@ -154,31 +198,40 @@ export default async function CatalogPagesPage({
|
|||||||
];
|
];
|
||||||
const [childCounts, itemCounts, parentPages] = await Promise.all([
|
const [childCounts, itemCounts, parentPages] = await Promise.all([
|
||||||
pageIds.length > 0
|
pageIds.length > 0
|
||||||
? prisma.catalogPagesBc.groupBy({
|
? db
|
||||||
by: ["parentId"],
|
.select({
|
||||||
where: { parentId: { in: pageIds } },
|
parentId: CatalogPagesBc.parentId,
|
||||||
_count: true,
|
total: count(),
|
||||||
})
|
})
|
||||||
|
.from(CatalogPagesBc)
|
||||||
|
.where(inArray(CatalogPagesBc.parentId, pageIds))
|
||||||
|
.groupBy(CatalogPagesBc.parentId)
|
||||||
: [],
|
: [],
|
||||||
pageIds.length > 0
|
pageIds.length > 0
|
||||||
? prisma.catalogItemsBc.groupBy({
|
? db
|
||||||
by: ["pageId"],
|
.select({
|
||||||
where: { pageId: { in: pageIds } },
|
pageId: CatalogItemsBc.pageId,
|
||||||
_count: true,
|
total: count(),
|
||||||
})
|
})
|
||||||
|
.from(CatalogItemsBc)
|
||||||
|
.where(inArray(CatalogItemsBc.pageId, pageIds))
|
||||||
|
.groupBy(CatalogItemsBc.pageId)
|
||||||
: [],
|
: [],
|
||||||
parentIds.length > 0
|
parentIds.length > 0
|
||||||
? prisma.catalogPagesBc.findMany({
|
? db
|
||||||
where: { id: { in: parentIds } },
|
.select({
|
||||||
select: { id: true, caption: true },
|
id: CatalogPagesBc.id,
|
||||||
})
|
caption: CatalogPagesBc.caption,
|
||||||
|
})
|
||||||
|
.from(CatalogPagesBc)
|
||||||
|
.where(inArray(CatalogPagesBc.id, parentIds))
|
||||||
: [],
|
: [],
|
||||||
]);
|
]);
|
||||||
const childCountMap = Object.fromEntries(
|
const childCountMap = Object.fromEntries(
|
||||||
childCounts.map((c) => [c.parentId, c._count]),
|
childCounts.map((c) => [c.parentId, Number(c.total)]),
|
||||||
);
|
);
|
||||||
const itemCountMap = Object.fromEntries(
|
const itemCountMap = Object.fromEntries(
|
||||||
itemCounts.map((c) => [c.pageId, c._count]),
|
itemCounts.map((c) => [c.pageId, Number(c.total)]),
|
||||||
);
|
);
|
||||||
const parentNameMap = Object.fromEntries(
|
const parentNameMap = Object.fromEntries(
|
||||||
parentPages.map((p) => [p.id, p.caption]),
|
parentPages.map((p) => [p.id, p.caption]),
|
||||||
@@ -195,7 +248,7 @@ export default async function CatalogPagesPage({
|
|||||||
pageLayout: String(p.pageLayout),
|
pageLayout: String(p.pageLayout),
|
||||||
enabled: String(p.enabled),
|
enabled: String(p.enabled),
|
||||||
visible: String(p.visible),
|
visible: String(p.visible),
|
||||||
minRank: 0, // BC pages have no min rank column
|
minRank: 0,
|
||||||
orderNum: p.orderNum,
|
orderNum: p.orderNum,
|
||||||
itemCount: itemCountMap[p.id] ?? 0,
|
itemCount: itemCountMap[p.id] ?? 0,
|
||||||
childCount: childCountMap[p.id] ?? 0,
|
childCount: childCountMap[p.id] ?? 0,
|
||||||
@@ -252,33 +305,39 @@ export default async function CatalogPagesPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Normal catalog mode ────────────────────────────────────────
|
const [totalPagesRows, totalItemsRows, enabledPagesRows] = await Promise.all([
|
||||||
|
db.select({ total: count() }).from(CatalogPages),
|
||||||
// Global stats (lightweight counts)
|
db.select({ total: count() }).from(CatalogItems),
|
||||||
const [totalPages, totalItems, enabledPages] = await Promise.all([
|
db
|
||||||
prisma.catalogPages.count(),
|
.select({ total: count() })
|
||||||
prisma.catalogItems.count(),
|
.from(CatalogPages)
|
||||||
prisma.catalogPages.count({ where: { enabled: "1" } }),
|
.where(eq(CatalogPages.enabled, "1")),
|
||||||
]);
|
]);
|
||||||
|
const totalPages = Number(totalPagesRows[0]?.total ?? 0);
|
||||||
|
const totalItems = Number(totalItemsRows[0]?.total ?? 0);
|
||||||
|
const enabledPages = Number(enabledPagesRows[0]?.total ?? 0);
|
||||||
|
|
||||||
// ── Root pages (tree view + Visual Manager seed) ────────────────
|
const rootPagesRaw = await db
|
||||||
const rootPagesRaw = await prisma.catalogPages.findMany({
|
.select()
|
||||||
where: { OR: [{ parentId: -1 }, { parentId: 0 }] },
|
.from(CatalogPages)
|
||||||
orderBy: { orderNum: "asc" },
|
.where(or(eq(CatalogPages.parentId, -1), eq(CatalogPages.parentId, 0)))
|
||||||
});
|
.orderBy(asc(CatalogPages.orderNum));
|
||||||
const rootIds = rootPagesRaw.map((p) => p.id);
|
const rootIds = rootPagesRaw.map((p) => p.id);
|
||||||
const [rootChildCounts, rootItemCountMap] = await Promise.all([
|
const [rootChildCounts, rootItemCountMap] = await Promise.all([
|
||||||
rootIds.length > 0
|
rootIds.length > 0
|
||||||
? prisma.catalogPages.groupBy({
|
? db
|
||||||
by: ["parentId"],
|
.select({
|
||||||
where: { parentId: { in: rootIds } },
|
parentId: CatalogPages.parentId,
|
||||||
_count: true,
|
total: count(),
|
||||||
})
|
})
|
||||||
|
.from(CatalogPages)
|
||||||
|
.where(inArray(CatalogPages.parentId, rootIds))
|
||||||
|
.groupBy(CatalogPages.parentId)
|
||||||
: [],
|
: [],
|
||||||
getCatalogItemCounts(rootIds),
|
getCatalogItemCounts(rootIds),
|
||||||
]);
|
]);
|
||||||
const rootChildMap = Object.fromEntries(
|
const rootChildMap = Object.fromEntries(
|
||||||
rootChildCounts.map((c) => [c.parentId, c._count]),
|
rootChildCounts.map((c) => [c.parentId, Number(c.total)]),
|
||||||
);
|
);
|
||||||
const rootItemMap = Object.fromEntries(rootItemCountMap);
|
const rootItemMap = Object.fromEntries(rootItemCountMap);
|
||||||
const rootPages: TreeNodeShape[] = rootPagesRaw.map((p) => ({
|
const rootPages: TreeNodeShape[] = rootPagesRaw.map((p) => ({
|
||||||
@@ -294,7 +353,6 @@ export default async function CatalogPagesPage({
|
|||||||
itemCount: rootItemMap[p.id] ?? 0,
|
itemCount: rootItemMap[p.id] ?? 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ── Table view data (skip if tree) ────────────────────────────
|
|
||||||
let rows: {
|
let rows: {
|
||||||
id: number;
|
id: number;
|
||||||
caption: string;
|
caption: string;
|
||||||
@@ -315,15 +373,15 @@ export default async function CatalogPagesPage({
|
|||||||
const parentId =
|
const parentId =
|
||||||
parentFilter !== null ? parseInt(parentFilter, 10) : undefined;
|
parentFilter !== null ? parseInt(parentFilter, 10) : undefined;
|
||||||
|
|
||||||
const where: Record<string, unknown> = {};
|
const conditions: SQL[] = [];
|
||||||
const hasParent = parentId !== undefined && !Number.isNaN(parentId);
|
const hasParent = parentId !== undefined && !Number.isNaN(parentId);
|
||||||
if (search) {
|
if (search) {
|
||||||
const searchPattern = `%${search}%`;
|
const searchPattern = `%${search}%`;
|
||||||
const parentClause = hasParent
|
const parentClause = hasParent
|
||||||
? Prisma.sql`AND cp.parent_id = ${parentId}`
|
? sql`AND cp.parent_id = ${parentId}`
|
||||||
: Prisma.empty;
|
: sql``;
|
||||||
|
|
||||||
const matchedIds = await prisma.$queryRaw<{ id: number }[]>(Prisma.sql`
|
const matchedIds = await rawRows<{ id: number }>(sql`
|
||||||
SELECT DISTINCT cp.id FROM catalog_pages cp
|
SELECT DISTINCT cp.id FROM catalog_pages cp
|
||||||
LEFT JOIN catalog_items ci ON CAST(ci.page_id AS CHAR) = CAST(cp.id AS CHAR)
|
LEFT JOIN catalog_items ci ON CAST(ci.page_id AS CHAR) = CAST(cp.id AS CHAR)
|
||||||
LEFT JOIN items_base ib ON FIND_IN_SET(ib.id, REPLACE(ci.item_ids, ';', ','))
|
LEFT JOIN items_base ib ON FIND_IN_SET(ib.id, REPLACE(ci.item_ids, ';', ','))
|
||||||
@@ -334,48 +392,68 @@ export default async function CatalogPagesPage({
|
|||||||
OR ib.item_name LIKE ${searchPattern})
|
OR ib.item_name LIKE ${searchPattern})
|
||||||
${parentClause}
|
${parentClause}
|
||||||
`);
|
`);
|
||||||
where.id = { in: matchedIds.map((r) => r.id) };
|
const ids = matchedIds.map((r) => r.id);
|
||||||
|
if (ids.length > 0) conditions.push(inArray(CatalogPages.id, ids));
|
||||||
|
else conditions.push(sql`1 = 0`);
|
||||||
} else if (hasParent) {
|
} else if (hasParent) {
|
||||||
where.parentId = parentId;
|
conditions.push(eq(CatalogPages.parentId, parentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderBy = sort ? { [sort]: order } : { orderNum: "asc" as const };
|
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
|
const sortColMap: Record<string, AnyMySqlColumn> = {
|
||||||
|
caption: CatalogPages.caption,
|
||||||
|
parentId: CatalogPages.parentId,
|
||||||
|
pageLayout: CatalogPages.pageLayout,
|
||||||
|
enabled: CatalogPages.enabled,
|
||||||
|
orderNum: CatalogPages.orderNum,
|
||||||
|
id: CatalogPages.id,
|
||||||
|
minRank: CatalogPages.minRank,
|
||||||
|
};
|
||||||
|
const sortCol = sort
|
||||||
|
? (sortColMap[sort] ?? CatalogPages.orderNum)
|
||||||
|
: CatalogPages.orderNum;
|
||||||
|
const orderBy =
|
||||||
|
(sort ? order : "asc") === "desc" ? desc(sortCol) : asc(sortCol);
|
||||||
|
|
||||||
const [pages, total] = await Promise.all([
|
const [pages, totalRows] = await Promise.all([
|
||||||
prisma.catalogPages.findMany({
|
db
|
||||||
where,
|
.select()
|
||||||
orderBy,
|
.from(CatalogPages)
|
||||||
skip: (page - 1) * perPage,
|
.where(where)
|
||||||
take: perPage,
|
.orderBy(orderBy)
|
||||||
}),
|
.limit(perPage)
|
||||||
prisma.catalogPages.count({ where }),
|
.offset((page - 1) * perPage),
|
||||||
|
db.select({ total: count() }).from(CatalogPages).where(where),
|
||||||
]);
|
]);
|
||||||
|
const total = Number(totalRows[0]?.total ?? 0);
|
||||||
|
|
||||||
pagination = calcPagination(total, page, perPage);
|
pagination = calcPagination(total, page, perPage);
|
||||||
|
|
||||||
// Count children and items per page, and resolve parent names in parallel
|
|
||||||
const pageIds = pages.map((p) => p.id);
|
const pageIds = pages.map((p) => p.id);
|
||||||
const parentIds = [
|
const parentIds = [
|
||||||
...new Set(pages.map((p) => p.parentId).filter((id) => id > 0)),
|
...new Set(pages.map((p) => p.parentId).filter((id) => id > 0)),
|
||||||
];
|
];
|
||||||
const [childCounts, itemCountMapRaw, parentPages] = await Promise.all([
|
const [childCounts, itemCountMapRaw, parentPages] = await Promise.all([
|
||||||
pageIds.length > 0
|
pageIds.length > 0
|
||||||
? prisma.catalogPages.groupBy({
|
? db
|
||||||
by: ["parentId"],
|
.select({
|
||||||
where: { parentId: { in: pageIds } },
|
parentId: CatalogPages.parentId,
|
||||||
_count: true,
|
total: count(),
|
||||||
})
|
})
|
||||||
|
.from(CatalogPages)
|
||||||
|
.where(inArray(CatalogPages.parentId, pageIds))
|
||||||
|
.groupBy(CatalogPages.parentId)
|
||||||
: [],
|
: [],
|
||||||
getCatalogItemCounts(pageIds),
|
getCatalogItemCounts(pageIds),
|
||||||
parentIds.length > 0
|
parentIds.length > 0
|
||||||
? prisma.catalogPages.findMany({
|
? db
|
||||||
where: { id: { in: parentIds } },
|
.select({ id: CatalogPages.id, caption: CatalogPages.caption })
|
||||||
select: { id: true, caption: true },
|
.from(CatalogPages)
|
||||||
})
|
.where(inArray(CatalogPages.id, parentIds))
|
||||||
: [],
|
: [],
|
||||||
]);
|
]);
|
||||||
const childCountMap = Object.fromEntries(
|
const childCountMap = Object.fromEntries(
|
||||||
childCounts.map((c) => [c.parentId, c._count]),
|
childCounts.map((c) => [c.parentId, Number(c.total)]),
|
||||||
);
|
);
|
||||||
const itemCountMap = Object.fromEntries(itemCountMapRaw);
|
const itemCountMap = Object.fromEntries(itemCountMapRaw);
|
||||||
const parentNameMap = Object.fromEntries(
|
const parentNameMap = Object.fromEntries(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
import { count, desc, inArray } from "drizzle-orm";
|
||||||
import { Terminal } from "lucide-react";
|
import { Terminal } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
@@ -11,9 +12,9 @@ import {
|
|||||||
} from "@/components/admin/dashboard";
|
} from "@/components/admin/dashboard";
|
||||||
import { fetchOpsHealth } from "@/lib/admin/ops-health";
|
import { fetchOpsHealth } from "@/lib/admin/ops-health";
|
||||||
import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users";
|
import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users";
|
||||||
|
import { db, EmulatorErrors, StaffActivities, User } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
import { CommandocentrumControls } from "./commandocentrum-controls";
|
import { CommandocentrumControls } from "./commandocentrum-controls";
|
||||||
|
|
||||||
@@ -46,22 +47,29 @@ export default async function CommandoCentrum() {
|
|||||||
|
|
||||||
const t = await getTranslations("pages.admin.commandocentrum");
|
const t = await getTranslations("pages.admin.commandocentrum");
|
||||||
|
|
||||||
const [totalUsers, errors, health, maintenance, activity, onlineRoster] =
|
const [totalUsersRows, errors, health, maintenance, activity, onlineRoster] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
prisma.user.count().catch(() => -1),
|
db
|
||||||
prisma.emulatorErrors
|
.select({ total: count() })
|
||||||
.findMany({ orderBy: { timestamp: "desc" }, take: 50 })
|
.from(User)
|
||||||
|
.catch(() => [{ total: -1 }]),
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(EmulatorErrors)
|
||||||
|
.orderBy(desc(EmulatorErrors.timestamp))
|
||||||
|
.limit(50)
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
fetchOpsHealth(),
|
fetchOpsHealth(),
|
||||||
siteSettings.getBool("maintenance_enabled", false),
|
siteSettings.getBool("maintenance_enabled", false),
|
||||||
prisma.staffActivities
|
db
|
||||||
.findMany({ orderBy: { createdAt: "desc" }, take: 12 })
|
.select()
|
||||||
.catch(
|
.from(StaffActivities)
|
||||||
() =>
|
.orderBy(desc(StaffActivities.createdAt))
|
||||||
[] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
|
.limit(12)
|
||||||
),
|
.catch(() => [] as (typeof StaffActivities.$inferSelect)[]),
|
||||||
fetchOpsOnlineUsers(40),
|
fetchOpsOnlineUsers(40),
|
||||||
]);
|
]);
|
||||||
|
const totalUsers = Number(totalUsersRows[0]?.total ?? -1);
|
||||||
|
|
||||||
const dbOk = health.dbOk;
|
const dbOk = health.dbOk;
|
||||||
const redisOk = health.redisOk;
|
const redisOk = health.redisOk;
|
||||||
@@ -74,11 +82,10 @@ export default async function CommandoCentrum() {
|
|||||||
).filter((n) => n > 0);
|
).filter((n) => n > 0);
|
||||||
let nameById = new Map<number, string>();
|
let nameById = new Map<number, string>();
|
||||||
if (staffIds.length) {
|
if (staffIds.length) {
|
||||||
const users = await prisma.user
|
const users = await db
|
||||||
.findMany({
|
.select({ id: User.id, username: User.username })
|
||||||
where: { id: { in: staffIds } },
|
.from(User)
|
||||||
select: { id: true, username: true },
|
.where(inArray(User.id, staffIds))
|
||||||
})
|
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
nameById = new Map(users.map((u) => [u.id, u.username]));
|
nameById = new Map(users.map((u) => [u.id, u.username]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { count, desc } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { db, EmulatorErrors } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export default async function EmulatorErrorsPage({
|
export default async function EmulatorErrorsPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -19,15 +20,17 @@ export default async function EmulatorErrorsPage({
|
|||||||
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, totalRows] = await Promise.all([
|
||||||
prisma.emulatorErrors.findMany({
|
db
|
||||||
orderBy: { id: "desc" },
|
.select()
|
||||||
skip: (page - 1) * perPage,
|
.from(EmulatorErrors)
|
||||||
take: perPage,
|
.orderBy(desc(EmulatorErrors.id))
|
||||||
}),
|
.limit(perPage)
|
||||||
prisma.emulatorErrors.count(),
|
.offset((page - 1) * perPage),
|
||||||
|
db.select({ total: count() }).from(EmulatorErrors),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const total = Number(totalRows[0]?.total ?? 0);
|
||||||
const lastPage = Math.ceil(total / perPage);
|
const lastPage = Math.ceil(total / perPage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { count, desc, eq, gte } from "drizzle-orm";
|
||||||
import { AlertTriangle, Database, HeartPulse, Server } from "lucide-react";
|
import { AlertTriangle, Database, HeartPulse, Server } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
@@ -5,35 +6,40 @@ import { getTranslations } from "next-intl/server";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { fetchOpsHealth } from "@/lib/admin/ops-health";
|
import { fetchOpsHealth } from "@/lib/admin/ops-health";
|
||||||
|
import { db, EmulatorErrors, EmulatorSettings } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { HealthCheckClient } from "./health-check-client";
|
import { HealthCheckClient } from "./health-check-client";
|
||||||
|
|
||||||
async function getDevOpsData() {
|
async function getDevOpsData() {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const dayAgo = now - 86400;
|
const dayAgo = now - 86400;
|
||||||
|
|
||||||
const [health, recentErrors, errorsToday, emuVersion] = await Promise.all([
|
const [health, recentErrors, errorsTodayRows, emuVersionRows] =
|
||||||
fetchOpsHealth(),
|
await Promise.all([
|
||||||
prisma.emulatorErrors.findMany({
|
fetchOpsHealth(),
|
||||||
orderBy: { id: "desc" },
|
db
|
||||||
take: 20,
|
.select()
|
||||||
}),
|
.from(EmulatorErrors)
|
||||||
prisma.emulatorErrors.count({
|
.orderBy(desc(EmulatorErrors.id))
|
||||||
where: { timestamp: { gte: dayAgo } },
|
.limit(20),
|
||||||
}),
|
db
|
||||||
prisma.emulatorSettings.findFirst({
|
.select({ total: count() })
|
||||||
where: { key: "version" },
|
.from(EmulatorErrors)
|
||||||
}),
|
.where(gte(EmulatorErrors.timestamp, dayAgo)),
|
||||||
]);
|
db
|
||||||
|
.select({ value: EmulatorSettings.value })
|
||||||
|
.from(EmulatorSettings)
|
||||||
|
.where(eq(EmulatorSettings.key, "version"))
|
||||||
|
.limit(1),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dbStatus: health.dbOk ? ("healthy" as const) : ("error" as const),
|
dbStatus: health.dbOk ? ("healthy" as const) : ("error" as const),
|
||||||
dbLatency: health.dbLatencyMs,
|
dbLatency: health.dbLatencyMs,
|
||||||
redisOk: health.redisOk,
|
redisOk: health.redisOk,
|
||||||
recentErrors,
|
recentErrors,
|
||||||
errorsToday,
|
errorsToday: Number(errorsTodayRows[0]?.total ?? 0),
|
||||||
emuVersion: emuVersion?.value ?? "Unknown",
|
emuVersion: emuVersionRows[0]?.value ?? "Unknown",
|
||||||
onlineUsers: health.onlineUsers,
|
onlineUsers: health.onlineUsers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { asc } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import {
|
import {
|
||||||
@@ -6,8 +7,8 @@ import {
|
|||||||
updateEmailTemplate,
|
updateEmailTemplate,
|
||||||
} from "@/actions/admin-email-templates";
|
} from "@/actions/admin-email-templates";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { db, EmailTemplates } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export default async function AdminEmailTemplates() {
|
export default async function AdminEmailTemplates() {
|
||||||
const { session, permissions } = await getAdminContext();
|
const { session, permissions } = await getAdminContext();
|
||||||
@@ -17,12 +18,12 @@ export default async function AdminEmailTemplates() {
|
|||||||
|
|
||||||
const t = await getTranslations("pages.admin.emailTemplates");
|
const t = await getTranslations("pages.admin.emailTemplates");
|
||||||
|
|
||||||
let templates: Awaited<ReturnType<typeof prisma.emailTemplates.findMany>> =
|
let templates: (typeof EmailTemplates.$inferSelect)[] = [];
|
||||||
[];
|
|
||||||
try {
|
try {
|
||||||
templates = await prisma.emailTemplates.findMany({
|
templates = await db
|
||||||
orderBy: { name: "asc" },
|
.select()
|
||||||
});
|
.from(EmailTemplates)
|
||||||
|
.orderBy(asc(EmailTemplates.name));
|
||||||
} catch {
|
} catch {
|
||||||
templates = [];
|
templates = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { asc, count } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import {
|
import {
|
||||||
@@ -6,8 +7,8 @@ import {
|
|||||||
} from "@/actions/admin-emulator";
|
} from "@/actions/admin-emulator";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { db, EmulatorSettings, EmulatorTexts } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
const TEXTS_LIMIT = 300;
|
const TEXTS_LIMIT = 300;
|
||||||
|
|
||||||
@@ -21,9 +22,10 @@ export default async function AdminEmulator() {
|
|||||||
|
|
||||||
let settings: { key: string; value: string }[] = [];
|
let settings: { key: string; value: string }[] = [];
|
||||||
try {
|
try {
|
||||||
settings = await prisma.emulatorSettings.findMany({
|
settings = await db
|
||||||
orderBy: { key: "asc" },
|
.select({ key: EmulatorSettings.key, value: EmulatorSettings.value })
|
||||||
});
|
.from(EmulatorSettings)
|
||||||
|
.orderBy(asc(EmulatorSettings.key));
|
||||||
} catch {
|
} catch {
|
||||||
settings = [];
|
settings = [];
|
||||||
}
|
}
|
||||||
@@ -31,15 +33,17 @@ export default async function AdminEmulator() {
|
|||||||
let texts: { key: string; value: string }[] = [];
|
let texts: { key: string; value: string }[] = [];
|
||||||
let textsCount = 0;
|
let textsCount = 0;
|
||||||
try {
|
try {
|
||||||
textsCount = await prisma.emulatorTexts.count();
|
const [row] = await db.select({ total: count() }).from(EmulatorTexts);
|
||||||
|
textsCount = Number(row?.total ?? 0);
|
||||||
} catch {
|
} catch {
|
||||||
textsCount = 0;
|
textsCount = 0;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
texts = await prisma.emulatorTexts.findMany({
|
texts = await db
|
||||||
orderBy: { key: "asc" },
|
.select({ key: EmulatorTexts.key, value: EmulatorTexts.value })
|
||||||
take: TEXTS_LIMIT,
|
.from(EmulatorTexts)
|
||||||
});
|
.orderBy(asc(EmulatorTexts.key))
|
||||||
|
.limit(TEXTS_LIMIT);
|
||||||
} catch {
|
} catch {
|
||||||
texts = [];
|
texts = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { asc, eq, inArray } from "drizzle-orm";
|
||||||
import { CalendarDays, Gift, Trophy, Users } from "lucide-react";
|
import { CalendarDays, Gift, Trophy, Users } from "lucide-react";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { db, User, WebsiteEvent, WebsiteEventType } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { EventForm } from "../event-form";
|
import { EventForm } from "../event-form";
|
||||||
import { EventPrizesManager } from "./event-prizes";
|
import { EventPrizesManager } from "./event-prizes";
|
||||||
import { EventRegistrations } from "./event-registrations";
|
import { EventRegistrations } from "./event-registrations";
|
||||||
@@ -28,22 +29,23 @@ export default async function EventDetailPage({
|
|||||||
const t = await getTranslations("pages.admin.events");
|
const t = await getTranslations("pages.admin.events");
|
||||||
const tHubs = await getTranslations("pages.admin.hubs");
|
const tHubs = await getTranslations("pages.admin.hubs");
|
||||||
|
|
||||||
const event = await prisma.websiteEvent.findUnique({
|
const event = await db.query.WebsiteEvent.findFirst({
|
||||||
where: { id: eventId },
|
where: eq(WebsiteEvent.id, eventId),
|
||||||
include: {
|
with: {
|
||||||
type: true,
|
type: true,
|
||||||
prizes: { orderBy: { position: "asc" } },
|
prizes: { orderBy: (p, { asc: a }) => [a(p.position)] },
|
||||||
winners: { orderBy: { position: "asc" } },
|
winners: { orderBy: (w, { asc: a }) => [a(w.position)] },
|
||||||
registrations: { orderBy: { createdAt: "desc" } },
|
registrations: { orderBy: (r, { desc: d }) => [d(r.createdAt)] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!event) notFound();
|
if (!event) notFound();
|
||||||
|
|
||||||
const eventTypes = await prisma.websiteEventType.findMany({
|
const eventTypes = await db
|
||||||
where: { isActive: 1 },
|
.select()
|
||||||
orderBy: { name: "asc" },
|
.from(WebsiteEventType)
|
||||||
});
|
.where(eq(WebsiteEventType.isActive, 1))
|
||||||
|
.orderBy(asc(WebsiteEventType.name));
|
||||||
|
|
||||||
// Fetch usernames for registrations and winners
|
// Fetch usernames for registrations and winners
|
||||||
const userIds = [
|
const userIds = [
|
||||||
@@ -52,10 +54,10 @@ export default async function EventDetailPage({
|
|||||||
];
|
];
|
||||||
const users =
|
const users =
|
||||||
userIds.length > 0
|
userIds.length > 0
|
||||||
? await prisma.user.findMany({
|
? await db
|
||||||
where: { id: { in: [...new Set(userIds)] } },
|
.select({ id: User.id, username: User.username })
|
||||||
select: { id: true, username: true },
|
.from(User)
|
||||||
})
|
.where(inArray(User.id, [...new Set(userIds)]))
|
||||||
: [];
|
: [];
|
||||||
const userMap = new Map(users.map((u) => [u.id, u.username]));
|
const userMap = new Map(users.map((u) => [u.id, u.username]));
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { db, WebsiteEventType } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
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() {
|
||||||
@@ -11,10 +12,11 @@ export default async function CreateEventPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const t = await getTranslations("pages.admin.events");
|
const t = await getTranslations("pages.admin.events");
|
||||||
const eventTypes = await prisma.websiteEventType.findMany({
|
const eventTypes = await db
|
||||||
where: { isActive: 1 },
|
.select()
|
||||||
orderBy: { name: "asc" },
|
.from(WebsiteEventType)
|
||||||
});
|
.where(eq(WebsiteEventType.isActive, 1))
|
||||||
|
.orderBy(asc(WebsiteEventType.name));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { asc } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { db, WebsiteEventType } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
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() {
|
||||||
@@ -9,9 +10,10 @@ export default async function EventTypesPage() {
|
|||||||
redirect("/admin/events");
|
redirect("/admin/events");
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventTypes = await prisma.websiteEventType.findMany({
|
const eventTypes = await db
|
||||||
orderBy: { name: "asc" },
|
.select()
|
||||||
});
|
.from(WebsiteEventType)
|
||||||
|
.orderBy(asc(WebsiteEventType.name));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { db, WebsiteSetting } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { FaviconForm } from "./favicon-form";
|
import { FaviconForm } from "./favicon-form";
|
||||||
|
|
||||||
export default async function AdminFaviconPage() {
|
export default async function AdminFaviconPage() {
|
||||||
@@ -11,9 +12,11 @@ export default async function AdminFaviconPage() {
|
|||||||
|
|
||||||
let faviconUrl: string | null = null;
|
let faviconUrl: string | null = null;
|
||||||
try {
|
try {
|
||||||
const setting = await prisma.websiteSetting.findUnique({
|
const [setting] = await db
|
||||||
where: { key: "cms_favicon" },
|
.select({ value: WebsiteSetting.value })
|
||||||
});
|
.from(WebsiteSetting)
|
||||||
|
.where(eq(WebsiteSetting.key, "cms_favicon"))
|
||||||
|
.limit(1);
|
||||||
faviconUrl = setting?.value ?? null;
|
faviconUrl = setting?.value ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
faviconUrl = null;
|
faviconUrl = null;
|
||||||
|
|||||||
Reference in new issue
Block a user