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 { getTranslations } from "next-intl/server";
|
||||
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";
|
||||
|
||||
export const revalidate = 300;
|
||||
@@ -19,10 +20,18 @@ export default async function RareValuesPage() {
|
||||
const t = await getTranslations("pages.rares");
|
||||
let categories: CategoryRow[] = [];
|
||||
try {
|
||||
categories = await prisma.websiteRareValueCategories.findMany({
|
||||
orderBy: [{ priority: "asc" }, { name: "asc" }],
|
||||
select: { id: true, name: true, badge: true, priority: true },
|
||||
});
|
||||
categories = await db
|
||||
.select({
|
||||
id: WebsiteRareValueCategories.id,
|
||||
name: WebsiteRareValueCategories.name,
|
||||
badge: WebsiteRareValueCategories.badge,
|
||||
priority: WebsiteRareValueCategories.priority,
|
||||
})
|
||||
.from(WebsiteRareValueCategories)
|
||||
.orderBy(
|
||||
asc(WebsiteRareValueCategories.priority),
|
||||
asc(WebsiteRareValueCategories.name),
|
||||
);
|
||||
} catch {
|
||||
categories = [];
|
||||
}
|
||||
@@ -30,12 +39,15 @@ export default async function RareValuesPage() {
|
||||
// Per-category rare counts (grouped so we avoid N queries).
|
||||
const counts = new Map<string, number>();
|
||||
try {
|
||||
const grouped = await prisma.websiteRareValues.groupBy({
|
||||
by: ["categoryId"],
|
||||
_count: { _all: true },
|
||||
});
|
||||
const grouped = await db
|
||||
.select({
|
||||
categoryId: WebsiteRareValues.categoryId,
|
||||
value: count(),
|
||||
})
|
||||
.from(WebsiteRareValues)
|
||||
.groupBy(WebsiteRareValues.categoryId);
|
||||
for (const g of grouped) {
|
||||
counts.set(String(g.categoryId), g._count._all);
|
||||
counts.set(String(g.categoryId), Number(g.value));
|
||||
}
|
||||
} catch {
|
||||
// leave counts empty -> shown as 0
|
||||
@@ -55,7 +67,7 @@ export default async function RareValuesPage() {
|
||||
<div className="card-grid sm-2 lg-3">
|
||||
{categories.map((c) => {
|
||||
const id = String(c.id);
|
||||
const count = counts.get(id) ?? 0;
|
||||
const rareCount = counts.get(id) ?? 0;
|
||||
const badgeSrc =
|
||||
badgesBase && c.badge ? `${badgesBase}/${c.badge}.gif` : "";
|
||||
return (
|
||||
@@ -83,7 +95,7 @@ export default async function RareValuesPage() {
|
||||
<div className="content-card-head-text">
|
||||
<p className="content-card-title">{c.name}</p>
|
||||
<p className="content-card-subtitle">
|
||||
{t("rareCount", { count })}
|
||||
{t("rareCount", { count: rareCount })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { ContentCard, StatBlock } from "@/components/public/ui";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, User } from "@/lib/db";
|
||||
import RedeemForm from "./RedeemForm";
|
||||
|
||||
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).
|
||||
let user: { username: string; credits: number } | null = null;
|
||||
try {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: { username: true, credits: true },
|
||||
});
|
||||
const [row] = await db
|
||||
.select({ username: User.username, credits: User.credits })
|
||||
.from(User)
|
||||
.where(eq(User.id, id))
|
||||
.limit(1);
|
||||
user = row ?? null;
|
||||
} catch {
|
||||
user = null;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { count, desc, eq } from "drizzle-orm";
|
||||
import { headers } from "next/headers";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -8,9 +9,9 @@ import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { Reveal } from "@/components/motion-reveal";
|
||||
import { ThemeSwitcher } from "@/components/theme-switcher";
|
||||
import { cached } from "@/lib/cache";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { resolveHotelName } from "@/lib/hotel-name";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { captchaConfig } from "@/lib/services/captcha";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
@@ -32,21 +33,23 @@ export default async function RegisterPage() {
|
||||
|
||||
const [online, recentUsers, latestUsers] = await Promise.all([
|
||||
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),
|
||||
prisma.user
|
||||
.findMany({
|
||||
where: { online: "1" },
|
||||
select: { username: true, look: true },
|
||||
take: 8,
|
||||
})
|
||||
db
|
||||
.select({ username: User.username, look: User.look })
|
||||
.from(User)
|
||||
.where(eq(User.online, "1"))
|
||||
.limit(8)
|
||||
.catch(() => []),
|
||||
prisma.user
|
||||
.findMany({
|
||||
orderBy: { accountCreated: "desc" },
|
||||
select: { username: true, look: true },
|
||||
take: 8,
|
||||
})
|
||||
db
|
||||
.select({ username: User.username, look: User.look })
|
||||
.from(User)
|
||||
.orderBy(desc(User.accountCreated))
|
||||
.limit(8)
|
||||
.catch(() => []),
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, Rooms, User } from "@/lib/db";
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
@@ -31,24 +32,25 @@ function parseRoomId(raw: string): number | null {
|
||||
async function getRoom(raw: string) {
|
||||
const id = parseRoomId(raw);
|
||||
if (id === null) return null;
|
||||
return prisma.rooms
|
||||
.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
ownerId: true,
|
||||
ownerName: true,
|
||||
name: true,
|
||||
description: true,
|
||||
state: true,
|
||||
users: true,
|
||||
usersMax: true,
|
||||
category: true,
|
||||
score: true,
|
||||
tags: true,
|
||||
},
|
||||
const [room] = await db
|
||||
.select({
|
||||
id: Rooms.id,
|
||||
ownerId: Rooms.ownerId,
|
||||
ownerName: Rooms.ownerName,
|
||||
name: Rooms.name,
|
||||
description: Rooms.description,
|
||||
state: Rooms.state,
|
||||
users: Rooms.users,
|
||||
usersMax: Rooms.usersMax,
|
||||
category: Rooms.category,
|
||||
score: Rooms.score,
|
||||
tags: Rooms.tags,
|
||||
})
|
||||
.catch(() => null);
|
||||
.from(Rooms)
|
||||
.where(eq(Rooms.id, id))
|
||||
.limit(1)
|
||||
.catch(() => []);
|
||||
return room ?? null;
|
||||
}
|
||||
|
||||
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.
|
||||
const owner =
|
||||
room.ownerId > 0
|
||||
? await prisma.user
|
||||
.findUnique({
|
||||
where: { id: room.ownerId },
|
||||
select: { username: true },
|
||||
})
|
||||
.catch(() => null)
|
||||
? (
|
||||
await db
|
||||
.select({ username: User.username })
|
||||
.from(User)
|
||||
.where(eq(User.id, room.ownerId))
|
||||
.limit(1)
|
||||
.catch(() => [])
|
||||
)[0]
|
||||
: null;
|
||||
const ownerName = owner?.username || room.ownerName || "Unknown";
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { and, eq, like } from "drizzle-orm";
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { db, Rooms, User } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const metadata: Metadata = { title: "Search" };
|
||||
@@ -43,38 +44,31 @@ export default async function SearchPage({
|
||||
);
|
||||
}
|
||||
|
||||
const users = await prisma.user
|
||||
.findMany({
|
||||
where: {
|
||||
username: { contains: query },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
motto: true,
|
||||
online: true,
|
||||
rank: true,
|
||||
},
|
||||
take: 30,
|
||||
const users = await db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
motto: User.motto,
|
||||
online: User.online,
|
||||
rank: User.rank,
|
||||
})
|
||||
.from(User)
|
||||
.where(like(User.username, `%${query}%`))
|
||||
.limit(30)
|
||||
.catch(() => []);
|
||||
|
||||
const rooms = await prisma.rooms
|
||||
.findMany({
|
||||
where: {
|
||||
name: { contains: query },
|
||||
state: "open",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
ownerName: true,
|
||||
users: true,
|
||||
usersMax: true,
|
||||
},
|
||||
take: 12,
|
||||
const rooms = await db
|
||||
.select({
|
||||
id: Rooms.id,
|
||||
name: Rooms.name,
|
||||
ownerName: Rooms.ownerName,
|
||||
users: Rooms.users,
|
||||
usersMax: Rooms.usersMax,
|
||||
})
|
||||
.from(Rooms)
|
||||
.where(and(like(Rooms.name, `%${query}%`), eq(Rooms.state, "open")))
|
||||
.limit(12)
|
||||
.catch(() => []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
@@ -11,8 +12,8 @@ import { env } from "@/env";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { LaravelEncrypter } from "@/lib/auth/laravel-encrypter";
|
||||
import { totpKeyUri } from "@/lib/auth/totp";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { resolveHotelName } from "@/lib/hotel-name";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -37,14 +38,16 @@ export default async function TwoFactorPage({
|
||||
twoFactorRecoveryCodes: string | null;
|
||||
} | null = null;
|
||||
try {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
twoFactorSecret: true,
|
||||
twoFactorConfirmedAt: true,
|
||||
twoFactorRecoveryCodes: true,
|
||||
},
|
||||
});
|
||||
const [row] = await db
|
||||
.select({
|
||||
twoFactorSecret: User.twoFactorSecret,
|
||||
twoFactorConfirmedAt: User.twoFactorConfirmedAt,
|
||||
twoFactorRecoveryCodes: User.twoFactorRecoveryCodes,
|
||||
})
|
||||
.from(User)
|
||||
.where(eq(User.id, id))
|
||||
.limit(1);
|
||||
user = row ?? null;
|
||||
} catch {
|
||||
user = null;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -5,8 +6,8 @@ import { getTranslations } from "next-intl/server";
|
||||
import { updateMotto } from "@/actions/user-settings";
|
||||
import { Reveal } from "@/components/motion-reveal";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -25,10 +26,17 @@ export default async function SettingsPage() {
|
||||
mail: string | null;
|
||||
} | null = null;
|
||||
try {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: { username: true, motto: true, look: true, mail: true },
|
||||
});
|
||||
const [row] = await db
|
||||
.select({
|
||||
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 {
|
||||
user = null;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { desc, eq, sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { signOutEverywhere } from "@/actions/sessions";
|
||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db, WebsiteLoginLogs } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -54,20 +55,30 @@ export default async function SessionsPage({
|
||||
const userId = Number(session.user.id);
|
||||
const { signedOutAll } = await searchParams;
|
||||
|
||||
const logins = await prisma.websiteLoginLogs
|
||||
.findMany({
|
||||
where: { userId },
|
||||
orderBy: { id: "desc" },
|
||||
take: 30,
|
||||
select: { id: true, ip: true, userAgent: true, createdAt: true },
|
||||
const logins = await db
|
||||
.select({
|
||||
id: WebsiteLoginLogs.id,
|
||||
ip: WebsiteLoginLogs.ip,
|
||||
userAgent: WebsiteLoginLogs.userAgent,
|
||||
createdAt: WebsiteLoginLogs.createdAt,
|
||||
})
|
||||
.from(WebsiteLoginLogs)
|
||||
.where(eq(WebsiteLoginLogs.userId, userId))
|
||||
.orderBy(desc(WebsiteLoginLogs.id))
|
||||
.limit(30)
|
||||
.catch(() => []);
|
||||
|
||||
type Failed = { id: number; ip_address: string; attempted_at: Date | string };
|
||||
const failed = await prisma.$queryRaw<Failed[]>`
|
||||
SELECT id, ip_address, attempted_at FROM failed_logins
|
||||
WHERE username = ${session.user.name} ORDER BY id DESC LIMIT 15
|
||||
`.catch(() => [] as Failed[]);
|
||||
let failed: Failed[] = [];
|
||||
try {
|
||||
const [rows] = (await db.execute(sql`
|
||||
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 (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { CSSProperties } from "react";
|
||||
import { buyShopArticle } from "@/actions/shop";
|
||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db, WebsiteShopArticles, WebsiteShopCategories } from "@/lib/db";
|
||||
import { excerpt } from "@/lib/format";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { creditsPerUnit } from "@/lib/services/paypal";
|
||||
|
||||
export const revalidate = 300;
|
||||
@@ -62,18 +63,18 @@ type ShopCategory = {
|
||||
icon: string | null;
|
||||
};
|
||||
|
||||
const ARTICLE_SELECT = {
|
||||
id: true,
|
||||
name: true,
|
||||
info: true,
|
||||
iconUrl: true,
|
||||
color: true,
|
||||
costs: true,
|
||||
credits: true,
|
||||
duckets: true,
|
||||
diamonds: true,
|
||||
badges: true,
|
||||
position: true,
|
||||
const articleSelect = {
|
||||
id: WebsiteShopArticles.id,
|
||||
name: WebsiteShopArticles.name,
|
||||
info: WebsiteShopArticles.info,
|
||||
iconUrl: WebsiteShopArticles.iconUrl,
|
||||
color: WebsiteShopArticles.color,
|
||||
costs: WebsiteShopArticles.costs,
|
||||
credits: WebsiteShopArticles.credits,
|
||||
duckets: WebsiteShopArticles.duckets,
|
||||
diamonds: WebsiteShopArticles.diamonds,
|
||||
badges: WebsiteShopArticles.badges,
|
||||
position: WebsiteShopArticles.position,
|
||||
} as const;
|
||||
|
||||
export default async function ShopPage({
|
||||
@@ -115,43 +116,31 @@ export default async function ShopPage({
|
||||
// Categories for the filter bar.
|
||||
let categories: ShopCategory[] = [];
|
||||
try {
|
||||
categories = await prisma.websiteShopCategories.findMany({
|
||||
select: { id: true, name: true, icon: true },
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
categories = await db
|
||||
.select({
|
||||
id: WebsiteShopCategories.id,
|
||||
name: WebsiteShopCategories.name,
|
||||
icon: WebsiteShopCategories.icon,
|
||||
})
|
||||
.from(WebsiteShopCategories)
|
||||
.orderBy(asc(WebsiteShopCategories.name));
|
||||
} catch {
|
||||
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[] = [];
|
||||
try {
|
||||
if (categoryId) {
|
||||
let ids: bigint[] = [];
|
||||
try {
|
||||
const rows = await prisma.$queryRawUnsafe<{ id: bigint }[]>(
|
||||
"SELECT id FROM website_shop_articles WHERE website_shop_category_id = ?",
|
||||
BigInt(categoryId),
|
||||
);
|
||||
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" },
|
||||
})
|
||||
: [];
|
||||
articles = await db
|
||||
.select(articleSelect)
|
||||
.from(WebsiteShopArticles)
|
||||
.where(eq(WebsiteShopArticles.categoryId, BigInt(categoryId)))
|
||||
.orderBy(asc(WebsiteShopArticles.position));
|
||||
} else {
|
||||
articles = await prisma.websiteShopArticles.findMany({
|
||||
select: ARTICLE_SELECT,
|
||||
orderBy: { position: "asc" },
|
||||
});
|
||||
articles = await db
|
||||
.select(articleSelect)
|
||||
.from(WebsiteShopArticles)
|
||||
.orderBy(asc(WebsiteShopArticles.position));
|
||||
}
|
||||
} catch {
|
||||
articles = [];
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { ContentCard, StatBlock } from "@/components/public/ui";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, User } from "@/lib/db";
|
||||
import {
|
||||
creditsPerUnit,
|
||||
isPayPalConfigured,
|
||||
@@ -27,10 +28,12 @@ export default async function TopUpPage({
|
||||
// Current credits balance for context (fail soft).
|
||||
let user: { username: string; credits: number } | null = null;
|
||||
try {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: { username: true, credits: true },
|
||||
});
|
||||
const [row] = await db
|
||||
.select({ username: User.username, credits: User.credits })
|
||||
.from(User)
|
||||
.where(eq(User.id, id))
|
||||
.limit(1);
|
||||
user = row ?? null;
|
||||
} catch {
|
||||
user = null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { asc, desc, eq, gte } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
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";
|
||||
|
||||
export const revalidate = 300;
|
||||
@@ -30,30 +31,35 @@ export default async function StaffPage() {
|
||||
|
||||
let staff: StaffMember[] = [];
|
||||
try {
|
||||
staff = await prisma.user.findMany({
|
||||
where: { rank: { gte: minStaffRank } },
|
||||
select: { username: true, look: true, rank: true, motto: true },
|
||||
orderBy: [{ rank: "desc" }, { username: "asc" }],
|
||||
take: 100,
|
||||
});
|
||||
staff = await db
|
||||
.select({
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
rank: User.rank,
|
||||
motto: User.motto,
|
||||
})
|
||||
.from(User)
|
||||
.where(gte(User.rank, minStaffRank))
|
||||
.orderBy(desc(User.rank), asc(User.username))
|
||||
.limit(100);
|
||||
} catch {
|
||||
staff = [];
|
||||
}
|
||||
|
||||
let teams: Team[] = [];
|
||||
try {
|
||||
const rows = await prisma.websiteTeams.findMany({
|
||||
where: { hiddenRank: false },
|
||||
select: {
|
||||
id: true,
|
||||
rankName: true,
|
||||
badge: true,
|
||||
jobDescription: true,
|
||||
staffColor: true,
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
teams = rows.map((t) => ({ ...t, id: String(t.id) }));
|
||||
const rows = await db
|
||||
.select({
|
||||
id: WebsiteTeams.id,
|
||||
rankName: WebsiteTeams.rankName,
|
||||
badge: WebsiteTeams.badge,
|
||||
jobDescription: WebsiteTeams.jobDescription,
|
||||
staffColor: WebsiteTeams.staffColor,
|
||||
})
|
||||
.from(WebsiteTeams)
|
||||
.where(eq(WebsiteTeams.hiddenRank, false))
|
||||
.orderBy(asc(WebsiteTeams.id));
|
||||
teams = rows.map((row) => ({ ...row, id: String(row.id) }));
|
||||
} catch {
|
||||
teams = [];
|
||||
}
|
||||
@@ -69,14 +75,14 @@ export default async function StaffPage() {
|
||||
subtitle={t("ranksSubtitle")}
|
||||
>
|
||||
<div className="card-grid sm-2 lg-3">
|
||||
{teams.map((t) => (
|
||||
<div key={t.id} className="card">
|
||||
<h3 style={{ margin: "0 0 0.25rem", color: t.staffColor }}>
|
||||
{t.rankName}
|
||||
{teams.map((team) => (
|
||||
<div key={team.id} className="card">
|
||||
<h3 style={{ margin: "0 0 0.25rem", color: team.staffColor }}>
|
||||
{team.rankName}
|
||||
</h3>
|
||||
{t.jobDescription ? (
|
||||
{team.jobDescription ? (
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
{t.jobDescription}
|
||||
{team.jobDescription}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { and, asc, desc, eq, inArray, or } from "drizzle-orm";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
@@ -12,9 +13,19 @@ import {
|
||||
StatBlock,
|
||||
} from "@/components/public/ui";
|
||||
import { auth } from "@/lib/auth";
|
||||
import {
|
||||
CameraWeb,
|
||||
db,
|
||||
MessengerFriendrequests,
|
||||
MessengerFriendships,
|
||||
Rooms,
|
||||
User,
|
||||
UsersBadges,
|
||||
UsersCurrency,
|
||||
WebsiteUserGuestbooks,
|
||||
} from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -47,12 +58,12 @@ export async function generateMetadata({
|
||||
params: Promise<{ username: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { username } = await params;
|
||||
const user = await prisma.user
|
||||
.findUnique({
|
||||
where: { username },
|
||||
select: { username: true, motto: true },
|
||||
})
|
||||
.catch(() => null);
|
||||
const [user] = await db
|
||||
.select({ username: User.username, motto: User.motto })
|
||||
.from(User)
|
||||
.where(eq(User.username, username))
|
||||
.limit(1)
|
||||
.catch(() => []);
|
||||
if (!user) return { title: "Profile" };
|
||||
return {
|
||||
title: user.username,
|
||||
@@ -78,26 +89,28 @@ export default async function ProfilePage({
|
||||
const { username } = await params;
|
||||
const { friend, guestbook: guestbookStatus, error } = await searchParams;
|
||||
|
||||
const [user, imagerBase, session] = await Promise.all([
|
||||
prisma.user.findUnique({
|
||||
where: { username },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
motto: true,
|
||||
look: true,
|
||||
rank: true,
|
||||
credits: true,
|
||||
online: true,
|
||||
accountCreated: true,
|
||||
},
|
||||
}),
|
||||
const [userRows, imagerBase, session] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
motto: User.motto,
|
||||
look: User.look,
|
||||
rank: User.rank,
|
||||
credits: User.credits,
|
||||
online: User.online,
|
||||
accountCreated: User.accountCreated,
|
||||
})
|
||||
.from(User)
|
||||
.where(eq(User.username, username))
|
||||
.limit(1),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
auth(),
|
||||
]);
|
||||
const user = userRows[0] ?? null;
|
||||
|
||||
if (!user) notFound();
|
||||
|
||||
@@ -112,34 +125,50 @@ export default async function ProfilePage({
|
||||
let outgoingPending = false;
|
||||
let incomingPending = false;
|
||||
if (isLoggedIn && !isSelf) {
|
||||
const [friendship, outgoing, incoming] = await Promise.all([
|
||||
prisma.messengerFriendships
|
||||
.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userOneId: meId, userTwoId: user.id },
|
||||
{ userOneId: user.id, userTwoId: meId },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.messengerFriendrequests
|
||||
.findFirst({
|
||||
where: { userFromId: meId, userToId: user.id },
|
||||
select: { id: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.messengerFriendrequests
|
||||
.findFirst({
|
||||
where: { userFromId: user.id, userToId: meId },
|
||||
select: { id: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
const [friendshipRows, outgoingRows, incomingRows] = await Promise.all([
|
||||
db
|
||||
.select({ id: MessengerFriendships.id })
|
||||
.from(MessengerFriendships)
|
||||
.where(
|
||||
or(
|
||||
and(
|
||||
eq(MessengerFriendships.userOneId, meId),
|
||||
eq(MessengerFriendships.userTwoId, user.id),
|
||||
),
|
||||
and(
|
||||
eq(MessengerFriendships.userOneId, user.id),
|
||||
eq(MessengerFriendships.userTwoId, meId),
|
||||
),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
.catch(() => []),
|
||||
db
|
||||
.select({ id: MessengerFriendrequests.id })
|
||||
.from(MessengerFriendrequests)
|
||||
.where(
|
||||
and(
|
||||
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);
|
||||
outgoingPending = Boolean(outgoing);
|
||||
incomingPending = Boolean(incoming);
|
||||
isFriend = Boolean(friendshipRows[0]);
|
||||
outgoingPending = Boolean(outgoingRows[0]);
|
||||
incomingPending = Boolean(incomingRows[0]);
|
||||
}
|
||||
|
||||
const feedbackErrorMessage =
|
||||
@@ -168,14 +197,18 @@ export default async function ProfilePage({
|
||||
|
||||
// Currency ledger rows (duckets/diamonds). Credits live on the users row
|
||||
// itself; the alt-currencies sit in the users_currency ledger keyed by type.
|
||||
const currencyRows = await prisma.usersCurrency
|
||||
.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: { in: [CURRENCY_DUCKETS, CURRENCY_DIAMONDS] },
|
||||
},
|
||||
select: { type: true, amount: true },
|
||||
const currencyRows = await db
|
||||
.select({
|
||||
type: UsersCurrency.type,
|
||||
amount: UsersCurrency.amount,
|
||||
})
|
||||
.from(UsersCurrency)
|
||||
.where(
|
||||
and(
|
||||
eq(UsersCurrency.userId, user.id),
|
||||
inArray(UsersCurrency.type, [CURRENCY_DUCKETS, CURRENCY_DIAMONDS]),
|
||||
),
|
||||
)
|
||||
.catch(() => []);
|
||||
const ducketsAmount =
|
||||
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
|
||||
// user_one_id; resolve the user_two_id side to usernames/looks for the grid.
|
||||
const friendships = await prisma.messengerFriendships
|
||||
.findMany({
|
||||
where: { userOneId: user.id },
|
||||
orderBy: { friendsSince: "desc" },
|
||||
take: 18,
|
||||
select: { userTwoId: true },
|
||||
})
|
||||
const friendships = await db
|
||||
.select({ userTwoId: MessengerFriendships.userTwoId })
|
||||
.from(MessengerFriendships)
|
||||
.where(eq(MessengerFriendships.userOneId, user.id))
|
||||
.orderBy(desc(MessengerFriendships.friendsSince))
|
||||
.limit(18)
|
||||
.catch(() => []);
|
||||
const friendIds = Array.from(new Set(friendships.map((f) => f.userTwoId)));
|
||||
const friendUsers = friendIds.length
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: friendIds } },
|
||||
select: { id: true, username: true, look: true },
|
||||
? await db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
})
|
||||
.from(User)
|
||||
.where(inArray(User.id, friendIds))
|
||||
.catch(() => [])
|
||||
: [];
|
||||
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));
|
||||
|
||||
// Rooms owned by this user.
|
||||
const rooms = await prisma.rooms
|
||||
.findMany({
|
||||
where: { ownerId: user.id },
|
||||
orderBy: { users: "desc" },
|
||||
take: 8,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
users: true,
|
||||
usersMax: true,
|
||||
state: true,
|
||||
},
|
||||
const rooms = await db
|
||||
.select({
|
||||
id: Rooms.id,
|
||||
name: Rooms.name,
|
||||
users: Rooms.users,
|
||||
usersMax: Rooms.usersMax,
|
||||
state: Rooms.state,
|
||||
})
|
||||
.from(Rooms)
|
||||
.where(eq(Rooms.ownerId, user.id))
|
||||
.orderBy(desc(Rooms.users))
|
||||
.limit(8)
|
||||
.catch(() => []);
|
||||
|
||||
const badges = await prisma.usersBadges
|
||||
.findMany({
|
||||
where: { userId: user.id },
|
||||
orderBy: { slotId: "asc" },
|
||||
})
|
||||
const badges = await db
|
||||
.select()
|
||||
.from(UsersBadges)
|
||||
.where(eq(UsersBadges.userId, user.id))
|
||||
.orderBy(asc(UsersBadges.slotId))
|
||||
.catch(() => []);
|
||||
|
||||
const photos = await prisma.cameraWeb
|
||||
.findMany({
|
||||
where: { userId: user.id },
|
||||
orderBy: { timestamp: "desc" },
|
||||
take: 6,
|
||||
})
|
||||
const photos = await db
|
||||
.select()
|
||||
.from(CameraWeb)
|
||||
.where(eq(CameraWeb.userId, user.id))
|
||||
.orderBy(desc(CameraWeb.timestamp))
|
||||
.limit(6)
|
||||
.catch(() => []);
|
||||
|
||||
const guestbook = await prisma.websiteUserGuestbooks
|
||||
.findMany({
|
||||
where: { profileId: user.id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 30,
|
||||
})
|
||||
const guestbook = await db
|
||||
.select()
|
||||
.from(WebsiteUserGuestbooks)
|
||||
.where(eq(WebsiteUserGuestbooks.profileId, user.id))
|
||||
.orderBy(desc(WebsiteUserGuestbooks.createdAt))
|
||||
.limit(30)
|
||||
.catch(() => []);
|
||||
|
||||
// Resolve author usernames/looks for the guestbook entries in one query.
|
||||
const authorIds = Array.from(new Set(guestbook.map((g) => g.userId)));
|
||||
const authors = authorIds.length
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: authorIds } },
|
||||
select: { id: true, username: true, look: true },
|
||||
? await db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
})
|
||||
.from(User)
|
||||
.where(inArray(User.id, authorIds))
|
||||
.catch(() => [])
|
||||
: [];
|
||||
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 Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { isValidVerificationToken } from "@/actions/email-verify";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, User } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -88,19 +89,20 @@ export default async function VerifyPage({
|
||||
const ok = await isValidVerificationToken(normalisedEmail, token);
|
||||
if (ok) {
|
||||
try {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { mail: normalisedEmail },
|
||||
select: { id: true, mailVerified: true },
|
||||
});
|
||||
const [user] = await db
|
||||
.select({ id: User.id, mailVerified: User.mailVerified })
|
||||
.from(User)
|
||||
.where(eq(User.mail, normalisedEmail))
|
||||
.limit(1);
|
||||
if (!user) {
|
||||
status = "invalid";
|
||||
} else if (user.mailVerified === "1") {
|
||||
status = "already";
|
||||
} else {
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { mailVerified: "1" },
|
||||
});
|
||||
await db
|
||||
.update(User)
|
||||
.set({ mailVerified: "1" })
|
||||
.where(eq(User.id, user.id));
|
||||
status = "verified";
|
||||
}
|
||||
} catch {
|
||||
|
||||
Reference in new issue
Block a user