From 7c39aef5d9cac667388e97b4e3185c5b7a04da80 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 1 Aug 2026 14:15:12 +0200 Subject: [PATCH] refactor(db): migrate app pages and APIs from Prisma facade to Drizzle Co-authored-by: Cursor --- src/app/(site)/apply/staff/page.tsx | 35 ++- src/app/(site)/apply/team/page.tsx | 19 +- src/app/(site)/badges/page.tsx | 11 +- src/app/(site)/banned/page.tsx | 28 +- src/app/(site)/draw-badge/page.tsx | 27 +- src/app/(site)/events/[slugOrId]/page.tsx | 107 ++++++-- src/app/(site)/events/page.tsx | 61 ++++- src/app/(site)/friends/page.tsx | 41 +-- .../guilds/[id]/forum/[threadId]/page.tsx | 87 ++++--- src/app/(site)/guilds/[id]/forum/new/page.tsx | 13 +- src/app/(site)/guilds/[id]/forum/page.tsx | 66 +++-- src/app/(site)/guilds/[id]/page.tsx | 55 ++-- src/app/(site)/guilds/page.tsx | 19 +- src/app/(site)/help/[category]/page.tsx | 51 ++-- src/app/(site)/help/page.tsx | 69 +++-- src/app/(site)/help/tickets/[id]/page.tsx | 60 +++-- src/app/(site)/help/tickets/page.tsx | 18 +- src/app/(site)/leaderboard/page.tsx | 85 +++--- src/app/(site)/login/page.tsx | 31 ++- src/app/(site)/marketplace/page.tsx | 35 +-- src/app/(site)/me/page.tsx | 242 ++++++++++-------- src/app/(site)/messages/page.tsx | 71 +++-- src/app/(site)/news/[slug]/page.tsx | 103 +++++--- src/app/(site)/news/page.tsx | 15 +- src/app/(site)/page.tsx | 59 +++-- src/app/(site)/photos/page.tsx | 12 +- src/app/(site)/polls/[id]/page.tsx | 66 +++-- src/app/(site)/polls/page.tsx | 48 +++- src/app/(site)/radio/apply/page.tsx | 19 +- src/app/(site)/radio/contests/[id]/page.tsx | 12 +- src/app/(site)/radio/contests/page.tsx | 26 +- src/app/(site)/radio/giveaways/[id]/page.tsx | 12 +- src/app/(site)/radio/giveaways/page.tsx | 30 +-- src/app/(site)/radio/leaderboard/page.tsx | 25 +- src/app/(site)/radio/page.tsx | 22 +- src/app/(site)/radio/requests/page.tsx | 22 +- src/app/(site)/radio/schedule/page.tsx | 22 +- src/app/(site)/radio/shouts/page.tsx | 24 +- src/app/(site)/rankings/page.tsx | 25 +- src/app/(site)/rares/[category]/page.tsx | 43 ++-- 40 files changed, 1124 insertions(+), 692 deletions(-) diff --git a/src/app/(site)/apply/staff/page.tsx b/src/app/(site)/apply/staff/page.tsx index 5c47a327..65462ae1 100644 --- a/src/app/(site)/apply/staff/page.tsx +++ b/src/app/(site)/apply/staff/page.tsx @@ -1,12 +1,18 @@ +import { desc, eq, inArray } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import type { CSSProperties } from "react"; import { applyStaff } from "@/actions/applications"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { + db, + WebsiteOpenPositions, + WebsiteStaffApplications, + WebsiteTeams, +} from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { resolveHotelName } from "@/lib/hotel-name"; -import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -59,8 +65,10 @@ export default async function ApplyStaffPage({ // ── Open positions ────────────────────────────────────────── // Each query is isolated so a DB hiccup degrades that section to empty // rather than 500-ing the whole page. - const positions = await prisma.websiteOpenPositions - .findMany({ orderBy: { createdAt: "desc" } }) + const positions = await db + .select() + .from(WebsiteOpenPositions) + .orderBy(desc(WebsiteOpenPositions.createdAt)) .catch(() => []); // Resolve display info for each position's rank. The rich rank fields @@ -68,17 +76,24 @@ export default async function ApplyStaffPage({ // the same rank id the position's permission_id points at. const rankIds = Array.from(new Set(positions.map((p) => p.permissionId))); const teams = rankIds.length - ? await prisma.websiteTeams - .findMany({ - where: { id: { in: rankIds.map((r) => BigInt(r)) } }, - }) + ? await db + .select() + .from(WebsiteTeams) + .where( + inArray( + WebsiteTeams.id, + rankIds.map((r) => BigInt(r)), + ), + ) .catch(() => []) : []; - const teamByRankId = new Map(teams.map((t) => [Number(t.id), t])); + const teamByRankId = new Map(teams.map((team) => [Number(team.id), team])); // Ranks this user has already applied for (block re-applying). - const myApps = await prisma.websiteStaffApplications - .findMany({ where: { userId }, select: { rankId: true } }) + const myApps = await db + .select({ rankId: WebsiteStaffApplications.rankId }) + .from(WebsiteStaffApplications) + .where(eq(WebsiteStaffApplications.userId, userId)) .catch(() => []); const appliedRankIds = new Set(myApps.map((a) => a.rankId)); diff --git a/src/app/(site)/apply/team/page.tsx b/src/app/(site)/apply/team/page.tsx index dc6aafd6..5dfb038a 100644 --- a/src/app/(site)/apply/team/page.tsx +++ b/src/app/(site)/apply/team/page.tsx @@ -1,11 +1,12 @@ +import { asc, eq } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import type { CSSProperties } from "react"; import { applyTeam } from "@/actions/applications"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { db, WebsiteStaffApplications, WebsiteTeams } from "@/lib/db"; import { resolveHotelName } from "@/lib/hotel-name"; -import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -59,17 +60,19 @@ export default async function ApplyTeamPage({ // The Prisma slice exposes teams directly (no separate team-position table // with position_kind), so the team application concept lists website_teams. // Hidden ranks are excluded from the public apply page. - const teams = await prisma.websiteTeams - .findMany({ - where: { hiddenRank: false }, - orderBy: { rankName: "asc" }, - }) + const teams = await db + .select() + .from(WebsiteTeams) + .where(eq(WebsiteTeams.hiddenRank, false)) + .orderBy(asc(WebsiteTeams.rankName)) .catch(() => []); // Teams this user has already applied to. Team applications reuse the staff // applications table with rank_id carrying the team id (the team flag). - const myApps = await prisma.websiteStaffApplications - .findMany({ where: { userId }, select: { rankId: true } }) + const myApps = await db + .select({ rankId: WebsiteStaffApplications.rankId }) + .from(WebsiteStaffApplications) + .where(eq(WebsiteStaffApplications.userId, userId)) .catch(() => []); const appliedTeamIds = new Set(myApps.map((a) => a.rankId)); diff --git a/src/app/(site)/badges/page.tsx b/src/app/(site)/badges/page.tsx index 74466ee7..931f4f6c 100644 --- a/src/app/(site)/badges/page.tsx +++ b/src/app/(site)/badges/page.tsx @@ -1,6 +1,7 @@ +import { asc } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; -import { prisma } from "@/lib/prisma"; +import { db, WebsiteBadges } from "@/lib/db"; export const revalidate = 300; @@ -12,10 +13,10 @@ const BADGE_IMG_BASE = "https://images.habbo.com/c_images/album1584"; export default async function BadgesPage() { const t = await getTranslations("pages.badges"); - const badges = await prisma.websiteBadges - .findMany({ - orderBy: { badgeName: "asc" }, - }) + const badges = await db + .select() + .from(WebsiteBadges) + .orderBy(asc(WebsiteBadges.badgeName)) .catch(() => []); return ( diff --git a/src/app/(site)/banned/page.tsx b/src/app/(site)/banned/page.tsx index c7594df8..a928c6fb 100644 --- a/src/app/(site)/banned/page.tsx +++ b/src/app/(site)/banned/page.tsx @@ -1,8 +1,9 @@ +import { and, desc, eq, gt, or } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { ContentCard } from "@/components/public/ui"; import { auth } from "@/lib/auth"; -import { activeBanWhere, unixNow } from "@/lib/bans"; -import { prisma } from "@/lib/prisma"; +import { unixNow } from "@/lib/bans"; +import { Ban, db } from "@/lib/db"; export const dynamic = "force-dynamic"; @@ -16,14 +17,21 @@ export default async function BannedPage() { let hasBan = false; if (session?.user?.id) { try { - const ban = await prisma.ban.findFirst({ - where: { - userId: Number(session.user.id), - ...activeBanWhere(unixNow()), - }, - orderBy: { timestamp: "desc" }, - select: { banReason: true, banExpire: true }, - }); + const now = unixNow(); + const [ban] = await db + .select({ + banReason: Ban.banReason, + banExpire: Ban.banExpire, + }) + .from(Ban) + .where( + and( + eq(Ban.userId, Number(session.user.id)), + or(eq(Ban.banExpire, 0), gt(Ban.banExpire, now)), + ), + ) + .orderBy(desc(Ban.timestamp)) + .limit(1); if (ban) { hasBan = true; reason = ban.banReason; diff --git a/src/app/(site)/draw-badge/page.tsx b/src/app/(site)/draw-badge/page.tsx index 29923389..a164e55b 100644 --- a/src/app/(site)/draw-badge/page.tsx +++ b/src/app/(site)/draw-badge/page.tsx @@ -1,8 +1,9 @@ +import { desc, eq } from "drizzle-orm"; import { redirect } from "next/navigation"; import { buyBadge } from "@/actions/draw-badge"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { db, User, WebsiteDrawbadges } from "@/lib/db"; import { siteSettings } from "@/lib/services/site-settings"; // Reads the live users + website_drawbadges tables and writes credits/badges on @@ -66,21 +67,27 @@ export default async function DrawBadgePage({ } try { - const buyer = await prisma.user.findUnique({ - where: { id: Number(session.user.id) }, - select: { credits: true }, - }); + const [buyer] = await db + .select({ credits: User.credits }) + .from(User) + .where(eq(User.id, Number(session.user.id))) + .limit(1); credits = buyer?.credits ?? 0; } catch { credits = 0; } try { - badges = await prisma.websiteDrawbadges.findMany({ - where: { published: true }, - select: { id: true, badgeUrl: true, badgeName: true, badgeDesc: true }, - orderBy: { id: "desc" }, - }); + badges = await db + .select({ + id: WebsiteDrawbadges.id, + badgeUrl: WebsiteDrawbadges.badgeUrl, + badgeName: WebsiteDrawbadges.badgeName, + badgeDesc: WebsiteDrawbadges.badgeDesc, + }) + .from(WebsiteDrawbadges) + .where(eq(WebsiteDrawbadges.published, true)) + .orderBy(desc(WebsiteDrawbadges.id)); } catch { badges = []; } diff --git a/src/app/(site)/events/[slugOrId]/page.tsx b/src/app/(site)/events/[slugOrId]/page.tsx index aa5af8d5..913d8360 100644 --- a/src/app/(site)/events/[slugOrId]/page.tsx +++ b/src/app/(site)/events/[slugOrId]/page.tsx @@ -1,11 +1,20 @@ +import { and, asc, count, eq, inArray } from "drizzle-orm"; import Image from "next/image"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { + db, + User, + WebsiteEvent, + WebsiteEventPrize, + WebsiteEventRegistration, + WebsiteEventType, + WebsiteEventWinner, +} from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; import { EventRegisterButton } from "./event-register-button"; export const dynamic = "force-dynamic"; @@ -30,47 +39,95 @@ export default async function EventDetailPage({ const session = await auth(); const userId = Number(session?.user?.id); - const event = await prisma.websiteEvent - .findUnique({ - where: { id: eventId }, - include: { - type: true, - prizes: { orderBy: { position: "asc" } }, - winners: { orderBy: { position: "asc" } }, - _count: { select: { registrations: true } }, - }, + const [eventRow] = await db + .select({ + id: WebsiteEvent.id, + title: WebsiteEvent.title, + description: WebsiteEvent.description, + status: WebsiteEvent.status, + image: WebsiteEvent.image, + startsAt: WebsiteEvent.startsAt, + endsAt: WebsiteEvent.endsAt, + maxPlayers: WebsiteEvent.maxPlayers, + typeId: WebsiteEvent.typeId, + typeName: WebsiteEventType.name, + typeColor: WebsiteEventType.color, + typeSlug: WebsiteEventType.slug, }) - .catch(() => null); + .from(WebsiteEvent) + .innerJoin(WebsiteEventType, eq(WebsiteEvent.typeId, WebsiteEventType.id)) + .where(eq(WebsiteEvent.id, eventId)) + .limit(1) + .catch(() => []); if ( - !event || - (event.status !== "published" && event.status !== "completed") + !eventRow || + (eventRow.status !== "published" && eventRow.status !== "completed") ) { notFound(); } + const [prizes, winners, regCountRows] = await Promise.all([ + db + .select() + .from(WebsiteEventPrize) + .where(eq(WebsiteEventPrize.eventId, eventRow.id)) + .orderBy(asc(WebsiteEventPrize.position)) + .catch(() => []), + db + .select() + .from(WebsiteEventWinner) + .where(eq(WebsiteEventWinner.eventId, eventRow.id)) + .orderBy(asc(WebsiteEventWinner.position)) + .catch(() => []), + db + .select({ value: count() }) + .from(WebsiteEventRegistration) + .where(eq(WebsiteEventRegistration.eventId, eventRow.id)) + .catch(() => [{ value: 0 }]), + ]); + + const event = { + ...eventRow, + type: { + name: eventRow.typeName, + color: eventRow.typeColor, + slug: eventRow.typeSlug, + }, + prizes, + winners, + _count: { registrations: Number(regCountRows[0]?.value ?? 0) }, + }; + const alreadyRegistered = Number.isInteger(userId) && userId > 0 ? Boolean( - await prisma.websiteEventRegistration - .findUnique({ - where: { eventId_userId: { eventId: event.id, userId } }, - }) - .catch(() => null), + ( + await db + .select({ id: WebsiteEventRegistration.id }) + .from(WebsiteEventRegistration) + .where( + and( + eq(WebsiteEventRegistration.eventId, event.id), + eq(WebsiteEventRegistration.userId, userId), + ), + ) + .limit(1) + .catch(() => []) + )[0], ) : false; const winnerIds = event.winners.map((w) => w.userId); - const winners = + const winnerUsers = winnerIds.length > 0 - ? await prisma.user - .findMany({ - where: { id: { in: winnerIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, winnerIds)) .catch(() => []) : []; - const winnerName = new Map(winners.map((u) => [u.id, u.username])); + const winnerName = new Map(winnerUsers.map((u) => [u.id, u.username])); const isFull = event.maxPlayers != null && event._count.registrations >= event.maxPlayers; diff --git a/src/app/(site)/events/page.tsx b/src/app/(site)/events/page.tsx index e5ed5b06..a5446deb 100644 --- a/src/app/(site)/events/page.tsx +++ b/src/app/(site)/events/page.tsx @@ -1,10 +1,16 @@ +import { count, desc, eq, inArray } from "drizzle-orm"; import Image from "next/image"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; +import { + db, + WebsiteEvent, + WebsiteEventRegistration, + WebsiteEventType, +} from "@/lib/db"; import { excerpt, slugify } from "@/lib/format"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 60; @@ -13,18 +19,53 @@ export const metadata = { title: "Events" }; export default async function EventsPage() { const t = await getTranslations("pages.events"); - const events = await prisma.websiteEvent - .findMany({ - where: { status: { in: ["published", "completed"] } }, - orderBy: { startsAt: "desc" }, - take: 50, - include: { - type: { select: { name: true, color: true, slug: true } }, - _count: { select: { registrations: true } }, - }, + const eventRows = await db + .select({ + id: WebsiteEvent.id, + title: WebsiteEvent.title, + description: WebsiteEvent.description, + status: WebsiteEvent.status, + image: WebsiteEvent.image, + startsAt: WebsiteEvent.startsAt, + maxPlayers: WebsiteEvent.maxPlayers, + typeName: WebsiteEventType.name, + typeColor: WebsiteEventType.color, + typeSlug: WebsiteEventType.slug, }) + .from(WebsiteEvent) + .innerJoin(WebsiteEventType, eq(WebsiteEvent.typeId, WebsiteEventType.id)) + .where(inArray(WebsiteEvent.status, ["published", "completed"])) + .orderBy(desc(WebsiteEvent.startsAt)) + .limit(50) .catch(() => []); + const eventIds = eventRows.map((e) => e.id); + const regCounts = + eventIds.length > 0 + ? await db + .select({ + eventId: WebsiteEventRegistration.eventId, + value: count(), + }) + .from(WebsiteEventRegistration) + .where(inArray(WebsiteEventRegistration.eventId, eventIds)) + .groupBy(WebsiteEventRegistration.eventId) + .catch(() => []) + : []; + const countByEvent = new Map( + regCounts.map((r) => [r.eventId, Number(r.value)]), + ); + + const events = eventRows.map((event) => ({ + ...event, + type: { + name: event.typeName, + color: event.typeColor, + slug: event.typeSlug, + }, + _count: { registrations: countByEvent.get(event.id) ?? 0 }, + })); + return (
diff --git a/src/app/(site)/friends/page.tsx b/src/app/(site)/friends/page.tsx index d1463b14..f318b192 100644 --- a/src/app/(site)/friends/page.tsx +++ b/src/app/(site)/friends/page.tsx @@ -1,3 +1,4 @@ +import { asc, eq, inArray, or } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -5,8 +6,8 @@ import type { CSSProperties } from "react"; import { removeFriendship } from "@/actions/messenger"; import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { db, MessengerFriendships, 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"; @@ -45,13 +46,18 @@ export default async function FriendsPage({ // Friendships are stored as TWO directional rows (user_one_id→user_two_id // and the reverse). Read both directions, then dedupe the friend ids. - const friendships = await prisma.messengerFriendships - .findMany({ - where: { - OR: [{ userOneId: userId }, { userTwoId: userId }], - }, - select: { userOneId: true, userTwoId: true }, + const friendships = await db + .select({ + userOneId: MessengerFriendships.userOneId, + userTwoId: MessengerFriendships.userTwoId, }) + .from(MessengerFriendships) + .where( + or( + eq(MessengerFriendships.userOneId, userId), + eq(MessengerFriendships.userTwoId, userId), + ), + ) .catch(() => []); const friendIds = Array.from( @@ -64,18 +70,17 @@ export default async function FriendsPage({ const [friends, imagerBase] = await Promise.all([ friendIds.length - ? prisma.user - .findMany({ - where: { id: { in: friendIds } }, - select: { - id: true, - username: true, - look: true, - motto: true, - online: true, - }, - orderBy: { username: "asc" }, + ? db + .select({ + id: User.id, + username: User.username, + look: User.look, + motto: User.motto, + online: User.online, }) + .from(User) + .where(inArray(User.id, friendIds)) + .orderBy(asc(User.username)) .catch(() => []) : Promise.resolve([]), siteSettings.get( diff --git a/src/app/(site)/guilds/[id]/forum/[threadId]/page.tsx b/src/app/(site)/guilds/[id]/forum/[threadId]/page.tsx index 08f78822..b5864234 100644 --- a/src/app/(site)/guilds/[id]/forum/[threadId]/page.tsx +++ b/src/app/(site)/guilds/[id]/forum/[threadId]/page.tsx @@ -1,3 +1,4 @@ +import { and, asc, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -5,8 +6,14 @@ import type { CSSProperties } from "react"; import { replyToThread } from "@/actions/social"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { + db, + Guilds, + GuildsForumsComments, + GuildsForumsThreads, + User, +} from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 60; @@ -63,22 +70,34 @@ export default async function GuildForumThreadPage({ }[] = []; try { - [guild, thread] = await Promise.all([ - prisma.guilds.findUnique({ - where: { id: guildId }, - select: { id: true, name: true }, - }), - prisma.guildsForumsThreads.findFirst({ - where: { id: threadId, guildId, state: 0 }, - select: { - id: true, - subject: true, - locked: true, - pinned: true, - openerId: true, - }, - }), + const [guildRow, threadRow] = await Promise.all([ + db + .select({ id: Guilds.id, name: Guilds.name }) + .from(Guilds) + .where(eq(Guilds.id, guildId)) + .limit(1) + .then((rows) => rows[0] ?? null), + db + .select({ + id: GuildsForumsThreads.id, + subject: GuildsForumsThreads.subject, + locked: GuildsForumsThreads.locked, + pinned: GuildsForumsThreads.pinned, + openerId: GuildsForumsThreads.openerId, + }) + .from(GuildsForumsThreads) + .where( + and( + eq(GuildsForumsThreads.id, threadId), + eq(GuildsForumsThreads.guildId, guildId), + eq(GuildsForumsThreads.state, 0), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null), ]); + guild = guildRow; + thread = threadRow; } catch { guild = null; thread = null; @@ -87,16 +106,24 @@ export default async function GuildForumThreadPage({ if (!guild || !thread) notFound(); try { - posts = await prisma.guildsForumsComments.findMany({ - where: { threadId: thread.id, state: 0 }, - orderBy: [{ createdAt: "asc" }, { id: "asc" }], - select: { - id: true, - userId: true, - message: true, - createdAt: true, - }, - }); + posts = await db + .select({ + id: GuildsForumsComments.id, + userId: GuildsForumsComments.userId, + message: GuildsForumsComments.message, + createdAt: GuildsForumsComments.createdAt, + }) + .from(GuildsForumsComments) + .where( + and( + eq(GuildsForumsComments.threadId, thread.id), + eq(GuildsForumsComments.state, 0), + ), + ) + .orderBy( + asc(GuildsForumsComments.createdAt), + asc(GuildsForumsComments.id), + ); } catch { posts = []; } @@ -107,10 +134,10 @@ export default async function GuildForumThreadPage({ let users: { id: number; username: string }[] = []; try { users = authorIds.length - ? await prisma.user.findMany({ - where: { id: { in: authorIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, authorIds)) : []; } catch { users = []; diff --git a/src/app/(site)/guilds/[id]/forum/new/page.tsx b/src/app/(site)/guilds/[id]/forum/new/page.tsx index a25f1948..d0062130 100644 --- a/src/app/(site)/guilds/[id]/forum/new/page.tsx +++ b/src/app/(site)/guilds/[id]/forum/new/page.tsx @@ -1,3 +1,4 @@ +import { eq } from "drizzle-orm"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -5,7 +6,7 @@ import type { CSSProperties } from "react"; import { postThread } from "@/actions/social"; import { ContentCard } from "@/components/public/ui"; import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { db, Guilds } from "@/lib/db"; export const dynamic = "force-dynamic"; @@ -47,10 +48,12 @@ export default async function NewThreadPage({ // Guild header (read-only). A DB hiccup degrades to notFound rather than 500. let guild: { id: number; name: string } | null = null; try { - guild = await prisma.guilds.findUnique({ - where: { id: guildId }, - select: { id: true, name: true }, - }); + const [row] = await db + .select({ id: Guilds.id, name: Guilds.name }) + .from(Guilds) + .where(eq(Guilds.id, guildId)) + .limit(1); + guild = row ?? null; } catch { guild = null; } diff --git a/src/app/(site)/guilds/[id]/forum/page.tsx b/src/app/(site)/guilds/[id]/forum/page.tsx index 118617c3..a39d514a 100644 --- a/src/app/(site)/guilds/[id]/forum/page.tsx +++ b/src/app/(site)/guilds/[id]/forum/page.tsx @@ -1,10 +1,11 @@ +import { and, desc, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import type { CSSProperties } from "react"; import { ContentCard, EmptyState } from "@/components/public/ui"; +import { db, Guilds, GuildsForumsThreads, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 300; @@ -53,10 +54,16 @@ export default async function GuildForumPage({ // Guild header (read-only). A DB hiccup degrades to notFound rather than 500. let guild: { id: number; name: string; badge: string } | null = null; try { - guild = await prisma.guilds.findUnique({ - where: { id: guildId }, - select: { id: true, name: true, badge: true }, - }); + const [row] = await db + .select({ + id: Guilds.id, + name: Guilds.name, + badge: Guilds.badge, + }) + .from(Guilds) + .where(eq(Guilds.id, guildId)) + .limit(1); + guild = row ?? null; } catch { guild = null; } @@ -66,21 +73,30 @@ export default async function GuildForumPage({ // Threads for this guild. Pinned first, then most-recently active. let threads: ThreadRow[] = []; try { - threads = await prisma.guildsForumsThreads.findMany({ - where: { guildId: guild.id, state: 0 }, - orderBy: [{ pinned: "desc" }, { updatedAt: "desc" }, { id: "desc" }], - take: 100, - select: { - id: true, - openerId: true, - subject: true, - postsCount: true, - createdAt: true, - updatedAt: true, - pinned: true, - locked: true, - }, - }); + threads = await db + .select({ + id: GuildsForumsThreads.id, + openerId: GuildsForumsThreads.openerId, + subject: GuildsForumsThreads.subject, + postsCount: GuildsForumsThreads.postsCount, + createdAt: GuildsForumsThreads.createdAt, + updatedAt: GuildsForumsThreads.updatedAt, + pinned: GuildsForumsThreads.pinned, + locked: GuildsForumsThreads.locked, + }) + .from(GuildsForumsThreads) + .where( + and( + eq(GuildsForumsThreads.guildId, guild.id), + eq(GuildsForumsThreads.state, 0), + ), + ) + .orderBy( + desc(GuildsForumsThreads.pinned), + desc(GuildsForumsThreads.updatedAt), + desc(GuildsForumsThreads.id), + ) + .limit(100); } catch { threads = []; } @@ -88,16 +104,16 @@ export default async function GuildForumPage({ // Resolve opener usernames in one query (no relation exists in the schema, so // join the users table manually by id). Falls back to the raw author id. const openerIds = Array.from( - new Set(threads.map((t) => t.openerId ?? 0).filter((v) => v > 0)), + new Set(threads.map((t2) => t2.openerId ?? 0).filter((v) => v > 0)), ); let users: { id: number; username: string }[] = []; try { users = openerIds.length - ? await prisma.user.findMany({ - where: { id: { in: openerIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, openerIds)) : []; } catch { users = []; diff --git a/src/app/(site)/guilds/[id]/page.tsx b/src/app/(site)/guilds/[id]/page.tsx index 6cade49b..bcaa63fd 100644 --- a/src/app/(site)/guilds/[id]/page.tsx +++ b/src/app/(site)/guilds/[id]/page.tsx @@ -1,10 +1,11 @@ +import { asc, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; +import { db, Guilds, GuildsMembers, User } 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 revalidate = 300; @@ -39,17 +40,19 @@ export default async function GuildPage({ let guild: GuildView | null = null; try { - guild = await prisma.guilds.findUnique({ - where: { id: guildId }, - select: { - id: true, - name: true, - description: true, - badge: true, - userId: true, - dateCreated: true, - }, - }); + const [row] = await db + .select({ + id: Guilds.id, + name: Guilds.name, + description: Guilds.description, + badge: Guilds.badge, + userId: Guilds.userId, + dateCreated: Guilds.dateCreated, + }) + .from(Guilds) + .where(eq(Guilds.id, guildId)) + .limit(1); + guild = row ?? null; } catch { guild = null; } @@ -60,12 +63,15 @@ export default async function GuildPage({ // join the users table manually by id. let memberRows: { userId: number; memberSince: number }[] = []; try { - memberRows = await prisma.guildsMembers.findMany({ - where: { guildId: guild.id }, - orderBy: { memberSince: "asc" }, - take: 200, - select: { userId: true, memberSince: true }, - }); + memberRows = await db + .select({ + userId: GuildsMembers.userId, + memberSince: GuildsMembers.memberSince, + }) + .from(GuildsMembers) + .where(eq(GuildsMembers.guildId, guild.id)) + .orderBy(asc(GuildsMembers.memberSince)) + .limit(200); } catch { memberRows = []; } @@ -80,10 +86,15 @@ export default async function GuildPage({ []; try { users = memberIds.length - ? await prisma.user.findMany({ - where: { id: { in: memberIds } }, - select: { id: true, username: true, look: true, motto: true }, - }) + ? await db + .select({ + id: User.id, + username: User.username, + look: User.look, + motto: User.motto, + }) + .from(User) + .where(inArray(User.id, memberIds)) : []; } catch { users = []; diff --git a/src/app/(site)/guilds/page.tsx b/src/app/(site)/guilds/page.tsx index ab65e203..48a42328 100644 --- a/src/app/(site)/guilds/page.tsx +++ b/src/app/(site)/guilds/page.tsx @@ -1,8 +1,9 @@ +import { desc } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; +import { db, Guilds } from "@/lib/db"; import { excerpt } from "@/lib/format"; -import { prisma } from "@/lib/prisma"; export const revalidate = 300; @@ -17,12 +18,16 @@ type GuildCard = { async function getGuilds(): Promise { try { - const guilds = await prisma.guilds.findMany({ - orderBy: { id: "desc" }, - take: 40, - select: { id: true, name: true, description: true, badge: true }, - }); - return guilds; + return await db + .select({ + id: Guilds.id, + name: Guilds.name, + description: Guilds.description, + badge: Guilds.badge, + }) + .from(Guilds) + .orderBy(desc(Guilds.id)) + .limit(40); } catch { return []; } diff --git a/src/app/(site)/help/[category]/page.tsx b/src/app/(site)/help/[category]/page.tsx index 503d795c..621f51b4 100644 --- a/src/app/(site)/help/[category]/page.tsx +++ b/src/app/(site)/help/[category]/page.tsx @@ -1,10 +1,11 @@ +import { asc, eq, ne } from "drizzle-orm"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { ContentCard } from "@/components/public/ui"; import { SanitizedHtml } from "@/components/shared/sanitized-html"; +import { db, WebsiteHelpCenterCategories } from "@/lib/db"; import { resolveHotelName } from "@/lib/hotel-name"; -import { prisma } from "@/lib/prisma"; import { sanitize } from "@/lib/sanitize"; export const revalidate = 300; @@ -54,21 +55,23 @@ export default async function HelpCategoryPage({ let cat: HelpCategory | null = null; try { - cat = await prisma.websiteHelpCenterCategories.findUnique({ - where: { id: categoryId }, - select: { - id: true, - name: true, - content: true, - position: true, - imageUrl: true, - buttonText: true, - buttonUrl: true, - buttonColor: true, - buttonBorderColor: true, - smallBox: true, - }, - }); + const [row] = await db + .select({ + id: WebsiteHelpCenterCategories.id, + name: WebsiteHelpCenterCategories.name, + content: WebsiteHelpCenterCategories.content, + position: WebsiteHelpCenterCategories.position, + imageUrl: WebsiteHelpCenterCategories.imageUrl, + buttonText: WebsiteHelpCenterCategories.buttonText, + buttonUrl: WebsiteHelpCenterCategories.buttonUrl, + buttonColor: WebsiteHelpCenterCategories.buttonColor, + buttonBorderColor: WebsiteHelpCenterCategories.buttonBorderColor, + smallBox: WebsiteHelpCenterCategories.smallBox, + }) + .from(WebsiteHelpCenterCategories) + .where(eq(WebsiteHelpCenterCategories.id, categoryId)) + .limit(1); + cat = row ?? null; } catch { cat = null; } @@ -81,12 +84,16 @@ export default async function HelpCategoryPage({ // query: a DB hiccup degrades this list to empty rather than 500-ing. let siblings: HelpCategoryLink[] = []; try { - siblings = await prisma.websiteHelpCenterCategories.findMany({ - where: { id: { not: categoryId } }, - orderBy: { position: "asc" }, - select: { id: true, name: true, position: true }, - take: 50, - }); + siblings = await db + .select({ + id: WebsiteHelpCenterCategories.id, + name: WebsiteHelpCenterCategories.name, + position: WebsiteHelpCenterCategories.position, + }) + .from(WebsiteHelpCenterCategories) + .where(ne(WebsiteHelpCenterCategories.id, categoryId)) + .orderBy(asc(WebsiteHelpCenterCategories.position)) + .limit(50); } catch { siblings = []; } diff --git a/src/app/(site)/help/page.tsx b/src/app/(site)/help/page.tsx index 46cd8b5c..aa16d974 100644 --- a/src/app/(site)/help/page.tsx +++ b/src/app/(site)/help/page.tsx @@ -1,7 +1,13 @@ +import { asc } 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, + WebsiteHelpCenterCategories, + WebsiteRuleCategories, + WebsiteRules, +} from "@/lib/db"; export const revalidate = 300; @@ -40,40 +46,57 @@ export default async function HelpCenterPage() { let categories: HelpCategory[] = []; try { - categories = await prisma.websiteHelpCenterCategories.findMany({ - orderBy: [{ position: "asc" }, { id: "asc" }], - select: { - id: true, - name: true, - content: true, - imageUrl: true, - buttonText: true, - buttonUrl: true, - buttonColor: true, - buttonBorderColor: true, - smallBox: true, - }, - }); + categories = await db + .select({ + id: WebsiteHelpCenterCategories.id, + name: WebsiteHelpCenterCategories.name, + content: WebsiteHelpCenterCategories.content, + imageUrl: WebsiteHelpCenterCategories.imageUrl, + buttonText: WebsiteHelpCenterCategories.buttonText, + buttonUrl: WebsiteHelpCenterCategories.buttonUrl, + buttonColor: WebsiteHelpCenterCategories.buttonColor, + buttonBorderColor: WebsiteHelpCenterCategories.buttonBorderColor, + smallBox: WebsiteHelpCenterCategories.smallBox, + }) + .from(WebsiteHelpCenterCategories) + .orderBy( + asc(WebsiteHelpCenterCategories.position), + asc(WebsiteHelpCenterCategories.id), + ); } catch { // DB unavailable — render the shell without help categories. } let ruleCategories: RuleCategory[] = []; try { - ruleCategories = await prisma.websiteRuleCategories.findMany({ - orderBy: { id: "asc" }, - select: { id: true, name: true, description: true, badge: true }, - }); + ruleCategories = await db + .select({ + id: WebsiteRuleCategories.id, + name: WebsiteRuleCategories.name, + description: WebsiteRuleCategories.description, + badge: WebsiteRuleCategories.badge, + }) + .from(WebsiteRuleCategories) + .orderBy(asc(WebsiteRuleCategories.id)); } catch { // DB unavailable — render the shell without rule categories. } let rules: Rule[] = []; try { - rules = await prisma.websiteRules.findMany({ - orderBy: [{ categoryId: "asc" }, { paragraph: "asc" }, { id: "asc" }], - select: { id: true, categoryId: true, paragraph: true, rule: true }, - }); + rules = await db + .select({ + id: WebsiteRules.id, + categoryId: WebsiteRules.categoryId, + paragraph: WebsiteRules.paragraph, + rule: WebsiteRules.rule, + }) + .from(WebsiteRules) + .orderBy( + asc(WebsiteRules.categoryId), + asc(WebsiteRules.paragraph), + asc(WebsiteRules.id), + ); } catch { // DB unavailable — render the shell without rules. } diff --git a/src/app/(site)/help/tickets/[id]/page.tsx b/src/app/(site)/help/tickets/[id]/page.tsx index 48be01e0..34e750bd 100644 --- a/src/app/(site)/help/tickets/[id]/page.tsx +++ b/src/app/(site)/help/tickets/[id]/page.tsx @@ -1,3 +1,4 @@ +import { asc, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -6,8 +7,13 @@ import { closeHelpTicket, replyHelpTicket } from "@/actions/help-tickets"; import { ContentCard } from "@/components/public/ui"; import { positiveBigInt } from "@/lib/api"; import { auth } from "@/lib/auth"; +import { + db, + User, + WebsiteHelpCenterTicketReplies, + WebsiteHelpCenterTickets, +} from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -67,17 +73,19 @@ export default async function HelpTicketDetailPage({ }[] = []; try { - ticket = await prisma.websiteHelpCenterTickets.findUnique({ - where: { id: ticketId }, - select: { - id: true, - userId: true, - title: true, - content: true, - open: true, - createdAt: true, - }, - }); + const [row] = await db + .select({ + id: WebsiteHelpCenterTickets.id, + userId: WebsiteHelpCenterTickets.userId, + title: WebsiteHelpCenterTickets.title, + content: WebsiteHelpCenterTickets.content, + open: WebsiteHelpCenterTickets.open, + createdAt: WebsiteHelpCenterTickets.createdAt, + }) + .from(WebsiteHelpCenterTickets) + .where(eq(WebsiteHelpCenterTickets.id, ticketId)) + .limit(1); + ticket = row ?? null; } catch { ticket = null; } @@ -85,16 +93,16 @@ export default async function HelpTicketDetailPage({ if (!ticket || ticket.userId !== userId) notFound(); try { - replies = await prisma.websiteHelpCenterTicketReplies.findMany({ - where: { ticketId: ticket.id }, - orderBy: { id: "asc" }, - select: { - id: true, - userId: true, - content: true, - createdAt: true, - }, - }); + replies = await db + .select({ + id: WebsiteHelpCenterTicketReplies.id, + userId: WebsiteHelpCenterTicketReplies.userId, + content: WebsiteHelpCenterTicketReplies.content, + createdAt: WebsiteHelpCenterTicketReplies.createdAt, + }) + .from(WebsiteHelpCenterTicketReplies) + .where(eq(WebsiteHelpCenterTicketReplies.ticketId, ticket.id)) + .orderBy(asc(WebsiteHelpCenterTicketReplies.id)); } catch { replies = []; } @@ -102,10 +110,10 @@ export default async function HelpTicketDetailPage({ const authorIds = [...new Set([userId, ...replies.map((r) => r.userId)])]; let users: { id: number; username: string }[] = []; try { - users = await prisma.user.findMany({ - where: { id: { in: authorIds } }, - select: { id: true, username: true }, - }); + users = await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, authorIds)); } catch { users = []; } diff --git a/src/app/(site)/help/tickets/page.tsx b/src/app/(site)/help/tickets/page.tsx index 62cd436a..b163ccc8 100644 --- a/src/app/(site)/help/tickets/page.tsx +++ b/src/app/(site)/help/tickets/page.tsx @@ -1,3 +1,4 @@ +import { desc, eq } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -5,8 +6,8 @@ import type { CSSProperties } from "react"; import { createTicket } from "@/actions/help-tickets"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { db, WebsiteHelpCenterTickets } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -41,15 +42,14 @@ export default async function HelpTicketsPage({ const userId = Number(session.user.id); - let tickets: Awaited< - ReturnType - > = []; + let tickets: (typeof WebsiteHelpCenterTickets.$inferSelect)[] = []; try { - tickets = await prisma.websiteHelpCenterTickets.findMany({ - where: { userId }, - orderBy: { createdAt: "desc" }, - take: 100, - }); + tickets = await db + .select() + .from(WebsiteHelpCenterTickets) + .where(eq(WebsiteHelpCenterTickets.userId, userId)) + .orderBy(desc(WebsiteHelpCenterTickets.createdAt)) + .limit(100); } catch { tickets = []; } diff --git a/src/app/(site)/leaderboard/page.tsx b/src/app/(site)/leaderboard/page.tsx index c679fdad..4ddaec82 100644 --- a/src/app/(site)/leaderboard/page.tsx +++ b/src/app/(site)/leaderboard/page.tsx @@ -1,8 +1,9 @@ +import { desc, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui"; +import { db, User, UsersCurrency, UsersSettings } from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; -import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; export const revalidate = 300; @@ -41,11 +42,15 @@ type Row = { username: string; look: string; value: number }; async function loadCreditsRows(): Promise { try { - const users = await prisma.user.findMany({ - orderBy: { credits: "desc" }, - take: 20, - select: { username: true, look: true, credits: true }, - }); + const users = await db + .select({ + username: User.username, + look: User.look, + credits: User.credits, + }) + .from(User) + .orderBy(desc(User.credits)) + .limit(20); return users.map((u) => ({ username: u.username, look: u.look, @@ -58,18 +63,26 @@ async function loadCreditsRows(): Promise { async function loadCurrencyRows(type: number): Promise { try { - const top = await prisma.usersCurrency.findMany({ - where: { type }, - orderBy: { amount: "desc" }, - take: 20, - select: { userId: true, amount: true }, - }); + const top = await db + .select({ + userId: UsersCurrency.userId, + amount: UsersCurrency.amount, + }) + .from(UsersCurrency) + .where(eq(UsersCurrency.type, type)) + .orderBy(desc(UsersCurrency.amount)) + .limit(20); if (top.length === 0) return []; - const users = await prisma.user.findMany({ - where: { id: { in: top.map((t) => t.userId) } }, - select: { id: true, username: true, look: true }, - }); + const users = await db + .select({ id: User.id, username: User.username, look: User.look }) + .from(User) + .where( + inArray( + User.id, + top.map((t) => t.userId), + ), + ); const byId = new Map(users.map((u) => [u.id, u])); return top @@ -85,31 +98,39 @@ async function loadCurrencyRows(type: number): Promise { } async function loadSettingsRows( - field: "achievementScore" | "respectsReceived" | "onlineTime" | "hofPoints", + field: "achievementScore" | "respectsReceived" | "onlineTime", ): Promise { try { - const top = await prisma.usersSettings.findMany({ - orderBy: { [field]: "desc" }, - take: 20, - select: { userId: true, [field]: true }, - }); + const column = UsersSettings[field]; + const top = await db + .select({ + userId: UsersSettings.userId, + value: column, + }) + .from(UsersSettings) + .orderBy(desc(column)) + .limit(20); if (top.length === 0) return []; - const users = await prisma.user.findMany({ - where: { id: { in: top.map((t) => t.userId) } }, - select: { id: true, username: true, look: true }, - }); + const users = await db + .select({ id: User.id, username: User.username, look: User.look }) + .from(User) + .where( + inArray( + User.id, + top.map((t) => t.userId), + ), + ); const byId = new Map(users.map((u) => [u.id, u])); - return (top as unknown as Array<{ userId: number; [k: string]: unknown }>) + return top .map((t) => { - const u = byId.get(t.userId as number); + const u = byId.get(t.userId); if (!u) return null; - // eslint-disable-next-line security/detect-object-injection -- field is union of known keys return { username: u.username, look: u.look, - value: Number(t[field] ?? 0), + value: Number(t.value ?? 0), }; }) .filter((r): r is Row => r !== null); @@ -134,10 +155,10 @@ export default async function LeaderboardPage({ }) { const t = await getTranslations("pages.leaderboard"); const { type } = await searchParams; - const active: TabKey = TABS.some((t) => t.key === type) + const active: TabKey = TABS.some((tab) => tab.key === type) ? (type as TabKey) : "credits"; - const activeTab = TABS.find((t) => t.key === active) ?? TABS[0]; + const activeTab = TABS.find((tab) => tab.key === active) ?? TABS[0]; const [rows, imagerBase] = await Promise.all([ active === "credits" diff --git a/src/app/(site)/login/page.tsx b/src/app/(site)/login/page.tsx index 0ba7bee0..6693af03 100644 --- a/src/app/(site)/login/page.tsx +++ b/src/app/(site)/login/page.tsx @@ -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 LoginPage() { 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(() => []), ]); diff --git a/src/app/(site)/marketplace/page.tsx b/src/app/(site)/marketplace/page.tsx index 16a5408b..e08f245b 100644 --- a/src/app/(site)/marketplace/page.tsx +++ b/src/app/(site)/marketplace/page.tsx @@ -1,7 +1,8 @@ +import { desc, eq, inArray } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; +import { db, MarketplaceItems, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 300; @@ -37,18 +38,18 @@ export default async function MarketplacePage() { timestamp: number; }[] = []; try { - offers = await prisma.marketplaceItems.findMany({ - where: { state: STATE_ACTIVE }, - select: { - id: true, - itemId: true, - userId: true, - price: true, - timestamp: true, - }, - orderBy: { timestamp: "desc" }, - take: 100, - }); + offers = await db + .select({ + id: MarketplaceItems.id, + itemId: MarketplaceItems.itemId, + userId: MarketplaceItems.userId, + price: MarketplaceItems.price, + timestamp: MarketplaceItems.timestamp, + }) + .from(MarketplaceItems) + .where(eq(MarketplaceItems.state, STATE_ACTIVE)) + .orderBy(desc(MarketplaceItems.timestamp)) + .limit(100); } catch { offers = []; } @@ -58,10 +59,10 @@ export default async function MarketplacePage() { try { const ids = [...new Set(offers.map((o) => o.userId))]; if (ids.length > 0) { - const users = await prisma.user.findMany({ - where: { id: { in: ids } }, - select: { id: true, username: true }, - }); + const users = await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, ids)); sellers = new Map(users.map((u) => [u.id, u.username])); } } catch { diff --git a/src/app/(site)/me/page.tsx b/src/app/(site)/me/page.tsx index c5eeda1e..ef3c20d8 100644 --- a/src/app/(site)/me/page.tsx +++ b/src/app/(site)/me/page.tsx @@ -1,3 +1,4 @@ +import { and, asc, count, desc, eq, gt, inArray, or } from "drizzle-orm"; import Image from "next/image"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -6,9 +7,20 @@ import { claimReferral } from "@/actions/referral"; import { Reveal } from "@/components/motion-reveal"; import { auth } from "@/lib/auth"; +import { + db, + MessengerFriendships, + MessengerOffline, + Rooms, + User, + UserReferrals, + UsersBadges, + UsersSettings, + WebsiteLoginLogs, + WebsiteSetting, +} from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; import { resolveHotelName } from "@/lib/hotel-name"; -import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; import CopyReferralButton from "./CopyReferralButton"; @@ -72,103 +84,127 @@ export default async function MePage({ const BADGE_IMG_BASE = "https://images.habbo.com/c_images/album1584"; const [ - user, + userRows, imagerBase, hotelName, - neededRaw, - amountRaw, - currencyRaw, + neededRows, + amountRows, + currencyRows, alertRaw, recentRooms, badges, - lastWebLogin, - userSettings, - friendCount, - unreadCount, + lastWebLoginRows, + userSettingsRows, + friendCountRows, + unreadCountRows, ] = await Promise.all([ - prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - username: true, - look: true, - motto: true, - rank: true, - credits: true, - accountCreated: true, - lastLogin: true, - lastOnline: true, - }, - }), + db + .select({ + id: User.id, + username: User.username, + look: User.look, + motto: User.motto, + rank: User.rank, + credits: User.credits, + accountCreated: User.accountCreated, + lastLogin: User.lastLogin, + lastOnline: User.lastOnline, + }) + .from(User) + .where(eq(User.id, userId)) + .limit(1), siteSettings.get( "habbo_imaging_url", "https://www.habbo.com/habbo-imaging/avatarimage", ), resolveHotelName(), - prisma.websiteSetting - .findUnique({ - where: { key: "referrals_needed" }, - select: { value: true }, - }) - .catch(() => null), - prisma.websiteSetting - .findUnique({ - where: { key: "referral_reward_amount" }, - select: { value: true }, - }) - .catch(() => null), - prisma.websiteSetting - .findFirst({ - where: { - key: { - in: ["referral_reward_currency_type", "referral_reward_currency"], - }, - }, - select: { value: true }, - }) - .catch(() => null), + db + .select({ value: WebsiteSetting.value }) + .from(WebsiteSetting) + .where(eq(WebsiteSetting.key, "referrals_needed")) + .limit(1) + .catch(() => []), + db + .select({ value: WebsiteSetting.value }) + .from(WebsiteSetting) + .where(eq(WebsiteSetting.key, "referral_reward_amount")) + .limit(1) + .catch(() => []), + db + .select({ value: WebsiteSetting.value }) + .from(WebsiteSetting) + .where( + inArray(WebsiteSetting.key, [ + "referral_reward_currency_type", + "referral_reward_currency", + ]), + ) + .limit(1) + .catch(() => []), siteSettings.get("hotel_alert_banner", ""), - prisma.rooms - .findMany({ - orderBy: { id: "desc" }, - select: { - id: true, - name: true, - ownerName: true, - users: true, - usersMax: true, - }, - take: 6, + db + .select({ + id: Rooms.id, + name: Rooms.name, + ownerName: Rooms.ownerName, + users: Rooms.users, + usersMax: Rooms.usersMax, }) + .from(Rooms) + .orderBy(desc(Rooms.id)) + .limit(6) .catch(() => []), - prisma.usersBadges - .findMany({ - where: { userId, slotId: { gt: 0 } }, - orderBy: { slotId: "asc" }, - select: { badgeCode: true, slotId: true }, + db + .select({ + badgeCode: UsersBadges.badgeCode, + slotId: UsersBadges.slotId, }) + .from(UsersBadges) + .where(and(eq(UsersBadges.userId, userId), gt(UsersBadges.slotId, 0))) + .orderBy(asc(UsersBadges.slotId)) .catch(() => []), - prisma.websiteLoginLogs - .findFirst({ - where: { userId }, - orderBy: { id: "desc" }, - select: { createdAt: true }, + db + .select({ createdAt: WebsiteLoginLogs.createdAt }) + .from(WebsiteLoginLogs) + .where(eq(WebsiteLoginLogs.userId, userId)) + .orderBy(desc(WebsiteLoginLogs.id)) + .limit(1) + .catch(() => []), + db + .select({ + achievementScore: UsersSettings.achievementScore, + respectsReceived: UsersSettings.respectsReceived, }) - .catch(() => null), - prisma.usersSettings - .findUnique({ - where: { userId }, - select: { achievementScore: true, respectsReceived: true }, - }) - .catch(() => null), - prisma.messengerFriendships - .count({ - where: { OR: [{ userOneId: userId }, { userTwoId: userId }] }, - }) - .catch(() => 0), - prisma.messengerOffline.count({ where: { userId } }).catch(() => 0), + .from(UsersSettings) + .where(eq(UsersSettings.userId, userId)) + .limit(1) + .catch(() => []), + db + .select({ value: count() }) + .from(MessengerFriendships) + .where( + or( + eq(MessengerFriendships.userOneId, userId), + eq(MessengerFriendships.userTwoId, userId), + ), + ) + .catch(() => [{ value: 0 }]), + db + .select({ value: count() }) + .from(MessengerOffline) + .where(eq(MessengerOffline.userId, userId)) + .catch(() => [{ value: 0 }]), ]); + const user = userRows[0] ?? null; + const neededRaw = neededRows[0] ?? null; + const amountRaw = amountRows[0] ?? null; + const currencyRaw = currencyRows[0] ?? null; + const lastWebLogin = lastWebLoginRows[0] ?? null; + const userSettings = userSettingsRows[0] ?? null; + const friendCount = Number(friendCountRows[0]?.value ?? 0); + const unreadCount = Number(unreadCountRows[0]?.value ?? 0); + void lastWebLogin; if (!user) throw new Error("user-not-found"); @@ -179,22 +215,29 @@ export default async function MePage({ .trim() .toLowerCase(); - const referrals = await prisma.userReferrals - .findFirst({ - where: { userId }, - select: { referralsTotal: true }, - orderBy: { id: "desc" }, - }) - .catch(() => null); + const [referrals] = await db + .select({ referralsTotal: UserReferrals.referralsTotal }) + .from(UserReferrals) + .where(eq(UserReferrals.userId, userId)) + .orderBy(desc(UserReferrals.id)) + .limit(1) + .catch(() => []); const referralTotal = referrals ? Number(referrals.referralsTotal) : 0; const canClaim = referralTotal >= needed; const remaining = Math.max(0, needed - referralTotal); - const friendships = await prisma.messengerFriendships - .findMany({ - where: { OR: [{ userOneId: userId }, { userTwoId: userId }] }, - select: { userOneId: true, userTwoId: true }, + const friendships = await db + .select({ + userOneId: MessengerFriendships.userOneId, + userTwoId: MessengerFriendships.userTwoId, }) + .from(MessengerFriendships) + .where( + or( + eq(MessengerFriendships.userOneId, userId), + eq(MessengerFriendships.userTwoId, userId), + ), + ) .catch(() => []); const friendIds = Array.from( @@ -206,17 +249,16 @@ export default async function MePage({ ); const friends = friendIds.length - ? await prisma.user - .findMany({ - where: { id: { in: friendIds } }, - select: { - id: true, - username: true, - look: true, - motto: true, - online: true, - }, + ? await db + .select({ + id: User.id, + username: User.username, + look: User.look, + motto: User.motto, + online: User.online, }) + .from(User) + .where(inArray(User.id, friendIds)) .catch(() => []) : []; diff --git a/src/app/(site)/messages/page.tsx b/src/app/(site)/messages/page.tsx index d743908f..7db4ff0c 100644 --- a/src/app/(site)/messages/page.tsx +++ b/src/app/(site)/messages/page.tsx @@ -1,3 +1,4 @@ +import { desc, eq, inArray, or } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -9,9 +10,15 @@ import { } from "@/actions/messenger"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { + db, + MessengerFriendrequests, + MessengerFriendships, + MessengerOffline, + User, +} 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"; @@ -64,24 +71,38 @@ export default async function MessagesPage({ try { const [reqRows, msgRows, friendRows, base] = await Promise.all([ - prisma.messengerFriendrequests.findMany({ - where: { userToId: userId }, - select: { id: true, userFromId: true }, - orderBy: { id: "desc" }, - take: 100, - }), - prisma.messengerOffline.findMany({ - where: { userId }, - select: { id: true, userFromId: true, message: true, sendedOn: true }, - orderBy: { sendedOn: "desc" }, - take: 50, - }), - prisma.messengerFriendships.findMany({ - where: { - OR: [{ userOneId: userId }, { userTwoId: userId }], - }, - select: { userOneId: true, userTwoId: true }, - }), + db + .select({ + id: MessengerFriendrequests.id, + userFromId: MessengerFriendrequests.userFromId, + }) + .from(MessengerFriendrequests) + .where(eq(MessengerFriendrequests.userToId, userId)) + .orderBy(desc(MessengerFriendrequests.id)) + .limit(100), + db + .select({ + id: MessengerOffline.id, + userFromId: MessengerOffline.userFromId, + message: MessengerOffline.message, + sendedOn: MessengerOffline.sendedOn, + }) + .from(MessengerOffline) + .where(eq(MessengerOffline.userId, userId)) + .orderBy(desc(MessengerOffline.sendedOn)) + .limit(50), + db + .select({ + userOneId: MessengerFriendships.userOneId, + userTwoId: MessengerFriendships.userTwoId, + }) + .from(MessengerFriendships) + .where( + or( + eq(MessengerFriendships.userOneId, userId), + eq(MessengerFriendships.userTwoId, userId), + ), + ), siteSettings.get( "habbo_imaging_url", "https://www.habbo.com/habbo-imaging/avatarimage", @@ -119,10 +140,14 @@ export default async function MessagesPage({ >(); if (otherIds.length > 0) { try { - const users = await prisma.user.findMany({ - where: { id: { in: otherIds } }, - select: { id: true, username: true, look: true }, - }); + const users = await db + .select({ + id: User.id, + username: User.username, + look: User.look, + }) + .from(User) + .where(inArray(User.id, otherIds)); userById = new Map(users.map((u) => [u.id, u])); } catch { userById = new Map(); diff --git a/src/app/(site)/news/[slug]/page.tsx b/src/app/(site)/news/[slug]/page.tsx index c0669c14..ead062e7 100644 --- a/src/app/(site)/news/[slug]/page.tsx +++ b/src/app/(site)/news/[slug]/page.tsx @@ -1,3 +1,4 @@ +import { and, asc, desc, eq, inArray } from "drizzle-orm"; import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; @@ -8,9 +9,15 @@ import { toggleReaction } from "@/actions/article-reactions"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { SanitizedHtml } from "@/components/shared/sanitized-html"; import { auth } from "@/lib/auth"; +import { + db, + User, + WebsiteArticleComments, + WebsiteArticleReactions, + WebsiteArticles, +} from "@/lib/db"; import { excerpt } from "@/lib/format"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; import { cacheQuery } from "@/lib/prisma-cache"; import { sanitize } from "@/lib/sanitize"; @@ -39,12 +46,11 @@ function feedbackStyle(tone: "success" | "error"): CSSProperties { } export async function generateStaticParams() { - const slugs = await prisma.websiteArticles - .findMany({ - select: { slug: true }, - orderBy: { createdAt: "desc" }, - take: 20, - }) + const slugs = await db + .select({ slug: WebsiteArticles.slug }) + .from(WebsiteArticles) + .orderBy(desc(WebsiteArticles.createdAt)) + .limit(20) .catch(() => []); return slugs.map((a) => ({ slug: a.slug })); } @@ -56,9 +62,15 @@ export async function generateMetadata({ }): Promise { const appUrl = process.env.APP_URL ?? "http://localhost:3000"; const { slug } = await params; - const article = await prisma.websiteArticles - .findUnique({ where: { slug }, select: { title: true, shortStory: true } }) - .catch(() => null); + const [article] = await db + .select({ + title: WebsiteArticles.title, + shortStory: WebsiteArticles.shortStory, + }) + .from(WebsiteArticles) + .where(eq(WebsiteArticles.slug, slug)) + .limit(1) + .catch(() => []); if (!article) return { title: "Article" }; return { title: article.title, @@ -97,22 +109,24 @@ export default async function ArticlePage({ "websiteArticles", "findUnique", { slug }, - () => - prisma.websiteArticles - .findUnique({ - where: { slug }, - select: { - id: true, - slug: true, - title: true, - shortStory: true, - fullStory: true, - image: true, - createdAt: true, - updatedAt: true, - }, + async () => { + const [row] = await db + .select({ + id: WebsiteArticles.id, + slug: WebsiteArticles.slug, + title: WebsiteArticles.title, + shortStory: WebsiteArticles.shortStory, + fullStory: WebsiteArticles.fullStory, + image: WebsiteArticles.image, + createdAt: WebsiteArticles.createdAt, + updatedAt: WebsiteArticles.updatedAt, }) - .catch(() => null), + .from(WebsiteArticles) + .where(eq(WebsiteArticles.slug, slug)) + .limit(1) + .catch(() => []); + return row ?? null; + }, ); if (!article) notFound(); @@ -127,10 +141,18 @@ export default async function ArticlePage({ let reactionCounts = new Map(); let myReaction: string | null = null; try { - const rows = await prisma.websiteArticleReactions.findMany({ - where: { articleId, active: true }, - select: { reaction: true, userId: true }, - }); + const rows = await db + .select({ + reaction: WebsiteArticleReactions.reaction, + userId: WebsiteArticleReactions.userId, + }) + .from(WebsiteArticleReactions) + .where( + and( + eq(WebsiteArticleReactions.articleId, articleId), + eq(WebsiteArticleReactions.active, true), + ), + ); for (const r of rows) { reactionCounts.set(r.reaction, (reactionCounts.get(r.reaction) ?? 0) + 1); if (sessionUserId !== null && r.userId === sessionUserId) @@ -142,15 +164,14 @@ export default async function ArticlePage({ } // Comments for this article (oldest first). - let comments: Awaited< - ReturnType - > = []; + let comments: (typeof WebsiteArticleComments.$inferSelect)[] = []; try { - comments = await prisma.websiteArticleComments.findMany({ - where: { articleId }, - orderBy: { createdAt: "asc" }, - take: 200, - }); + comments = await db + .select() + .from(WebsiteArticleComments) + .where(eq(WebsiteArticleComments.articleId, articleId)) + .orderBy(asc(WebsiteArticleComments.createdAt)) + .limit(200); } catch { comments = []; } @@ -160,10 +181,10 @@ export default async function ArticlePage({ let usernameById = new Map(); if (commenterIds.length > 0) { try { - const users = await prisma.user.findMany({ - where: { id: { in: commenterIds } }, - select: { id: true, username: true }, - }); + const users = await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, commenterIds)); usernameById = new Map(users.map((u) => [u.id, u.username])); } catch { usernameById = new Map(); diff --git a/src/app/(site)/news/page.tsx b/src/app/(site)/news/page.tsx index 68dd07be..763655e9 100644 --- a/src/app/(site)/news/page.tsx +++ b/src/app/(site)/news/page.tsx @@ -1,10 +1,11 @@ +import { desc } from "drizzle-orm"; import Image from "next/image"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; +import { db, WebsiteArticles } from "@/lib/db"; import { excerpt } from "@/lib/format"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 300; @@ -13,13 +14,13 @@ export const metadata = { title: "News" }; export default async function NewsPage() { const t = await getTranslations("pages.news"); - let articles: Awaited> = - []; + let articles: (typeof WebsiteArticles.$inferSelect)[] = []; try { - articles = await prisma.websiteArticles.findMany({ - orderBy: { createdAt: "desc" }, - take: 30, - }); + articles = await db + .select() + .from(WebsiteArticles) + .orderBy(desc(WebsiteArticles.createdAt)) + .limit(30); } catch { // DB unavailable — render the empty state rather than a 500. articles = []; diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx index ffa578cc..2c75d234 100644 --- a/src/app/(site)/page.tsx +++ b/src/app/(site)/page.tsx @@ -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"; @@ -13,10 +14,10 @@ import { ThemeSwitcher } from "@/components/theme-switcher"; import { TypewriterText } from "@/components/typewriter-text"; import { auth } from "@/lib/auth"; import { cached } from "@/lib/cache"; +import { CameraWeb, db, Rooms, User, WebsiteArticles } from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; import { formatDate } from "@/lib/format-date"; import { resolveHotelName } from "@/lib/hotel-name"; -import { prisma } from "@/lib/prisma"; import { captchaConfig } from "@/lib/services/captcha"; import { siteSettings } from "@/lib/services/site-settings"; @@ -43,29 +44,47 @@ async function getHotelData() { 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), - cached("total_users", 30_000, () => prisma.user.count()).catch(() => 0), - cached("total_rooms", 30_000, () => prisma.rooms.count()).catch(() => 0), - prisma.websiteArticles - .findMany({ orderBy: { createdAt: "desc" }, take: 5 }) + cached("total_users", 30_000, () => + db + .select({ total: count() }) + .from(User) + .then((rows) => rows[0]?.total ?? 0), + ).catch(() => 0), + cached("total_rooms", 30_000, () => + db + .select({ total: count() }) + .from(Rooms) + .then((rows) => rows[0]?.total ?? 0), + ).catch(() => 0), + db + .select() + .from(WebsiteArticles) + .orderBy(desc(WebsiteArticles.createdAt)) + .limit(5) .catch(() => []), - prisma.user - .findMany({ - where: { online: "1" }, - select: { username: true, look: true }, - take: 30, - }) + db + .select({ username: User.username, look: User.look }) + .from(User) + .where(eq(User.online, "1")) + .limit(30) .catch(() => []), - prisma.cameraWeb - .findMany({ orderBy: { timestamp: "desc" }, take: 4 }) + db + .select() + .from(CameraWeb) + .orderBy(desc(CameraWeb.timestamp)) + .limit(4) .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(() => []), ]); diff --git a/src/app/(site)/photos/page.tsx b/src/app/(site)/photos/page.tsx index 38657033..54d825e0 100644 --- a/src/app/(site)/photos/page.tsx +++ b/src/app/(site)/photos/page.tsx @@ -1,11 +1,12 @@ +import { desc } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { type LightboxPhoto, PhotoLightbox, } from "@/components/public/photo-lightbox"; import { ContentCard, EmptyState } from "@/components/public/ui"; +import { CameraWeb, db } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 300; @@ -24,10 +25,11 @@ export default async function PhotosPage() { let photos: Photo[] = []; try { - photos = await prisma.cameraWeb.findMany({ - orderBy: { timestamp: "desc" }, - take: 48, - }); + photos = await db + .select() + .from(CameraWeb) + .orderBy(desc(CameraWeb.timestamp)) + .limit(48); } catch { photos = []; } diff --git a/src/app/(site)/polls/[id]/page.tsx b/src/app/(site)/polls/[id]/page.tsx index 897a44d7..e2e6897a 100644 --- a/src/app/(site)/polls/[id]/page.tsx +++ b/src/app/(site)/polls/[id]/page.tsx @@ -1,10 +1,16 @@ +import { and, asc, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { + db, + WebsitePoll, + WebsitePollQuestion, + WebsitePollVote, +} from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; import { PollVoteForm } from "./poll-vote-form"; export const dynamic = "force-dynamic"; @@ -22,19 +28,24 @@ export default async function PollDetailPage({ const session = await auth(); const userId = Number(session?.user?.id); - const poll = await prisma.websitePoll - .findUnique({ - where: { id: pollId }, - include: { - questions: { orderBy: { sortOrder: "asc" } }, - }, - }) - .catch(() => null); + const [poll] = await db + .select() + .from(WebsitePoll) + .where(eq(WebsitePoll.id, pollId)) + .limit(1) + .catch(() => []); if (!poll || (poll.status !== "active" && poll.status !== "closed")) { notFound(); } + const questions = await db + .select() + .from(WebsitePollQuestion) + .where(eq(WebsitePollQuestion.pollId, poll.id)) + .orderBy(asc(WebsitePollQuestion.sortOrder)) + .catch(() => []); + const now = Date.now(); if ( poll.startsAt && @@ -50,24 +61,31 @@ export default async function PollDetailPage({ (poll.endsAt != null && poll.endsAt.getTime() < now); const isOpen = poll.status === "active" && !hasEnded; - const questionIds = poll.questions.map((q) => q.id); + const questionIds = questions.map((q) => q.id); const myVotes = Number.isInteger(userId) && userId > 0 && questionIds.length > 0 - ? await prisma.websitePollVote - .findMany({ - where: { userId, questionId: { in: questionIds } }, - }) + ? await db + .select() + .from(WebsitePollVote) + .where( + and( + eq(WebsitePollVote.userId, userId), + inArray(WebsitePollVote.questionId, questionIds), + ), + ) .catch(() => []) : []; const hasVoted = myVotes.length > 0; const allVotes = - poll.showResults === 1 - ? await prisma.websitePollVote - .findMany({ - where: { questionId: { in: questionIds } }, - select: { questionId: true, answer: true }, + poll.showResults === 1 && questionIds.length > 0 + ? await db + .select({ + questionId: WebsitePollVote.questionId, + answer: WebsitePollVote.answer, }) + .from(WebsitePollVote) + .where(inArray(WebsitePollVote.questionId, questionIds)) .catch(() => []) : []; @@ -112,7 +130,7 @@ export default async function PollDetailPage({ > ({ + questions={questions.map((q) => ({ id: q.id, question: q.question, type: q.type, @@ -140,7 +158,7 @@ export default async function PollDetailPage({ {poll.showResults === 1 && (hasVoted || hasEnded) ? (
- {poll.questions.map((q) => { + {questions.map((q) => { const options = q.options .split("\n") .map((o) => o.trim()) @@ -179,9 +197,9 @@ export default async function PollDetailPage({
{options.map((opt) => { - const count = counts.get(opt) ?? 0; + const voteCount = counts.get(opt) ?? 0; const pct = - total > 0 ? Math.round((count / total) * 100) : 0; + total > 0 ? Math.round((voteCount / total) * 100) : 0; return (
{opt} - {t("voteCount", { count, pct })} + {t("voteCount", { count: voteCount, pct })}
[]); + const pollIds = pollRows.map((p) => p.id); + const questionCounts = + pollIds.length > 0 + ? await db + .select({ + pollId: WebsitePollQuestion.pollId, + value: count(), + }) + .from(WebsitePollQuestion) + .where(inArray(WebsitePollQuestion.pollId, pollIds)) + .groupBy(WebsitePollQuestion.pollId) + .catch(() => []) + : []; + const countByPoll = new Map( + questionCounts.map((r) => [r.pollId, Number(r.value)]), + ); + + const polls = pollRows.map((poll) => ({ + ...poll, + _count: { questions: countByPoll.get(poll.id) ?? 0 }, + })); + return (
diff --git a/src/app/(site)/radio/apply/page.tsx b/src/app/(site)/radio/apply/page.tsx index 03a542a6..d22fd3ab 100644 --- a/src/app/(site)/radio/apply/page.tsx +++ b/src/app/(site)/radio/apply/page.tsx @@ -1,11 +1,12 @@ +import { desc, eq } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import type { CSSProperties } from "react"; import { applyDj } from "@/actions/radio-apply"; import { ContentCard } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { db, RadioApplications } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -49,13 +50,17 @@ export default async function RadioApplyPage({ // Surface the applicant's most recent application (if any) so they aren't // left guessing. Isolated query: a DB hiccup degrades this to null, not 500. - const existing = await prisma.radioApplications - .findFirst({ - where: { userId: BigInt(userId) }, - orderBy: { id: "desc" }, - select: { id: true, status: true, createdAt: true }, + const [existing] = await db + .select({ + id: RadioApplications.id, + status: RadioApplications.status, + createdAt: RadioApplications.createdAt, }) - .catch(() => null); + .from(RadioApplications) + .where(eq(RadioApplications.userId, BigInt(userId))) + .orderBy(desc(RadioApplications.id)) + .limit(1) + .catch(() => []); const submittedDate = existing?.createdAt ? formatDate(existing.createdAt, "date") diff --git a/src/app/(site)/radio/contests/[id]/page.tsx b/src/app/(site)/radio/contests/[id]/page.tsx index b9b15dfa..da92fb22 100644 --- a/src/app/(site)/radio/contests/[id]/page.tsx +++ b/src/app/(site)/radio/contests/[id]/page.tsx @@ -1,9 +1,10 @@ +import { eq } from "drizzle-orm"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { ContentCard } from "@/components/public/ui"; +import { db, RadioContests } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 300; @@ -22,9 +23,12 @@ export default async function RadioContestDetailPage({ notFound(); } - const contest = await prisma.radioContests - .findUnique({ where: { id: contestId } }) - .catch(() => null); + const [contest] = await db + .select() + .from(RadioContests) + .where(eq(RadioContests.id, contestId)) + .limit(1) + .catch(() => []); if (!contest) notFound(); diff --git a/src/app/(site)/radio/contests/page.tsx b/src/app/(site)/radio/contests/page.tsx index 1342ca78..d1755ab8 100644 --- a/src/app/(site)/radio/contests/page.tsx +++ b/src/app/(site)/radio/contests/page.tsx @@ -1,27 +1,27 @@ +import { desc, eq } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; +import { db, RadioContests } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 300; export default async function RadioContestsPage() { const t = await getTranslations("pages.radioContests"); - const contests = await prisma.radioContests - .findMany({ - where: { isActive: true }, - orderBy: { startDate: "desc" }, - take: 50, - select: { - id: true, - title: true, - prize: true, - startDate: true, - endDate: true, - }, + const contests = await db + .select({ + id: RadioContests.id, + title: RadioContests.title, + prize: RadioContests.prize, + startDate: RadioContests.startDate, + endDate: RadioContests.endDate, }) + .from(RadioContests) + .where(eq(RadioContests.isActive, true)) + .orderBy(desc(RadioContests.startDate)) + .limit(50) .catch(() => []); return ( diff --git a/src/app/(site)/radio/giveaways/[id]/page.tsx b/src/app/(site)/radio/giveaways/[id]/page.tsx index 14dece29..a9e06892 100644 --- a/src/app/(site)/radio/giveaways/[id]/page.tsx +++ b/src/app/(site)/radio/giveaways/[id]/page.tsx @@ -1,9 +1,10 @@ +import { eq } from "drizzle-orm"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { ContentCard } from "@/components/public/ui"; +import { db, RadioGiveaways } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 300; @@ -22,9 +23,12 @@ export default async function RadioGiveawayDetailPage({ notFound(); } - const giveaway = await prisma.radioGiveaways - .findUnique({ where: { id: giveawayId } }) - .catch(() => null); + const [giveaway] = await db + .select() + .from(RadioGiveaways) + .where(eq(RadioGiveaways.id, giveawayId)) + .limit(1) + .catch(() => []); if (!giveaway) notFound(); diff --git a/src/app/(site)/radio/giveaways/page.tsx b/src/app/(site)/radio/giveaways/page.tsx index 2d6eb65d..3f9cca7b 100644 --- a/src/app/(site)/radio/giveaways/page.tsx +++ b/src/app/(site)/radio/giveaways/page.tsx @@ -1,29 +1,29 @@ +import { desc, eq } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; +import { db, RadioGiveaways } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const revalidate = 300; export default async function RadioGiveawaysPage() { const t = await getTranslations("pages.radioGiveaways"); - const giveaways = await prisma.radioGiveaways - .findMany({ - where: { isActive: true }, - orderBy: { startDate: "desc" }, - take: 50, - select: { - id: true, - title: true, - prize: true, - prizeAmount: true, - prizeCurrency: true, - startDate: true, - endDate: true, - }, + const giveaways = await db + .select({ + id: RadioGiveaways.id, + title: RadioGiveaways.title, + prize: RadioGiveaways.prize, + prizeAmount: RadioGiveaways.prizeAmount, + prizeCurrency: RadioGiveaways.prizeCurrency, + startDate: RadioGiveaways.startDate, + endDate: RadioGiveaways.endDate, }) + .from(RadioGiveaways) + .where(eq(RadioGiveaways.isActive, true)) + .orderBy(desc(RadioGiveaways.startDate)) + .limit(50) .catch(() => []); return ( diff --git a/src/app/(site)/radio/leaderboard/page.tsx b/src/app/(site)/radio/leaderboard/page.tsx index 9862cacc..401765a6 100644 --- a/src/app/(site)/radio/leaderboard/page.tsx +++ b/src/app/(site)/radio/leaderboard/page.tsx @@ -1,18 +1,13 @@ +import { sql } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui"; +import { db } from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; -import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; export const revalidate = 300; -// The radio_listener_points table in the live AtomCMS DB carries user_id + -// points columns, but the generated Prisma model is a stub (id/timestamps only), -// so it has no `points`/`userId` delegate fields. We therefore read it via -// $queryRaw, joining users for the username/look — the same approach used for -// the keyless/@@ignore'd emulator tables (see src/app/admin/logs). MySQL may -// return BigInt columns as bigint, so values are coerced defensively. type LeaderRow = { username: string; look: string; @@ -23,14 +18,14 @@ async function loadRows(): Promise< { username: string; look: string; points: number }[] > { try { - const rows = await prisma.$queryRaw` - SELECT u.username AS username, u.look AS look, p.points AS points - FROM radio_listener_points p - INNER JOIN users u ON u.id = p.user_id - ORDER BY p.points DESC - LIMIT 50 - `; - return rows.map((r) => ({ + const [rows] = (await db.execute(sql` + SELECT u.username AS username, u.look AS look, p.points AS points + FROM radio_listener_points p + INNER JOIN users u ON u.id = p.user_id + ORDER BY p.points DESC + LIMIT 50 + `)) as unknown as [LeaderRow[], unknown]; + return (rows ?? []).map((r) => ({ username: r.username, look: r.look, points: Number(r.points), diff --git a/src/app/(site)/radio/page.tsx b/src/app/(site)/radio/page.tsx index 722bee2e..25e790c6 100644 --- a/src/app/(site)/radio/page.tsx +++ b/src/app/(site)/radio/page.tsx @@ -1,7 +1,8 @@ +import { asc, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui"; -import { prisma } from "@/lib/prisma"; +import { db, RadioSchedules, User } from "@/lib/db"; import { siteSettings } from "@/lib/services/site-settings"; // Radio config + today's schedule are read live from the DB / settings table. @@ -78,11 +79,11 @@ export default async function RadioPage() { // ── Today's schedule (isolated; a DB hiccup degrades to empty) ── const today = WEEKDAYS[new Date().getUTCDay()] ?? ""; - const schedules = await prisma.radioSchedules - .findMany({ - where: { isActive: true }, - orderBy: { startTime: "asc" }, - }) + const schedules = await db + .select() + .from(RadioSchedules) + .where(eq(RadioSchedules.isActive, true)) + .orderBy(asc(RadioSchedules.startTime)) .catch(() => []); const todaysSlots = schedules.filter( @@ -94,11 +95,10 @@ export default async function RadioPage() { // lookup (DJ ids comfortably fit in a JS-safe integer). const djIds = Array.from(new Set(todaysSlots.map((s) => Number(s.userId)))); const djs = djIds.length - ? await prisma.user - .findMany({ - where: { id: { in: djIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, djIds)) .catch(() => []) : []; const djById = new Map(djs.map((d) => [d.id, d.username])); diff --git a/src/app/(site)/radio/requests/page.tsx b/src/app/(site)/radio/requests/page.tsx index 08862804..0a0c141b 100644 --- a/src/app/(site)/radio/requests/page.tsx +++ b/src/app/(site)/radio/requests/page.tsx @@ -1,10 +1,11 @@ +import { desc, inArray } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import type { CSSProperties } from "react"; import { submitRequest } from "@/actions/radio-requests"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { db, RadioSongRequests, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -47,11 +48,11 @@ export default async function RadioRequestsPage({ : null; const [requests, session] = await Promise.all([ - prisma.radioSongRequests - .findMany({ - orderBy: { createdAt: "desc" }, - take: 50, - }) + db + .select() + .from(RadioSongRequests) + .orderBy(desc(RadioSongRequests.createdAt)) + .limit(50) .catch(() => []), auth(), ]); @@ -60,11 +61,10 @@ export default async function RadioRequestsPage({ const userIds = Array.from(new Set(requests.map((r) => Number(r.userId)))); const users = userIds.length - ? await prisma.user - .findMany({ - where: { id: { in: userIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, userIds)) .catch(() => []) : []; const userById = new Map(users.map((u) => [u.id, u.username])); diff --git a/src/app/(site)/radio/schedule/page.tsx b/src/app/(site)/radio/schedule/page.tsx index a8d97257..60474b37 100644 --- a/src/app/(site)/radio/schedule/page.tsx +++ b/src/app/(site)/radio/schedule/page.tsx @@ -1,6 +1,7 @@ +import { asc, eq, inArray } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; -import { prisma } from "@/lib/prisma"; +import { db, RadioSchedules, User } from "@/lib/db"; export const revalidate = 300; @@ -35,21 +36,20 @@ type Slot = { export default async function RadioSchedulePage() { const t = await getTranslations("pages.radioSchedule"); - const schedules = await prisma.radioSchedules - .findMany({ - where: { isActive: true }, - orderBy: { startTime: "asc" }, - }) + const schedules = await db + .select() + .from(RadioSchedules) + .where(eq(RadioSchedules.isActive, true)) + .orderBy(asc(RadioSchedules.startTime)) .catch(() => []); // Resolve DJ usernames. schedule.userId is BigInt; User.id is Int — convert. const djIds = Array.from(new Set(schedules.map((s) => Number(s.userId)))); const djs = djIds.length - ? await prisma.user - .findMany({ - where: { id: { in: djIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, djIds)) .catch(() => []) : []; const djById = new Map(djs.map((u) => [u.id, u.username])); diff --git a/src/app/(site)/radio/shouts/page.tsx b/src/app/(site)/radio/shouts/page.tsx index 74678cde..6ced52d2 100644 --- a/src/app/(site)/radio/shouts/page.tsx +++ b/src/app/(site)/radio/shouts/page.tsx @@ -1,11 +1,12 @@ +import { desc, inArray } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import type { CSSProperties } from "react"; import { postShout } from "@/actions/radio-shouts"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { db, RadioShouts, User } 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"; @@ -37,11 +38,11 @@ export default async function RadioShoutsPage({ const { posted, error } = await searchParams; const [shouts, imagerBase, session] = await Promise.all([ - prisma.radioShouts - .findMany({ - orderBy: { createdAt: "desc" }, - take: 50, - }) + db + .select() + .from(RadioShouts) + .orderBy(desc(RadioShouts.createdAt)) + .limit(50) .catch(() => []), siteSettings.get( "habbo_imaging_url", @@ -55,11 +56,14 @@ export default async function RadioShoutsPage({ // Resolve author usernames/looks. shout.userId is BigInt; User.id is Int. const authorIds = Array.from(new Set(shouts.map((s) => Number(s.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])); diff --git a/src/app/(site)/rankings/page.tsx b/src/app/(site)/rankings/page.tsx index 2fca74ca..4fd1d0eb 100644 --- a/src/app/(site)/rankings/page.tsx +++ b/src/app/(site)/rankings/page.tsx @@ -1,3 +1,4 @@ +import { desc } from "drizzle-orm"; import Image from "next/image"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; @@ -7,8 +8,8 @@ import { OnlineBadge, RankBadge, } from "@/components/public/ui"; +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 revalidate = 300; @@ -27,17 +28,17 @@ export default async function RankingsPage() { const t = await getTranslations("pages.rankings"); let users: TopUser[] = []; try { - users = await prisma.user.findMany({ - select: { - username: true, - look: true, - credits: true, - rank: true, - online: true, - }, - orderBy: { credits: "desc" }, - take: 12, - }); + users = await db + .select({ + username: User.username, + look: User.look, + credits: User.credits, + rank: User.rank, + online: User.online, + }) + .from(User) + .orderBy(desc(User.credits)) + .limit(12); } catch { users = []; } diff --git a/src/app/(site)/rares/[category]/page.tsx b/src/app/(site)/rares/[category]/page.tsx index 88fb188d..853330a7 100644 --- a/src/app/(site)/rares/[category]/page.tsx +++ b/src/app/(site)/rares/[category]/page.tsx @@ -1,8 +1,9 @@ +import { asc, eq } from "drizzle-orm"; import Link from "next/link"; import { notFound } from "next/navigation"; 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; @@ -52,10 +53,16 @@ export default async function RareCategoryPage({ let cat: { id: bigint; name: string; badge: string } | null = null; try { - cat = await prisma.websiteRareValueCategories.findUnique({ - where: { id: categoryId }, - select: { id: true, name: true, badge: true }, - }); + const [row] = await db + .select({ + id: WebsiteRareValueCategories.id, + name: WebsiteRareValueCategories.name, + badge: WebsiteRareValueCategories.badge, + }) + .from(WebsiteRareValueCategories) + .where(eq(WebsiteRareValueCategories.id, categoryId)) + .limit(1); + cat = row ?? null; } catch { cat = null; } @@ -64,19 +71,19 @@ export default async function RareCategoryPage({ let rares: RareRow[] = []; try { - rares = await prisma.websiteRareValues.findMany({ - where: { categoryId: categoryId }, - orderBy: { name: "asc" }, - select: { - id: true, - name: true, - creditValue: true, - currencyValue: true, - currencyType: true, - furnitureIcon: true, - itemId: true, - }, - }); + rares = await db + .select({ + id: WebsiteRareValues.id, + name: WebsiteRareValues.name, + creditValue: WebsiteRareValues.creditValue, + currencyValue: WebsiteRareValues.currencyValue, + currencyType: WebsiteRareValues.currencyType, + furnitureIcon: WebsiteRareValues.furnitureIcon, + itemId: WebsiteRareValues.itemId, + }) + .from(WebsiteRareValues) + .where(eq(WebsiteRareValues.categoryId, categoryId)) + .orderBy(asc(WebsiteRareValues.name)); } catch { rares = []; }