refactor(db): migrate app pages and APIs from Prisma facade to Drizzle

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 14:15:12 +02:00
1 parent 53b350057d
commit 7c39aef5d9
40 files changed
+1124 -692

No files matched your search

+25 -10
View File
@@ -1,12 +1,18 @@
import { desc, eq, inArray } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { applyStaff } from "@/actions/applications"; import { applyStaff } from "@/actions/applications";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import {
db,
WebsiteOpenPositions,
WebsiteStaffApplications,
WebsiteTeams,
} from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -59,8 +65,10 @@ export default async function ApplyStaffPage({
// ── Open positions ────────────────────────────────────────── // ── Open positions ──────────────────────────────────────────
// Each query is isolated so a DB hiccup degrades that section to empty // Each query is isolated so a DB hiccup degrades that section to empty
// rather than 500-ing the whole page. // rather than 500-ing the whole page.
const positions = await prisma.websiteOpenPositions const positions = await db
.findMany({ orderBy: { createdAt: "desc" } }) .select()
.from(WebsiteOpenPositions)
.orderBy(desc(WebsiteOpenPositions.createdAt))
.catch(() => []); .catch(() => []);
// Resolve display info for each position's rank. The rich rank fields // 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. // the same rank id the position's permission_id points at.
const rankIds = Array.from(new Set(positions.map((p) => p.permissionId))); const rankIds = Array.from(new Set(positions.map((p) => p.permissionId)));
const teams = rankIds.length const teams = rankIds.length
? await prisma.websiteTeams ? await db
.findMany({ .select()
where: { id: { in: rankIds.map((r) => BigInt(r)) } }, .from(WebsiteTeams)
}) .where(
inArray(
WebsiteTeams.id,
rankIds.map((r) => BigInt(r)),
),
)
.catch(() => []) .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). // Ranks this user has already applied for (block re-applying).
const myApps = await prisma.websiteStaffApplications const myApps = await db
.findMany({ where: { userId }, select: { rankId: true } }) .select({ rankId: WebsiteStaffApplications.rankId })
.from(WebsiteStaffApplications)
.where(eq(WebsiteStaffApplications.userId, userId))
.catch(() => []); .catch(() => []);
const appliedRankIds = new Set(myApps.map((a) => a.rankId)); const appliedRankIds = new Set(myApps.map((a) => a.rankId));
+11 -8
View File
@@ -1,11 +1,12 @@
import { asc, eq } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { applyTeam } from "@/actions/applications"; import { applyTeam } from "@/actions/applications";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, WebsiteStaffApplications, WebsiteTeams } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -59,17 +60,19 @@ export default async function ApplyTeamPage({
// The Prisma slice exposes teams directly (no separate team-position table // The Prisma slice exposes teams directly (no separate team-position table
// with position_kind), so the team application concept lists website_teams. // with position_kind), so the team application concept lists website_teams.
// Hidden ranks are excluded from the public apply page. // Hidden ranks are excluded from the public apply page.
const teams = await prisma.websiteTeams const teams = await db
.findMany({ .select()
where: { hiddenRank: false }, .from(WebsiteTeams)
orderBy: { rankName: "asc" }, .where(eq(WebsiteTeams.hiddenRank, false))
}) .orderBy(asc(WebsiteTeams.rankName))
.catch(() => []); .catch(() => []);
// Teams this user has already applied to. Team applications reuse the staff // Teams this user has already applied to. Team applications reuse the staff
// applications table with rank_id carrying the team id (the team flag). // applications table with rank_id carrying the team id (the team flag).
const myApps = await prisma.websiteStaffApplications const myApps = await db
.findMany({ where: { userId }, select: { rankId: true } }) .select({ rankId: WebsiteStaffApplications.rankId })
.from(WebsiteStaffApplications)
.where(eq(WebsiteStaffApplications.userId, userId))
.catch(() => []); .catch(() => []);
const appliedTeamIds = new Set(myApps.map((a) => a.rankId)); const appliedTeamIds = new Set(myApps.map((a) => a.rankId));
+6 -5
View File
@@ -1,6 +1,7 @@
import { asc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { prisma } from "@/lib/prisma"; import { db, WebsiteBadges } from "@/lib/db";
export const revalidate = 300; export const revalidate = 300;
@@ -12,10 +13,10 @@ const BADGE_IMG_BASE = "https://images.habbo.com/c_images/album1584";
export default async function BadgesPage() { export default async function BadgesPage() {
const t = await getTranslations("pages.badges"); const t = await getTranslations("pages.badges");
const badges = await prisma.websiteBadges const badges = await db
.findMany({ .select()
orderBy: { badgeName: "asc" }, .from(WebsiteBadges)
}) .orderBy(asc(WebsiteBadges.badgeName))
.catch(() => []); .catch(() => []);
return ( return (
+18 -10
View File
@@ -1,8 +1,9 @@
import { and, desc, eq, gt, or } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard } from "@/components/public/ui"; import { ContentCard } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { activeBanWhere, unixNow } from "@/lib/bans"; import { unixNow } from "@/lib/bans";
import { prisma } from "@/lib/prisma"; import { Ban, db } from "@/lib/db";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -16,14 +17,21 @@ export default async function BannedPage() {
let hasBan = false; let hasBan = false;
if (session?.user?.id) { if (session?.user?.id) {
try { try {
const ban = await prisma.ban.findFirst({ const now = unixNow();
where: { const [ban] = await db
userId: Number(session.user.id), .select({
...activeBanWhere(unixNow()), banReason: Ban.banReason,
}, banExpire: Ban.banExpire,
orderBy: { timestamp: "desc" }, })
select: { banReason: true, banExpire: true }, .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) { if (ban) {
hasBan = true; hasBan = true;
reason = ban.banReason; reason = ban.banReason;
+17 -10
View File
@@ -1,8 +1,9 @@
import { desc, eq } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { buyBadge } from "@/actions/draw-badge"; import { buyBadge } from "@/actions/draw-badge";
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma"; import { db, User, WebsiteDrawbadges } from "@/lib/db";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
// Reads the live users + website_drawbadges tables and writes credits/badges on // Reads the live users + website_drawbadges tables and writes credits/badges on
@@ -66,21 +67,27 @@ export default async function DrawBadgePage({
} }
try { try {
const buyer = await prisma.user.findUnique({ const [buyer] = await db
where: { id: Number(session.user.id) }, .select({ credits: User.credits })
select: { credits: true }, .from(User)
}); .where(eq(User.id, Number(session.user.id)))
.limit(1);
credits = buyer?.credits ?? 0; credits = buyer?.credits ?? 0;
} catch { } catch {
credits = 0; credits = 0;
} }
try { try {
badges = await prisma.websiteDrawbadges.findMany({ badges = await db
where: { published: true }, .select({
select: { id: true, badgeUrl: true, badgeName: true, badgeDesc: true }, id: WebsiteDrawbadges.id,
orderBy: { id: "desc" }, badgeUrl: WebsiteDrawbadges.badgeUrl,
}); badgeName: WebsiteDrawbadges.badgeName,
badgeDesc: WebsiteDrawbadges.badgeDesc,
})
.from(WebsiteDrawbadges)
.where(eq(WebsiteDrawbadges.published, true))
.orderBy(desc(WebsiteDrawbadges.id));
} catch { } catch {
badges = []; badges = [];
} }
+82 -25
View File
@@ -1,11 +1,20 @@
import { and, asc, count, eq, inArray } from "drizzle-orm";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import {
db,
User,
WebsiteEvent,
WebsiteEventPrize,
WebsiteEventRegistration,
WebsiteEventType,
WebsiteEventWinner,
} from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
import { EventRegisterButton } from "./event-register-button"; import { EventRegisterButton } from "./event-register-button";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -30,47 +39,95 @@ export default async function EventDetailPage({
const session = await auth(); const session = await auth();
const userId = Number(session?.user?.id); const userId = Number(session?.user?.id);
const event = await prisma.websiteEvent const [eventRow] = await db
.findUnique({ .select({
where: { id: eventId }, id: WebsiteEvent.id,
include: { title: WebsiteEvent.title,
type: true, description: WebsiteEvent.description,
prizes: { orderBy: { position: "asc" } }, status: WebsiteEvent.status,
winners: { orderBy: { position: "asc" } }, image: WebsiteEvent.image,
_count: { select: { registrations: true } }, 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 ( if (
!event || !eventRow ||
(event.status !== "published" && event.status !== "completed") (eventRow.status !== "published" && eventRow.status !== "completed")
) { ) {
notFound(); 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 = const alreadyRegistered =
Number.isInteger(userId) && userId > 0 Number.isInteger(userId) && userId > 0
? Boolean( ? Boolean(
await prisma.websiteEventRegistration (
.findUnique({ await db
where: { eventId_userId: { eventId: event.id, userId } }, .select({ id: WebsiteEventRegistration.id })
}) .from(WebsiteEventRegistration)
.catch(() => null), .where(
and(
eq(WebsiteEventRegistration.eventId, event.id),
eq(WebsiteEventRegistration.userId, userId),
),
)
.limit(1)
.catch(() => [])
)[0],
) )
: false; : false;
const winnerIds = event.winners.map((w) => w.userId); const winnerIds = event.winners.map((w) => w.userId);
const winners = const winnerUsers =
winnerIds.length > 0 winnerIds.length > 0
? await prisma.user ? await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: winnerIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, winnerIds))
})
.catch(() => []) .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 = const isFull =
event.maxPlayers != null && event._count.registrations >= event.maxPlayers; event.maxPlayers != null && event._count.registrations >= event.maxPlayers;
+51 -10
View File
@@ -1,10 +1,16 @@
import { count, desc, eq, inArray } from "drizzle-orm";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import {
db,
WebsiteEvent,
WebsiteEventRegistration,
WebsiteEventType,
} from "@/lib/db";
import { excerpt, slugify } from "@/lib/format"; import { excerpt, slugify } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 60; export const revalidate = 60;
@@ -13,18 +19,53 @@ export const metadata = { title: "Events" };
export default async function EventsPage() { export default async function EventsPage() {
const t = await getTranslations("pages.events"); const t = await getTranslations("pages.events");
const events = await prisma.websiteEvent const eventRows = await db
.findMany({ .select({
where: { status: { in: ["published", "completed"] } }, id: WebsiteEvent.id,
orderBy: { startsAt: "desc" }, title: WebsiteEvent.title,
take: 50, description: WebsiteEvent.description,
include: { status: WebsiteEvent.status,
type: { select: { name: true, color: true, slug: true } }, image: WebsiteEvent.image,
_count: { select: { registrations: true } }, 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(() => []); .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 ( return (
<main style={{ display: "grid", gap: "1.5rem" }}> <main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard icon="📅" title={t("title")} subtitle={t("subtitle")} /> <ContentCard icon="📅" title={t("title")} subtitle={t("subtitle")} />
+23 -18
View File
@@ -1,3 +1,4 @@
import { asc, eq, inArray, or } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
@@ -5,8 +6,8 @@ import type { CSSProperties } from "react";
import { removeFriendship } from "@/actions/messenger"; import { removeFriendship } from "@/actions/messenger";
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui"; import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, MessengerFriendships, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format"; import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -45,13 +46,18 @@ export default async function FriendsPage({
// Friendships are stored as TWO directional rows (user_one_id→user_two_id // Friendships are stored as TWO directional rows (user_one_id→user_two_id
// and the reverse). Read both directions, then dedupe the friend ids. // and the reverse). Read both directions, then dedupe the friend ids.
const friendships = await prisma.messengerFriendships const friendships = await db
.findMany({ .select({
where: { userOneId: MessengerFriendships.userOneId,
OR: [{ userOneId: userId }, { userTwoId: userId }], userTwoId: MessengerFriendships.userTwoId,
},
select: { userOneId: true, userTwoId: true },
}) })
.from(MessengerFriendships)
.where(
or(
eq(MessengerFriendships.userOneId, userId),
eq(MessengerFriendships.userTwoId, userId),
),
)
.catch(() => []); .catch(() => []);
const friendIds = Array.from( const friendIds = Array.from(
@@ -64,18 +70,17 @@ export default async function FriendsPage({
const [friends, imagerBase] = await Promise.all([ const [friends, imagerBase] = await Promise.all([
friendIds.length friendIds.length
? prisma.user ? db
.findMany({ .select({
where: { id: { in: friendIds } }, id: User.id,
select: { username: User.username,
id: true, look: User.look,
username: true, motto: User.motto,
look: true, online: User.online,
motto: true,
online: true,
},
orderBy: { username: "asc" },
}) })
.from(User)
.where(inArray(User.id, friendIds))
.orderBy(asc(User.username))
.catch(() => []) .catch(() => [])
: Promise.resolve([]), : Promise.resolve([]),
siteSettings.get( siteSettings.get(
@@ -1,3 +1,4 @@
import { and, asc, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
@@ -5,8 +6,14 @@ import type { CSSProperties } from "react";
import { replyToThread } from "@/actions/social"; import { replyToThread } from "@/actions/social";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import {
db,
Guilds,
GuildsForumsComments,
GuildsForumsThreads,
User,
} from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 60; export const revalidate = 60;
@@ -63,22 +70,34 @@ export default async function GuildForumThreadPage({
}[] = []; }[] = [];
try { try {
[guild, thread] = await Promise.all([ const [guildRow, threadRow] = await Promise.all([
prisma.guilds.findUnique({ db
where: { id: guildId }, .select({ id: Guilds.id, name: Guilds.name })
select: { id: true, name: true }, .from(Guilds)
}), .where(eq(Guilds.id, guildId))
prisma.guildsForumsThreads.findFirst({ .limit(1)
where: { id: threadId, guildId, state: 0 }, .then((rows) => rows[0] ?? null),
select: { db
id: true, .select({
subject: true, id: GuildsForumsThreads.id,
locked: true, subject: GuildsForumsThreads.subject,
pinned: true, locked: GuildsForumsThreads.locked,
openerId: true, 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 { } catch {
guild = null; guild = null;
thread = null; thread = null;
@@ -87,16 +106,24 @@ export default async function GuildForumThreadPage({
if (!guild || !thread) notFound(); if (!guild || !thread) notFound();
try { try {
posts = await prisma.guildsForumsComments.findMany({ posts = await db
where: { threadId: thread.id, state: 0 }, .select({
orderBy: [{ createdAt: "asc" }, { id: "asc" }], id: GuildsForumsComments.id,
select: { userId: GuildsForumsComments.userId,
id: true, message: GuildsForumsComments.message,
userId: true, createdAt: GuildsForumsComments.createdAt,
message: true, })
createdAt: true, .from(GuildsForumsComments)
}, .where(
}); and(
eq(GuildsForumsComments.threadId, thread.id),
eq(GuildsForumsComments.state, 0),
),
)
.orderBy(
asc(GuildsForumsComments.createdAt),
asc(GuildsForumsComments.id),
);
} catch { } catch {
posts = []; posts = [];
} }
@@ -107,10 +134,10 @@ export default async function GuildForumThreadPage({
let users: { id: number; username: string }[] = []; let users: { id: number; username: string }[] = [];
try { try {
users = authorIds.length users = authorIds.length
? await prisma.user.findMany({ ? await db
where: { id: { in: authorIds } }, .select({ id: User.id, username: User.username })
select: { id: true, username: true }, .from(User)
}) .where(inArray(User.id, authorIds))
: []; : [];
} catch { } catch {
users = []; users = [];
@@ -1,3 +1,4 @@
import { eq } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
@@ -5,7 +6,7 @@ import type { CSSProperties } from "react";
import { postThread } from "@/actions/social"; import { postThread } from "@/actions/social";
import { ContentCard } from "@/components/public/ui"; import { ContentCard } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma"; import { db, Guilds } from "@/lib/db";
export const dynamic = "force-dynamic"; 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. // Guild header (read-only). A DB hiccup degrades to notFound rather than 500.
let guild: { id: number; name: string } | null = null; let guild: { id: number; name: string } | null = null;
try { try {
guild = await prisma.guilds.findUnique({ const [row] = await db
where: { id: guildId }, .select({ id: Guilds.id, name: Guilds.name })
select: { id: true, name: true }, .from(Guilds)
}); .where(eq(Guilds.id, guildId))
.limit(1);
guild = row ?? null;
} catch { } catch {
guild = null; guild = null;
} }
+41 -25
View File
@@ -1,10 +1,11 @@
import { and, desc, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, Guilds, GuildsForumsThreads, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 300; 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. // Guild header (read-only). A DB hiccup degrades to notFound rather than 500.
let guild: { id: number; name: string; badge: string } | null = null; let guild: { id: number; name: string; badge: string } | null = null;
try { try {
guild = await prisma.guilds.findUnique({ const [row] = await db
where: { id: guildId }, .select({
select: { id: true, name: true, badge: true }, id: Guilds.id,
}); name: Guilds.name,
badge: Guilds.badge,
})
.from(Guilds)
.where(eq(Guilds.id, guildId))
.limit(1);
guild = row ?? null;
} catch { } catch {
guild = null; guild = null;
} }
@@ -66,21 +73,30 @@ export default async function GuildForumPage({
// Threads for this guild. Pinned first, then most-recently active. // Threads for this guild. Pinned first, then most-recently active.
let threads: ThreadRow[] = []; let threads: ThreadRow[] = [];
try { try {
threads = await prisma.guildsForumsThreads.findMany({ threads = await db
where: { guildId: guild.id, state: 0 }, .select({
orderBy: [{ pinned: "desc" }, { updatedAt: "desc" }, { id: "desc" }], id: GuildsForumsThreads.id,
take: 100, openerId: GuildsForumsThreads.openerId,
select: { subject: GuildsForumsThreads.subject,
id: true, postsCount: GuildsForumsThreads.postsCount,
openerId: true, createdAt: GuildsForumsThreads.createdAt,
subject: true, updatedAt: GuildsForumsThreads.updatedAt,
postsCount: true, pinned: GuildsForumsThreads.pinned,
createdAt: true, locked: GuildsForumsThreads.locked,
updatedAt: true, })
pinned: true, .from(GuildsForumsThreads)
locked: true, .where(
}, and(
}); eq(GuildsForumsThreads.guildId, guild.id),
eq(GuildsForumsThreads.state, 0),
),
)
.orderBy(
desc(GuildsForumsThreads.pinned),
desc(GuildsForumsThreads.updatedAt),
desc(GuildsForumsThreads.id),
)
.limit(100);
} catch { } catch {
threads = []; threads = [];
} }
@@ -88,16 +104,16 @@ export default async function GuildForumPage({
// Resolve opener usernames in one query (no relation exists in the schema, so // 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. // join the users table manually by id). Falls back to the raw author id.
const openerIds = Array.from( 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 }[] = []; let users: { id: number; username: string }[] = [];
try { try {
users = openerIds.length users = openerIds.length
? await prisma.user.findMany({ ? await db
where: { id: { in: openerIds } }, .select({ id: User.id, username: User.username })
select: { id: true, username: true }, .from(User)
}) .where(inArray(User.id, openerIds))
: []; : [];
} catch { } catch {
users = []; users = [];
+33 -22
View File
@@ -1,10 +1,11 @@
import { asc, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
import { db, Guilds, GuildsMembers, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format"; import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300; export const revalidate = 300;
@@ -39,17 +40,19 @@ export default async function GuildPage({
let guild: GuildView | null = null; let guild: GuildView | null = null;
try { try {
guild = await prisma.guilds.findUnique({ const [row] = await db
where: { id: guildId }, .select({
select: { id: Guilds.id,
id: true, name: Guilds.name,
name: true, description: Guilds.description,
description: true, badge: Guilds.badge,
badge: true, userId: Guilds.userId,
userId: true, dateCreated: Guilds.dateCreated,
dateCreated: true, })
}, .from(Guilds)
}); .where(eq(Guilds.id, guildId))
.limit(1);
guild = row ?? null;
} catch { } catch {
guild = null; guild = null;
} }
@@ -60,12 +63,15 @@ export default async function GuildPage({
// join the users table manually by id. // join the users table manually by id.
let memberRows: { userId: number; memberSince: number }[] = []; let memberRows: { userId: number; memberSince: number }[] = [];
try { try {
memberRows = await prisma.guildsMembers.findMany({ memberRows = await db
where: { guildId: guild.id }, .select({
orderBy: { memberSince: "asc" }, userId: GuildsMembers.userId,
take: 200, memberSince: GuildsMembers.memberSince,
select: { userId: true, memberSince: true }, })
}); .from(GuildsMembers)
.where(eq(GuildsMembers.guildId, guild.id))
.orderBy(asc(GuildsMembers.memberSince))
.limit(200);
} catch { } catch {
memberRows = []; memberRows = [];
} }
@@ -80,10 +86,15 @@ export default async function GuildPage({
[]; [];
try { try {
users = memberIds.length users = memberIds.length
? await prisma.user.findMany({ ? await db
where: { id: { in: memberIds } }, .select({
select: { id: true, username: true, look: true, motto: true }, id: User.id,
}) username: User.username,
look: User.look,
motto: User.motto,
})
.from(User)
.where(inArray(User.id, memberIds))
: []; : [];
} catch { } catch {
users = []; users = [];
+12 -7
View File
@@ -1,8 +1,9 @@
import { desc } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, Guilds } from "@/lib/db";
import { excerpt } from "@/lib/format"; import { excerpt } from "@/lib/format";
import { prisma } from "@/lib/prisma";
export const revalidate = 300; export const revalidate = 300;
@@ -17,12 +18,16 @@ type GuildCard = {
async function getGuilds(): Promise<GuildCard[]> { async function getGuilds(): Promise<GuildCard[]> {
try { try {
const guilds = await prisma.guilds.findMany({ return await db
orderBy: { id: "desc" }, .select({
take: 40, id: Guilds.id,
select: { id: true, name: true, description: true, badge: true }, name: Guilds.name,
}); description: Guilds.description,
return guilds; badge: Guilds.badge,
})
.from(Guilds)
.orderBy(desc(Guilds.id))
.limit(40);
} catch { } catch {
return []; return [];
} }
+29 -22
View File
@@ -1,10 +1,11 @@
import { asc, eq, ne } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard } from "@/components/public/ui"; import { ContentCard } from "@/components/public/ui";
import { SanitizedHtml } from "@/components/shared/sanitized-html"; import { SanitizedHtml } from "@/components/shared/sanitized-html";
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { prisma } from "@/lib/prisma";
import { sanitize } from "@/lib/sanitize"; import { sanitize } from "@/lib/sanitize";
export const revalidate = 300; export const revalidate = 300;
@@ -54,21 +55,23 @@ export default async function HelpCategoryPage({
let cat: HelpCategory | null = null; let cat: HelpCategory | null = null;
try { try {
cat = await prisma.websiteHelpCenterCategories.findUnique({ const [row] = await db
where: { id: categoryId }, .select({
select: { id: WebsiteHelpCenterCategories.id,
id: true, name: WebsiteHelpCenterCategories.name,
name: true, content: WebsiteHelpCenterCategories.content,
content: true, position: WebsiteHelpCenterCategories.position,
position: true, imageUrl: WebsiteHelpCenterCategories.imageUrl,
imageUrl: true, buttonText: WebsiteHelpCenterCategories.buttonText,
buttonText: true, buttonUrl: WebsiteHelpCenterCategories.buttonUrl,
buttonUrl: true, buttonColor: WebsiteHelpCenterCategories.buttonColor,
buttonColor: true, buttonBorderColor: WebsiteHelpCenterCategories.buttonBorderColor,
buttonBorderColor: true, smallBox: WebsiteHelpCenterCategories.smallBox,
smallBox: true, })
}, .from(WebsiteHelpCenterCategories)
}); .where(eq(WebsiteHelpCenterCategories.id, categoryId))
.limit(1);
cat = row ?? null;
} catch { } catch {
cat = null; cat = null;
} }
@@ -81,12 +84,16 @@ export default async function HelpCategoryPage({
// query: a DB hiccup degrades this list to empty rather than 500-ing. // query: a DB hiccup degrades this list to empty rather than 500-ing.
let siblings: HelpCategoryLink[] = []; let siblings: HelpCategoryLink[] = [];
try { try {
siblings = await prisma.websiteHelpCenterCategories.findMany({ siblings = await db
where: { id: { not: categoryId } }, .select({
orderBy: { position: "asc" }, id: WebsiteHelpCenterCategories.id,
select: { id: true, name: true, position: true }, name: WebsiteHelpCenterCategories.name,
take: 50, position: WebsiteHelpCenterCategories.position,
}); })
.from(WebsiteHelpCenterCategories)
.where(ne(WebsiteHelpCenterCategories.id, categoryId))
.orderBy(asc(WebsiteHelpCenterCategories.position))
.limit(50);
} catch { } catch {
siblings = []; siblings = [];
} }
+46 -23
View File
@@ -1,7 +1,13 @@
import { asc } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { prisma } from "@/lib/prisma"; import {
db,
WebsiteHelpCenterCategories,
WebsiteRuleCategories,
WebsiteRules,
} from "@/lib/db";
export const revalidate = 300; export const revalidate = 300;
@@ -40,40 +46,57 @@ export default async function HelpCenterPage() {
let categories: HelpCategory[] = []; let categories: HelpCategory[] = [];
try { try {
categories = await prisma.websiteHelpCenterCategories.findMany({ categories = await db
orderBy: [{ position: "asc" }, { id: "asc" }], .select({
select: { id: WebsiteHelpCenterCategories.id,
id: true, name: WebsiteHelpCenterCategories.name,
name: true, content: WebsiteHelpCenterCategories.content,
content: true, imageUrl: WebsiteHelpCenterCategories.imageUrl,
imageUrl: true, buttonText: WebsiteHelpCenterCategories.buttonText,
buttonText: true, buttonUrl: WebsiteHelpCenterCategories.buttonUrl,
buttonUrl: true, buttonColor: WebsiteHelpCenterCategories.buttonColor,
buttonColor: true, buttonBorderColor: WebsiteHelpCenterCategories.buttonBorderColor,
buttonBorderColor: true, smallBox: WebsiteHelpCenterCategories.smallBox,
smallBox: true, })
}, .from(WebsiteHelpCenterCategories)
}); .orderBy(
asc(WebsiteHelpCenterCategories.position),
asc(WebsiteHelpCenterCategories.id),
);
} catch { } catch {
// DB unavailable — render the shell without help categories. // DB unavailable — render the shell without help categories.
} }
let ruleCategories: RuleCategory[] = []; let ruleCategories: RuleCategory[] = [];
try { try {
ruleCategories = await prisma.websiteRuleCategories.findMany({ ruleCategories = await db
orderBy: { id: "asc" }, .select({
select: { id: true, name: true, description: true, badge: true }, id: WebsiteRuleCategories.id,
}); name: WebsiteRuleCategories.name,
description: WebsiteRuleCategories.description,
badge: WebsiteRuleCategories.badge,
})
.from(WebsiteRuleCategories)
.orderBy(asc(WebsiteRuleCategories.id));
} catch { } catch {
// DB unavailable — render the shell without rule categories. // DB unavailable — render the shell without rule categories.
} }
let rules: Rule[] = []; let rules: Rule[] = [];
try { try {
rules = await prisma.websiteRules.findMany({ rules = await db
orderBy: [{ categoryId: "asc" }, { paragraph: "asc" }, { id: "asc" }], .select({
select: { id: true, categoryId: true, paragraph: true, rule: true }, 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 { } catch {
// DB unavailable — render the shell without rules. // DB unavailable — render the shell without rules.
} }
+34 -26
View File
@@ -1,3 +1,4 @@
import { asc, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
@@ -6,8 +7,13 @@ import { closeHelpTicket, replyHelpTicket } from "@/actions/help-tickets";
import { ContentCard } from "@/components/public/ui"; import { ContentCard } from "@/components/public/ui";
import { positiveBigInt } from "@/lib/api"; import { positiveBigInt } from "@/lib/api";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import {
db,
User,
WebsiteHelpCenterTicketReplies,
WebsiteHelpCenterTickets,
} from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -67,17 +73,19 @@ export default async function HelpTicketDetailPage({
}[] = []; }[] = [];
try { try {
ticket = await prisma.websiteHelpCenterTickets.findUnique({ const [row] = await db
where: { id: ticketId }, .select({
select: { id: WebsiteHelpCenterTickets.id,
id: true, userId: WebsiteHelpCenterTickets.userId,
userId: true, title: WebsiteHelpCenterTickets.title,
title: true, content: WebsiteHelpCenterTickets.content,
content: true, open: WebsiteHelpCenterTickets.open,
open: true, createdAt: WebsiteHelpCenterTickets.createdAt,
createdAt: true, })
}, .from(WebsiteHelpCenterTickets)
}); .where(eq(WebsiteHelpCenterTickets.id, ticketId))
.limit(1);
ticket = row ?? null;
} catch { } catch {
ticket = null; ticket = null;
} }
@@ -85,16 +93,16 @@ export default async function HelpTicketDetailPage({
if (!ticket || ticket.userId !== userId) notFound(); if (!ticket || ticket.userId !== userId) notFound();
try { try {
replies = await prisma.websiteHelpCenterTicketReplies.findMany({ replies = await db
where: { ticketId: ticket.id }, .select({
orderBy: { id: "asc" }, id: WebsiteHelpCenterTicketReplies.id,
select: { userId: WebsiteHelpCenterTicketReplies.userId,
id: true, content: WebsiteHelpCenterTicketReplies.content,
userId: true, createdAt: WebsiteHelpCenterTicketReplies.createdAt,
content: true, })
createdAt: true, .from(WebsiteHelpCenterTicketReplies)
}, .where(eq(WebsiteHelpCenterTicketReplies.ticketId, ticket.id))
}); .orderBy(asc(WebsiteHelpCenterTicketReplies.id));
} catch { } catch {
replies = []; replies = [];
} }
@@ -102,10 +110,10 @@ export default async function HelpTicketDetailPage({
const authorIds = [...new Set([userId, ...replies.map((r) => r.userId)])]; const authorIds = [...new Set([userId, ...replies.map((r) => r.userId)])];
let users: { id: number; username: string }[] = []; let users: { id: number; username: string }[] = [];
try { try {
users = await prisma.user.findMany({ users = await db
where: { id: { in: authorIds } }, .select({ id: User.id, username: User.username })
select: { id: true, username: true }, .from(User)
}); .where(inArray(User.id, authorIds));
} catch { } catch {
users = []; users = [];
} }
+9 -9
View File
@@ -1,3 +1,4 @@
import { desc, eq } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
@@ -5,8 +6,8 @@ import type { CSSProperties } from "react";
import { createTicket } from "@/actions/help-tickets"; import { createTicket } from "@/actions/help-tickets";
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, WebsiteHelpCenterTickets } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -41,15 +42,14 @@ export default async function HelpTicketsPage({
const userId = Number(session.user.id); const userId = Number(session.user.id);
let tickets: Awaited< let tickets: (typeof WebsiteHelpCenterTickets.$inferSelect)[] = [];
ReturnType<typeof prisma.websiteHelpCenterTickets.findMany>
> = [];
try { try {
tickets = await prisma.websiteHelpCenterTickets.findMany({ tickets = await db
where: { userId }, .select()
orderBy: { createdAt: "desc" }, .from(WebsiteHelpCenterTickets)
take: 100, .where(eq(WebsiteHelpCenterTickets.userId, userId))
}); .orderBy(desc(WebsiteHelpCenterTickets.createdAt))
.limit(100);
} catch { } catch {
tickets = []; tickets = [];
} }
+53 -32
View File
@@ -1,8 +1,9 @@
import { desc, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui"; import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
import { db, User, UsersCurrency, UsersSettings } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format"; import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300; export const revalidate = 300;
@@ -41,11 +42,15 @@ type Row = { username: string; look: string; value: number };
async function loadCreditsRows(): Promise<Row[]> { async function loadCreditsRows(): Promise<Row[]> {
try { try {
const users = await prisma.user.findMany({ const users = await db
orderBy: { credits: "desc" }, .select({
take: 20, username: User.username,
select: { username: true, look: true, credits: true }, look: User.look,
}); credits: User.credits,
})
.from(User)
.orderBy(desc(User.credits))
.limit(20);
return users.map((u) => ({ return users.map((u) => ({
username: u.username, username: u.username,
look: u.look, look: u.look,
@@ -58,18 +63,26 @@ async function loadCreditsRows(): Promise<Row[]> {
async function loadCurrencyRows(type: number): Promise<Row[]> { async function loadCurrencyRows(type: number): Promise<Row[]> {
try { try {
const top = await prisma.usersCurrency.findMany({ const top = await db
where: { type }, .select({
orderBy: { amount: "desc" }, userId: UsersCurrency.userId,
take: 20, amount: UsersCurrency.amount,
select: { userId: true, amount: true }, })
}); .from(UsersCurrency)
.where(eq(UsersCurrency.type, type))
.orderBy(desc(UsersCurrency.amount))
.limit(20);
if (top.length === 0) return []; if (top.length === 0) return [];
const users = await prisma.user.findMany({ const users = await db
where: { id: { in: top.map((t) => t.userId) } }, .select({ id: User.id, username: User.username, look: User.look })
select: { id: true, username: true, look: true }, .from(User)
}); .where(
inArray(
User.id,
top.map((t) => t.userId),
),
);
const byId = new Map(users.map((u) => [u.id, u])); const byId = new Map(users.map((u) => [u.id, u]));
return top return top
@@ -85,31 +98,39 @@ async function loadCurrencyRows(type: number): Promise<Row[]> {
} }
async function loadSettingsRows( async function loadSettingsRows(
field: "achievementScore" | "respectsReceived" | "onlineTime" | "hofPoints", field: "achievementScore" | "respectsReceived" | "onlineTime",
): Promise<Row[]> { ): Promise<Row[]> {
try { try {
const top = await prisma.usersSettings.findMany({ const column = UsersSettings[field];
orderBy: { [field]: "desc" }, const top = await db
take: 20, .select({
select: { userId: true, [field]: true }, userId: UsersSettings.userId,
}); value: column,
})
.from(UsersSettings)
.orderBy(desc(column))
.limit(20);
if (top.length === 0) return []; if (top.length === 0) return [];
const users = await prisma.user.findMany({ const users = await db
where: { id: { in: top.map((t) => t.userId) } }, .select({ id: User.id, username: User.username, look: User.look })
select: { id: true, username: true, look: true }, .from(User)
}); .where(
inArray(
User.id,
top.map((t) => t.userId),
),
);
const byId = new Map(users.map((u) => [u.id, u])); 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) => { .map((t) => {
const u = byId.get(t.userId as number); const u = byId.get(t.userId);
if (!u) return null; if (!u) return null;
// eslint-disable-next-line security/detect-object-injection -- field is union of known keys
return { return {
username: u.username, username: u.username,
look: u.look, look: u.look,
value: Number(t[field] ?? 0), value: Number(t.value ?? 0),
}; };
}) })
.filter((r): r is Row => r !== null); .filter((r): r is Row => r !== null);
@@ -134,10 +155,10 @@ export default async function LeaderboardPage({
}) { }) {
const t = await getTranslations("pages.leaderboard"); const t = await getTranslations("pages.leaderboard");
const { type } = await searchParams; 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) ? (type as TabKey)
: "credits"; : "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([ const [rows, imagerBase] = await Promise.all([
active === "credits" active === "credits"
+17 -14
View File
@@ -1,3 +1,4 @@
import { count, desc, eq } from "drizzle-orm";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -8,9 +9,9 @@ import { LanguageSwitcher } from "@/components/language-switcher";
import { Reveal } from "@/components/motion-reveal"; import { Reveal } from "@/components/motion-reveal";
import { ThemeSwitcher } from "@/components/theme-switcher"; import { ThemeSwitcher } from "@/components/theme-switcher";
import { cached } from "@/lib/cache"; import { cached } from "@/lib/cache";
import { db, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format"; import { avatarImageUrl } from "@/lib/format";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { prisma } from "@/lib/prisma";
import { captchaConfig } from "@/lib/services/captcha"; import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
@@ -32,21 +33,23 @@ export default async function LoginPage() {
const [online, recentUsers, latestUsers] = await Promise.all([ const [online, recentUsers, latestUsers] = await Promise.all([
cached("online_count", 10_000, () => cached("online_count", 10_000, () =>
prisma.user.count({ where: { online: "1" } }), db
.select({ total: count() })
.from(User)
.where(eq(User.online, "1"))
.then((rows) => rows[0]?.total ?? 0),
).catch(() => 0), ).catch(() => 0),
prisma.user db
.findMany({ .select({ username: User.username, look: User.look })
where: { online: "1" }, .from(User)
select: { username: true, look: true }, .where(eq(User.online, "1"))
take: 8, .limit(8)
})
.catch(() => []), .catch(() => []),
prisma.user db
.findMany({ .select({ username: User.username, look: User.look })
orderBy: { accountCreated: "desc" }, .from(User)
select: { username: true, look: true }, .orderBy(desc(User.accountCreated))
take: 8, .limit(8)
})
.catch(() => []), .catch(() => []),
]); ]);
+18 -17
View File
@@ -1,7 +1,8 @@
import { desc, eq, inArray } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
import { db, MarketplaceItems, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 300; export const revalidate = 300;
@@ -37,18 +38,18 @@ export default async function MarketplacePage() {
timestamp: number; timestamp: number;
}[] = []; }[] = [];
try { try {
offers = await prisma.marketplaceItems.findMany({ offers = await db
where: { state: STATE_ACTIVE }, .select({
select: { id: MarketplaceItems.id,
id: true, itemId: MarketplaceItems.itemId,
itemId: true, userId: MarketplaceItems.userId,
userId: true, price: MarketplaceItems.price,
price: true, timestamp: MarketplaceItems.timestamp,
timestamp: true, })
}, .from(MarketplaceItems)
orderBy: { timestamp: "desc" }, .where(eq(MarketplaceItems.state, STATE_ACTIVE))
take: 100, .orderBy(desc(MarketplaceItems.timestamp))
}); .limit(100);
} catch { } catch {
offers = []; offers = [];
} }
@@ -58,10 +59,10 @@ export default async function MarketplacePage() {
try { try {
const ids = [...new Set(offers.map((o) => o.userId))]; const ids = [...new Set(offers.map((o) => o.userId))];
if (ids.length > 0) { if (ids.length > 0) {
const users = await prisma.user.findMany({ const users = await db
where: { id: { in: ids } }, .select({ id: User.id, username: User.username })
select: { id: true, username: true }, .from(User)
}); .where(inArray(User.id, ids));
sellers = new Map(users.map((u) => [u.id, u.username])); sellers = new Map(users.map((u) => [u.id, u.username]));
} }
} catch { } catch {
+142 -100
View File
@@ -1,3 +1,4 @@
import { and, asc, count, desc, eq, gt, inArray, or } from "drizzle-orm";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -6,9 +7,20 @@ import { claimReferral } from "@/actions/referral";
import { Reveal } from "@/components/motion-reveal"; import { Reveal } from "@/components/motion-reveal";
import { auth } from "@/lib/auth"; 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 { avatarImageUrl } from "@/lib/format";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import CopyReferralButton from "./CopyReferralButton"; 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 BADGE_IMG_BASE = "https://images.habbo.com/c_images/album1584";
const [ const [
user, userRows,
imagerBase, imagerBase,
hotelName, hotelName,
neededRaw, neededRows,
amountRaw, amountRows,
currencyRaw, currencyRows,
alertRaw, alertRaw,
recentRooms, recentRooms,
badges, badges,
lastWebLogin, lastWebLoginRows,
userSettings, userSettingsRows,
friendCount, friendCountRows,
unreadCount, unreadCountRows,
] = await Promise.all([ ] = await Promise.all([
prisma.user.findUnique({ db
where: { id: userId }, .select({
select: { id: User.id,
id: true, username: User.username,
username: true, look: User.look,
look: true, motto: User.motto,
motto: true, rank: User.rank,
rank: true, credits: User.credits,
credits: true, accountCreated: User.accountCreated,
accountCreated: true, lastLogin: User.lastLogin,
lastLogin: true, lastOnline: User.lastOnline,
lastOnline: true, })
}, .from(User)
}), .where(eq(User.id, userId))
.limit(1),
siteSettings.get( siteSettings.get(
"habbo_imaging_url", "habbo_imaging_url",
"https://www.habbo.com/habbo-imaging/avatarimage", "https://www.habbo.com/habbo-imaging/avatarimage",
), ),
resolveHotelName(), resolveHotelName(),
prisma.websiteSetting db
.findUnique({ .select({ value: WebsiteSetting.value })
where: { key: "referrals_needed" }, .from(WebsiteSetting)
select: { value: true }, .where(eq(WebsiteSetting.key, "referrals_needed"))
}) .limit(1)
.catch(() => null), .catch(() => []),
prisma.websiteSetting db
.findUnique({ .select({ value: WebsiteSetting.value })
where: { key: "referral_reward_amount" }, .from(WebsiteSetting)
select: { value: true }, .where(eq(WebsiteSetting.key, "referral_reward_amount"))
}) .limit(1)
.catch(() => null), .catch(() => []),
prisma.websiteSetting db
.findFirst({ .select({ value: WebsiteSetting.value })
where: { .from(WebsiteSetting)
key: { .where(
in: ["referral_reward_currency_type", "referral_reward_currency"], inArray(WebsiteSetting.key, [
}, "referral_reward_currency_type",
}, "referral_reward_currency",
select: { value: true }, ]),
}) )
.catch(() => null), .limit(1)
.catch(() => []),
siteSettings.get("hotel_alert_banner", ""), siteSettings.get("hotel_alert_banner", ""),
prisma.rooms db
.findMany({ .select({
orderBy: { id: "desc" }, id: Rooms.id,
select: { name: Rooms.name,
id: true, ownerName: Rooms.ownerName,
name: true, users: Rooms.users,
ownerName: true, usersMax: Rooms.usersMax,
users: true,
usersMax: true,
},
take: 6,
}) })
.from(Rooms)
.orderBy(desc(Rooms.id))
.limit(6)
.catch(() => []), .catch(() => []),
prisma.usersBadges db
.findMany({ .select({
where: { userId, slotId: { gt: 0 } }, badgeCode: UsersBadges.badgeCode,
orderBy: { slotId: "asc" }, slotId: UsersBadges.slotId,
select: { badgeCode: true, slotId: true },
}) })
.from(UsersBadges)
.where(and(eq(UsersBadges.userId, userId), gt(UsersBadges.slotId, 0)))
.orderBy(asc(UsersBadges.slotId))
.catch(() => []), .catch(() => []),
prisma.websiteLoginLogs db
.findFirst({ .select({ createdAt: WebsiteLoginLogs.createdAt })
where: { userId }, .from(WebsiteLoginLogs)
orderBy: { id: "desc" }, .where(eq(WebsiteLoginLogs.userId, userId))
select: { createdAt: true }, .orderBy(desc(WebsiteLoginLogs.id))
.limit(1)
.catch(() => []),
db
.select({
achievementScore: UsersSettings.achievementScore,
respectsReceived: UsersSettings.respectsReceived,
}) })
.catch(() => null), .from(UsersSettings)
prisma.usersSettings .where(eq(UsersSettings.userId, userId))
.findUnique({ .limit(1)
where: { userId }, .catch(() => []),
select: { achievementScore: true, respectsReceived: true }, db
}) .select({ value: count() })
.catch(() => null), .from(MessengerFriendships)
prisma.messengerFriendships .where(
.count({ or(
where: { OR: [{ userOneId: userId }, { userTwoId: userId }] }, eq(MessengerFriendships.userOneId, userId),
}) eq(MessengerFriendships.userTwoId, userId),
.catch(() => 0), ),
prisma.messengerOffline.count({ where: { userId } }).catch(() => 0), )
.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; void lastWebLogin;
if (!user) throw new Error("user-not-found"); if (!user) throw new Error("user-not-found");
@@ -179,22 +215,29 @@ export default async function MePage({
.trim() .trim()
.toLowerCase(); .toLowerCase();
const referrals = await prisma.userReferrals const [referrals] = await db
.findFirst({ .select({ referralsTotal: UserReferrals.referralsTotal })
where: { userId }, .from(UserReferrals)
select: { referralsTotal: true }, .where(eq(UserReferrals.userId, userId))
orderBy: { id: "desc" }, .orderBy(desc(UserReferrals.id))
}) .limit(1)
.catch(() => null); .catch(() => []);
const referralTotal = referrals ? Number(referrals.referralsTotal) : 0; const referralTotal = referrals ? Number(referrals.referralsTotal) : 0;
const canClaim = referralTotal >= needed; const canClaim = referralTotal >= needed;
const remaining = Math.max(0, needed - referralTotal); const remaining = Math.max(0, needed - referralTotal);
const friendships = await prisma.messengerFriendships const friendships = await db
.findMany({ .select({
where: { OR: [{ userOneId: userId }, { userTwoId: userId }] }, userOneId: MessengerFriendships.userOneId,
select: { userOneId: true, userTwoId: true }, userTwoId: MessengerFriendships.userTwoId,
}) })
.from(MessengerFriendships)
.where(
or(
eq(MessengerFriendships.userOneId, userId),
eq(MessengerFriendships.userTwoId, userId),
),
)
.catch(() => []); .catch(() => []);
const friendIds = Array.from( const friendIds = Array.from(
@@ -206,17 +249,16 @@ export default async function MePage({
); );
const friends = friendIds.length const friends = friendIds.length
? await prisma.user ? await db
.findMany({ .select({
where: { id: { in: friendIds } }, id: User.id,
select: { username: User.username,
id: true, look: User.look,
username: true, motto: User.motto,
look: true, online: User.online,
motto: true,
online: true,
},
}) })
.from(User)
.where(inArray(User.id, friendIds))
.catch(() => []) .catch(() => [])
: []; : [];
+48 -23
View File
@@ -1,3 +1,4 @@
import { desc, eq, inArray, or } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
@@ -9,9 +10,15 @@ import {
} from "@/actions/messenger"; } from "@/actions/messenger";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import {
db,
MessengerFriendrequests,
MessengerFriendships,
MessengerOffline,
User,
} from "@/lib/db";
import { avatarImageUrl } from "@/lib/format"; import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -64,24 +71,38 @@ export default async function MessagesPage({
try { try {
const [reqRows, msgRows, friendRows, base] = await Promise.all([ const [reqRows, msgRows, friendRows, base] = await Promise.all([
prisma.messengerFriendrequests.findMany({ db
where: { userToId: userId }, .select({
select: { id: true, userFromId: true }, id: MessengerFriendrequests.id,
orderBy: { id: "desc" }, userFromId: MessengerFriendrequests.userFromId,
take: 100, })
}), .from(MessengerFriendrequests)
prisma.messengerOffline.findMany({ .where(eq(MessengerFriendrequests.userToId, userId))
where: { userId }, .orderBy(desc(MessengerFriendrequests.id))
select: { id: true, userFromId: true, message: true, sendedOn: true }, .limit(100),
orderBy: { sendedOn: "desc" }, db
take: 50, .select({
}), id: MessengerOffline.id,
prisma.messengerFriendships.findMany({ userFromId: MessengerOffline.userFromId,
where: { message: MessengerOffline.message,
OR: [{ userOneId: userId }, { userTwoId: userId }], sendedOn: MessengerOffline.sendedOn,
}, })
select: { userOneId: true, userTwoId: true }, .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( siteSettings.get(
"habbo_imaging_url", "habbo_imaging_url",
"https://www.habbo.com/habbo-imaging/avatarimage", "https://www.habbo.com/habbo-imaging/avatarimage",
@@ -119,10 +140,14 @@ export default async function MessagesPage({
>(); >();
if (otherIds.length > 0) { if (otherIds.length > 0) {
try { try {
const users = await prisma.user.findMany({ const users = await db
where: { id: { in: otherIds } }, .select({
select: { id: true, username: true, look: true }, 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])); userById = new Map(users.map((u) => [u.id, u]));
} catch { } catch {
userById = new Map(); userById = new Map();
+62 -41
View File
@@ -1,3 +1,4 @@
import { and, asc, desc, eq, inArray } from "drizzle-orm";
import type { Metadata } from "next"; import type { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
@@ -8,9 +9,15 @@ import { toggleReaction } from "@/actions/article-reactions";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { SanitizedHtml } from "@/components/shared/sanitized-html"; import { SanitizedHtml } from "@/components/shared/sanitized-html";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import {
db,
User,
WebsiteArticleComments,
WebsiteArticleReactions,
WebsiteArticles,
} from "@/lib/db";
import { excerpt } from "@/lib/format"; import { excerpt } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
import { cacheQuery } from "@/lib/prisma-cache"; import { cacheQuery } from "@/lib/prisma-cache";
import { sanitize } from "@/lib/sanitize"; import { sanitize } from "@/lib/sanitize";
@@ -39,12 +46,11 @@ function feedbackStyle(tone: "success" | "error"): CSSProperties {
} }
export async function generateStaticParams() { export async function generateStaticParams() {
const slugs = await prisma.websiteArticles const slugs = await db
.findMany({ .select({ slug: WebsiteArticles.slug })
select: { slug: true }, .from(WebsiteArticles)
orderBy: { createdAt: "desc" }, .orderBy(desc(WebsiteArticles.createdAt))
take: 20, .limit(20)
})
.catch(() => []); .catch(() => []);
return slugs.map((a) => ({ slug: a.slug })); return slugs.map((a) => ({ slug: a.slug }));
} }
@@ -56,9 +62,15 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const appUrl = process.env.APP_URL ?? "http://localhost:3000"; const appUrl = process.env.APP_URL ?? "http://localhost:3000";
const { slug } = await params; const { slug } = await params;
const article = await prisma.websiteArticles const [article] = await db
.findUnique({ where: { slug }, select: { title: true, shortStory: true } }) .select({
.catch(() => null); title: WebsiteArticles.title,
shortStory: WebsiteArticles.shortStory,
})
.from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, slug))
.limit(1)
.catch(() => []);
if (!article) return { title: "Article" }; if (!article) return { title: "Article" };
return { return {
title: article.title, title: article.title,
@@ -97,22 +109,24 @@ export default async function ArticlePage({
"websiteArticles", "websiteArticles",
"findUnique", "findUnique",
{ slug }, { slug },
() => async () => {
prisma.websiteArticles const [row] = await db
.findUnique({ .select({
where: { slug }, id: WebsiteArticles.id,
select: { slug: WebsiteArticles.slug,
id: true, title: WebsiteArticles.title,
slug: true, shortStory: WebsiteArticles.shortStory,
title: true, fullStory: WebsiteArticles.fullStory,
shortStory: true, image: WebsiteArticles.image,
fullStory: true, createdAt: WebsiteArticles.createdAt,
image: true, updatedAt: WebsiteArticles.updatedAt,
createdAt: true,
updatedAt: true,
},
}) })
.catch(() => null), .from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, slug))
.limit(1)
.catch(() => []);
return row ?? null;
},
); );
if (!article) notFound(); if (!article) notFound();
@@ -127,10 +141,18 @@ export default async function ArticlePage({
let reactionCounts = new Map<string, number>(); let reactionCounts = new Map<string, number>();
let myReaction: string | null = null; let myReaction: string | null = null;
try { try {
const rows = await prisma.websiteArticleReactions.findMany({ const rows = await db
where: { articleId, active: true }, .select({
select: { reaction: true, userId: true }, reaction: WebsiteArticleReactions.reaction,
}); userId: WebsiteArticleReactions.userId,
})
.from(WebsiteArticleReactions)
.where(
and(
eq(WebsiteArticleReactions.articleId, articleId),
eq(WebsiteArticleReactions.active, true),
),
);
for (const r of rows) { for (const r of rows) {
reactionCounts.set(r.reaction, (reactionCounts.get(r.reaction) ?? 0) + 1); reactionCounts.set(r.reaction, (reactionCounts.get(r.reaction) ?? 0) + 1);
if (sessionUserId !== null && r.userId === sessionUserId) if (sessionUserId !== null && r.userId === sessionUserId)
@@ -142,15 +164,14 @@ export default async function ArticlePage({
} }
// Comments for this article (oldest first). // Comments for this article (oldest first).
let comments: Awaited< let comments: (typeof WebsiteArticleComments.$inferSelect)[] = [];
ReturnType<typeof prisma.websiteArticleComments.findMany>
> = [];
try { try {
comments = await prisma.websiteArticleComments.findMany({ comments = await db
where: { articleId }, .select()
orderBy: { createdAt: "asc" }, .from(WebsiteArticleComments)
take: 200, .where(eq(WebsiteArticleComments.articleId, articleId))
}); .orderBy(asc(WebsiteArticleComments.createdAt))
.limit(200);
} catch { } catch {
comments = []; comments = [];
} }
@@ -160,10 +181,10 @@ export default async function ArticlePage({
let usernameById = new Map<number, string>(); let usernameById = new Map<number, string>();
if (commenterIds.length > 0) { if (commenterIds.length > 0) {
try { try {
const users = await prisma.user.findMany({ const users = await db
where: { id: { in: commenterIds } }, .select({ id: User.id, username: User.username })
select: { id: true, username: true }, .from(User)
}); .where(inArray(User.id, commenterIds));
usernameById = new Map(users.map((u) => [u.id, u.username])); usernameById = new Map(users.map((u) => [u.id, u.username]));
} catch { } catch {
usernameById = new Map(); usernameById = new Map();
+8 -7
View File
@@ -1,10 +1,11 @@
import { desc } from "drizzle-orm";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, WebsiteArticles } from "@/lib/db";
import { excerpt } from "@/lib/format"; import { excerpt } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 300; export const revalidate = 300;
@@ -13,13 +14,13 @@ export const metadata = { title: "News" };
export default async function NewsPage() { export default async function NewsPage() {
const t = await getTranslations("pages.news"); const t = await getTranslations("pages.news");
let articles: Awaited<ReturnType<typeof prisma.websiteArticles.findMany>> = let articles: (typeof WebsiteArticles.$inferSelect)[] = [];
[];
try { try {
articles = await prisma.websiteArticles.findMany({ articles = await db
orderBy: { createdAt: "desc" }, .select()
take: 30, .from(WebsiteArticles)
}); .orderBy(desc(WebsiteArticles.createdAt))
.limit(30);
} catch { } catch {
// DB unavailable — render the empty state rather than a 500. // DB unavailable — render the empty state rather than a 500.
articles = []; articles = [];
+39 -20
View File
@@ -1,3 +1,4 @@
import { count, desc, eq } from "drizzle-orm";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -13,10 +14,10 @@ import { ThemeSwitcher } from "@/components/theme-switcher";
import { TypewriterText } from "@/components/typewriter-text"; import { TypewriterText } from "@/components/typewriter-text";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { cached } from "@/lib/cache"; import { cached } from "@/lib/cache";
import { CameraWeb, db, Rooms, User, WebsiteArticles } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format"; import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { prisma } from "@/lib/prisma";
import { captchaConfig } from "@/lib/services/captcha"; import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
@@ -43,29 +44,47 @@ async function getHotelData() {
latestUsers, latestUsers,
] = await Promise.all([ ] = await Promise.all([
cached("online_count", 10_000, () => cached("online_count", 10_000, () =>
prisma.user.count({ where: { online: "1" } }), db
.select({ total: count() })
.from(User)
.where(eq(User.online, "1"))
.then((rows) => rows[0]?.total ?? 0),
).catch(() => 0), ).catch(() => 0),
cached("total_users", 30_000, () => prisma.user.count()).catch(() => 0), cached("total_users", 30_000, () =>
cached("total_rooms", 30_000, () => prisma.rooms.count()).catch(() => 0), db
prisma.websiteArticles .select({ total: count() })
.findMany({ orderBy: { createdAt: "desc" }, take: 5 }) .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(() => []), .catch(() => []),
prisma.user db
.findMany({ .select({ username: User.username, look: User.look })
where: { online: "1" }, .from(User)
select: { username: true, look: true }, .where(eq(User.online, "1"))
take: 30, .limit(30)
})
.catch(() => []), .catch(() => []),
prisma.cameraWeb db
.findMany({ orderBy: { timestamp: "desc" }, take: 4 }) .select()
.from(CameraWeb)
.orderBy(desc(CameraWeb.timestamp))
.limit(4)
.catch(() => []), .catch(() => []),
prisma.user db
.findMany({ .select({ username: User.username, look: User.look })
orderBy: { accountCreated: "desc" }, .from(User)
select: { username: true, look: true }, .orderBy(desc(User.accountCreated))
take: 8, .limit(8)
})
.catch(() => []), .catch(() => []),
]); ]);
+7 -5
View File
@@ -1,11 +1,12 @@
import { desc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
type LightboxPhoto, type LightboxPhoto,
PhotoLightbox, PhotoLightbox,
} from "@/components/public/photo-lightbox"; } from "@/components/public/photo-lightbox";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { CameraWeb, db } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 300; export const revalidate = 300;
@@ -24,10 +25,11 @@ export default async function PhotosPage() {
let photos: Photo[] = []; let photos: Photo[] = [];
try { try {
photos = await prisma.cameraWeb.findMany({ photos = await db
orderBy: { timestamp: "desc" }, .select()
take: 48, .from(CameraWeb)
}); .orderBy(desc(CameraWeb.timestamp))
.limit(48);
} catch { } catch {
photos = []; photos = [];
} }
+42 -24
View File
@@ -1,10 +1,16 @@
import { and, asc, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import {
db,
WebsitePoll,
WebsitePollQuestion,
WebsitePollVote,
} from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
import { PollVoteForm } from "./poll-vote-form"; import { PollVoteForm } from "./poll-vote-form";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -22,19 +28,24 @@ export default async function PollDetailPage({
const session = await auth(); const session = await auth();
const userId = Number(session?.user?.id); const userId = Number(session?.user?.id);
const poll = await prisma.websitePoll const [poll] = await db
.findUnique({ .select()
where: { id: pollId }, .from(WebsitePoll)
include: { .where(eq(WebsitePoll.id, pollId))
questions: { orderBy: { sortOrder: "asc" } }, .limit(1)
}, .catch(() => []);
})
.catch(() => null);
if (!poll || (poll.status !== "active" && poll.status !== "closed")) { if (!poll || (poll.status !== "active" && poll.status !== "closed")) {
notFound(); notFound();
} }
const questions = await db
.select()
.from(WebsitePollQuestion)
.where(eq(WebsitePollQuestion.pollId, poll.id))
.orderBy(asc(WebsitePollQuestion.sortOrder))
.catch(() => []);
const now = Date.now(); const now = Date.now();
if ( if (
poll.startsAt && poll.startsAt &&
@@ -50,24 +61,31 @@ export default async function PollDetailPage({
(poll.endsAt != null && poll.endsAt.getTime() < now); (poll.endsAt != null && poll.endsAt.getTime() < now);
const isOpen = poll.status === "active" && !hasEnded; const isOpen = poll.status === "active" && !hasEnded;
const questionIds = poll.questions.map((q) => q.id); const questionIds = questions.map((q) => q.id);
const myVotes = const myVotes =
Number.isInteger(userId) && userId > 0 && questionIds.length > 0 Number.isInteger(userId) && userId > 0 && questionIds.length > 0
? await prisma.websitePollVote ? await db
.findMany({ .select()
where: { userId, questionId: { in: questionIds } }, .from(WebsitePollVote)
}) .where(
and(
eq(WebsitePollVote.userId, userId),
inArray(WebsitePollVote.questionId, questionIds),
),
)
.catch(() => []) .catch(() => [])
: []; : [];
const hasVoted = myVotes.length > 0; const hasVoted = myVotes.length > 0;
const allVotes = const allVotes =
poll.showResults === 1 poll.showResults === 1 && questionIds.length > 0
? await prisma.websitePollVote ? await db
.findMany({ .select({
where: { questionId: { in: questionIds } }, questionId: WebsitePollVote.questionId,
select: { questionId: true, answer: true }, answer: WebsitePollVote.answer,
}) })
.from(WebsitePollVote)
.where(inArray(WebsitePollVote.questionId, questionIds))
.catch(() => []) .catch(() => [])
: []; : [];
@@ -112,7 +130,7 @@ export default async function PollDetailPage({
> >
<PollVoteForm <PollVoteForm
pollId={poll.id} pollId={poll.id}
questions={poll.questions.map((q) => ({ questions={questions.map((q) => ({
id: q.id, id: q.id,
question: q.question, question: q.question,
type: q.type, type: q.type,
@@ -140,7 +158,7 @@ export default async function PollDetailPage({
{poll.showResults === 1 && (hasVoted || hasEnded) ? ( {poll.showResults === 1 && (hasVoted || hasEnded) ? (
<ContentCard icon="📈" title={t("resultsTitle")}> <ContentCard icon="📈" title={t("resultsTitle")}>
<div style={{ display: "grid", gap: "1.25rem" }}> <div style={{ display: "grid", gap: "1.25rem" }}>
{poll.questions.map((q) => { {questions.map((q) => {
const options = q.options const options = q.options
.split("\n") .split("\n")
.map((o) => o.trim()) .map((o) => o.trim())
@@ -179,9 +197,9 @@ export default async function PollDetailPage({
</h3> </h3>
<div style={{ display: "grid", gap: "0.55rem" }}> <div style={{ display: "grid", gap: "0.55rem" }}>
{options.map((opt) => { {options.map((opt) => {
const count = counts.get(opt) ?? 0; const voteCount = counts.get(opt) ?? 0;
const pct = const pct =
total > 0 ? Math.round((count / total) * 100) : 0; total > 0 ? Math.round((voteCount / total) * 100) : 0;
return ( return (
<div key={opt}> <div key={opt}>
<div <div
@@ -194,7 +212,7 @@ export default async function PollDetailPage({
> >
<span>{opt}</span> <span>{opt}</span>
<span className="muted"> <span className="muted">
{t("voteCount", { count, pct })} {t("voteCount", { count: voteCount, pct })}
</span> </span>
</div> </div>
<div <div
+35 -13
View File
@@ -1,9 +1,10 @@
import { and, count, desc, inArray, isNull, lte, or } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, WebsitePoll, WebsitePollQuestion } from "@/lib/db";
import { excerpt } from "@/lib/format"; import { excerpt } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 60; export const revalidate = 60;
@@ -13,20 +14,41 @@ export default async function PollsPage() {
const t = await getTranslations("pages.polls"); const t = await getTranslations("pages.polls");
const now = new Date(); const now = new Date();
const polls = await prisma.websitePoll const pollRows = await db
.findMany({ .select()
where: { .from(WebsitePoll)
status: { in: ["active", "closed"] }, .where(
OR: [{ startsAt: null }, { startsAt: { lte: now } }], and(
}, inArray(WebsitePoll.status, ["active", "closed"]),
orderBy: { createdAt: "desc" }, or(isNull(WebsitePoll.startsAt), lte(WebsitePoll.startsAt, now)),
take: 50, ),
include: { )
_count: { select: { questions: true } }, .orderBy(desc(WebsitePoll.createdAt))
}, .limit(50)
})
.catch(() => []); .catch(() => []);
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 ( return (
<main style={{ display: "grid", gap: "1.5rem" }}> <main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard icon="📊" title={t("title")} subtitle={t("subtitle")} /> <ContentCard icon="📊" title={t("title")} subtitle={t("subtitle")} />
+12 -7
View File
@@ -1,11 +1,12 @@
import { desc, eq } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { applyDj } from "@/actions/radio-apply"; import { applyDj } from "@/actions/radio-apply";
import { ContentCard } from "@/components/public/ui"; import { ContentCard } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, RadioApplications } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -49,13 +50,17 @@ export default async function RadioApplyPage({
// Surface the applicant's most recent application (if any) so they aren't // 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. // left guessing. Isolated query: a DB hiccup degrades this to null, not 500.
const existing = await prisma.radioApplications const [existing] = await db
.findFirst({ .select({
where: { userId: BigInt(userId) }, id: RadioApplications.id,
orderBy: { id: "desc" }, status: RadioApplications.status,
select: { id: true, status: true, createdAt: true }, createdAt: RadioApplications.createdAt,
}) })
.catch(() => null); .from(RadioApplications)
.where(eq(RadioApplications.userId, BigInt(userId)))
.orderBy(desc(RadioApplications.id))
.limit(1)
.catch(() => []);
const submittedDate = existing?.createdAt const submittedDate = existing?.createdAt
? formatDate(existing.createdAt, "date") ? formatDate(existing.createdAt, "date")
+8 -4
View File
@@ -1,9 +1,10 @@
import { eq } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard } from "@/components/public/ui"; import { ContentCard } from "@/components/public/ui";
import { db, RadioContests } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 300; export const revalidate = 300;
@@ -22,9 +23,12 @@ export default async function RadioContestDetailPage({
notFound(); notFound();
} }
const contest = await prisma.radioContests const [contest] = await db
.findUnique({ where: { id: contestId } }) .select()
.catch(() => null); .from(RadioContests)
.where(eq(RadioContests.id, contestId))
.limit(1)
.catch(() => []);
if (!contest) notFound(); if (!contest) notFound();
+13 -13
View File
@@ -1,27 +1,27 @@
import { desc, eq } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, RadioContests } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 300; export const revalidate = 300;
export default async function RadioContestsPage() { export default async function RadioContestsPage() {
const t = await getTranslations("pages.radioContests"); const t = await getTranslations("pages.radioContests");
const contests = await prisma.radioContests const contests = await db
.findMany({ .select({
where: { isActive: true }, id: RadioContests.id,
orderBy: { startDate: "desc" }, title: RadioContests.title,
take: 50, prize: RadioContests.prize,
select: { startDate: RadioContests.startDate,
id: true, endDate: RadioContests.endDate,
title: true,
prize: true,
startDate: true,
endDate: true,
},
}) })
.from(RadioContests)
.where(eq(RadioContests.isActive, true))
.orderBy(desc(RadioContests.startDate))
.limit(50)
.catch(() => []); .catch(() => []);
return ( return (
+8 -4
View File
@@ -1,9 +1,10 @@
import { eq } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard } from "@/components/public/ui"; import { ContentCard } from "@/components/public/ui";
import { db, RadioGiveaways } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 300; export const revalidate = 300;
@@ -22,9 +23,12 @@ export default async function RadioGiveawayDetailPage({
notFound(); notFound();
} }
const giveaway = await prisma.radioGiveaways const [giveaway] = await db
.findUnique({ where: { id: giveawayId } }) .select()
.catch(() => null); .from(RadioGiveaways)
.where(eq(RadioGiveaways.id, giveawayId))
.limit(1)
.catch(() => []);
if (!giveaway) notFound(); if (!giveaway) notFound();
+15 -15
View File
@@ -1,29 +1,29 @@
import { desc, eq } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, RadioGiveaways } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const revalidate = 300; export const revalidate = 300;
export default async function RadioGiveawaysPage() { export default async function RadioGiveawaysPage() {
const t = await getTranslations("pages.radioGiveaways"); const t = await getTranslations("pages.radioGiveaways");
const giveaways = await prisma.radioGiveaways const giveaways = await db
.findMany({ .select({
where: { isActive: true }, id: RadioGiveaways.id,
orderBy: { startDate: "desc" }, title: RadioGiveaways.title,
take: 50, prize: RadioGiveaways.prize,
select: { prizeAmount: RadioGiveaways.prizeAmount,
id: true, prizeCurrency: RadioGiveaways.prizeCurrency,
title: true, startDate: RadioGiveaways.startDate,
prize: true, endDate: RadioGiveaways.endDate,
prizeAmount: true,
prizeCurrency: true,
startDate: true,
endDate: true,
},
}) })
.from(RadioGiveaways)
.where(eq(RadioGiveaways.isActive, true))
.orderBy(desc(RadioGiveaways.startDate))
.limit(50)
.catch(() => []); .catch(() => []);
return ( return (
+10 -15
View File
@@ -1,18 +1,13 @@
import { sql } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui"; import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
import { db } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format"; import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300; 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 = { type LeaderRow = {
username: string; username: string;
look: string; look: string;
@@ -23,14 +18,14 @@ async function loadRows(): Promise<
{ username: string; look: string; points: number }[] { username: string; look: string; points: number }[]
> { > {
try { try {
const rows = await prisma.$queryRaw<LeaderRow[]>` const [rows] = (await db.execute(sql`
SELECT u.username AS username, u.look AS look, p.points AS points SELECT u.username AS username, u.look AS look, p.points AS points
FROM radio_listener_points p FROM radio_listener_points p
INNER JOIN users u ON u.id = p.user_id INNER JOIN users u ON u.id = p.user_id
ORDER BY p.points DESC ORDER BY p.points DESC
LIMIT 50 LIMIT 50
`; `)) as unknown as [LeaderRow[], unknown];
return rows.map((r) => ({ return (rows ?? []).map((r) => ({
username: r.username, username: r.username,
look: r.look, look: r.look,
points: Number(r.points), points: Number(r.points),
+11 -11
View File
@@ -1,7 +1,8 @@
import { asc, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui"; 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"; import { siteSettings } from "@/lib/services/site-settings";
// Radio config + today's schedule are read live from the DB / settings table. // 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) ── // ── Today's schedule (isolated; a DB hiccup degrades to empty) ──
const today = WEEKDAYS[new Date().getUTCDay()] ?? ""; const today = WEEKDAYS[new Date().getUTCDay()] ?? "";
const schedules = await prisma.radioSchedules const schedules = await db
.findMany({ .select()
where: { isActive: true }, .from(RadioSchedules)
orderBy: { startTime: "asc" }, .where(eq(RadioSchedules.isActive, true))
}) .orderBy(asc(RadioSchedules.startTime))
.catch(() => []); .catch(() => []);
const todaysSlots = schedules.filter( const todaysSlots = schedules.filter(
@@ -94,11 +95,10 @@ export default async function RadioPage() {
// lookup (DJ ids comfortably fit in a JS-safe integer). // lookup (DJ ids comfortably fit in a JS-safe integer).
const djIds = Array.from(new Set(todaysSlots.map((s) => Number(s.userId)))); const djIds = Array.from(new Set(todaysSlots.map((s) => Number(s.userId))));
const djs = djIds.length const djs = djIds.length
? await prisma.user ? await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: djIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, djIds))
})
.catch(() => []) .catch(() => [])
: []; : [];
const djById = new Map(djs.map((d) => [d.id, d.username])); const djById = new Map(djs.map((d) => [d.id, d.username]));
+11 -11
View File
@@ -1,10 +1,11 @@
import { desc, inArray } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { submitRequest } from "@/actions/radio-requests"; import { submitRequest } from "@/actions/radio-requests";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, RadioSongRequests, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -47,11 +48,11 @@ export default async function RadioRequestsPage({
: null; : null;
const [requests, session] = await Promise.all([ const [requests, session] = await Promise.all([
prisma.radioSongRequests db
.findMany({ .select()
orderBy: { createdAt: "desc" }, .from(RadioSongRequests)
take: 50, .orderBy(desc(RadioSongRequests.createdAt))
}) .limit(50)
.catch(() => []), .catch(() => []),
auth(), auth(),
]); ]);
@@ -60,11 +61,10 @@ export default async function RadioRequestsPage({
const userIds = Array.from(new Set(requests.map((r) => Number(r.userId)))); const userIds = Array.from(new Set(requests.map((r) => Number(r.userId))));
const users = userIds.length const users = userIds.length
? await prisma.user ? await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: userIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, userIds))
})
.catch(() => []) .catch(() => [])
: []; : [];
const userById = new Map(users.map((u) => [u.id, u.username])); const userById = new Map(users.map((u) => [u.id, u.username]));
+11 -11
View File
@@ -1,6 +1,7 @@
import { asc, eq, inArray } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { prisma } from "@/lib/prisma"; import { db, RadioSchedules, User } from "@/lib/db";
export const revalidate = 300; export const revalidate = 300;
@@ -35,21 +36,20 @@ type Slot = {
export default async function RadioSchedulePage() { export default async function RadioSchedulePage() {
const t = await getTranslations("pages.radioSchedule"); const t = await getTranslations("pages.radioSchedule");
const schedules = await prisma.radioSchedules const schedules = await db
.findMany({ .select()
where: { isActive: true }, .from(RadioSchedules)
orderBy: { startTime: "asc" }, .where(eq(RadioSchedules.isActive, true))
}) .orderBy(asc(RadioSchedules.startTime))
.catch(() => []); .catch(() => []);
// Resolve DJ usernames. schedule.userId is BigInt; User.id is Int — convert. // 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 djIds = Array.from(new Set(schedules.map((s) => Number(s.userId))));
const djs = djIds.length const djs = djIds.length
? await prisma.user ? await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: djIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, djIds))
})
.catch(() => []) .catch(() => [])
: []; : [];
const djById = new Map(djs.map((u) => [u.id, u.username])); const djById = new Map(djs.map((u) => [u.id, u.username]));
+14 -10
View File
@@ -1,11 +1,12 @@
import { desc, inArray } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { postShout } from "@/actions/radio-shouts"; import { postShout } from "@/actions/radio-shouts";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, RadioShouts, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format"; import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -37,11 +38,11 @@ export default async function RadioShoutsPage({
const { posted, error } = await searchParams; const { posted, error } = await searchParams;
const [shouts, imagerBase, session] = await Promise.all([ const [shouts, imagerBase, session] = await Promise.all([
prisma.radioShouts db
.findMany({ .select()
orderBy: { createdAt: "desc" }, .from(RadioShouts)
take: 50, .orderBy(desc(RadioShouts.createdAt))
}) .limit(50)
.catch(() => []), .catch(() => []),
siteSettings.get( siteSettings.get(
"habbo_imaging_url", "habbo_imaging_url",
@@ -55,11 +56,14 @@ export default async function RadioShoutsPage({
// Resolve author usernames/looks. shout.userId is BigInt; User.id is Int. // 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 authorIds = Array.from(new Set(shouts.map((s) => Number(s.userId))));
const authors = authorIds.length const authors = authorIds.length
? await prisma.user ? await db
.findMany({ .select({
where: { id: { in: authorIds } }, id: User.id,
select: { id: true, username: true, look: true }, username: User.username,
look: User.look,
}) })
.from(User)
.where(inArray(User.id, authorIds))
.catch(() => []) .catch(() => [])
: []; : [];
const authorById = new Map(authors.map((a) => [a.id, a])); const authorById = new Map(authors.map((a) => [a.id, a]));
+13 -12
View File
@@ -1,3 +1,4 @@
import { desc } from "drizzle-orm";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
@@ -7,8 +8,8 @@ import {
OnlineBadge, OnlineBadge,
RankBadge, RankBadge,
} from "@/components/public/ui"; } from "@/components/public/ui";
import { db, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format"; import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300; export const revalidate = 300;
@@ -27,17 +28,17 @@ export default async function RankingsPage() {
const t = await getTranslations("pages.rankings"); const t = await getTranslations("pages.rankings");
let users: TopUser[] = []; let users: TopUser[] = [];
try { try {
users = await prisma.user.findMany({ users = await db
select: { .select({
username: true, username: User.username,
look: true, look: User.look,
credits: true, credits: User.credits,
rank: true, rank: User.rank,
online: true, online: User.online,
}, })
orderBy: { credits: "desc" }, .from(User)
take: 12, .orderBy(desc(User.credits))
}); .limit(12);
} catch { } catch {
users = []; users = [];
} }
+25 -18
View File
@@ -1,8 +1,9 @@
import { asc, eq } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { prisma } from "@/lib/prisma"; import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300; export const revalidate = 300;
@@ -52,10 +53,16 @@ export default async function RareCategoryPage({
let cat: { id: bigint; name: string; badge: string } | null = null; let cat: { id: bigint; name: string; badge: string } | null = null;
try { try {
cat = await prisma.websiteRareValueCategories.findUnique({ const [row] = await db
where: { id: categoryId }, .select({
select: { id: true, name: true, badge: true }, id: WebsiteRareValueCategories.id,
}); name: WebsiteRareValueCategories.name,
badge: WebsiteRareValueCategories.badge,
})
.from(WebsiteRareValueCategories)
.where(eq(WebsiteRareValueCategories.id, categoryId))
.limit(1);
cat = row ?? null;
} catch { } catch {
cat = null; cat = null;
} }
@@ -64,19 +71,19 @@ export default async function RareCategoryPage({
let rares: RareRow[] = []; let rares: RareRow[] = [];
try { try {
rares = await prisma.websiteRareValues.findMany({ rares = await db
where: { categoryId: categoryId }, .select({
orderBy: { name: "asc" }, id: WebsiteRareValues.id,
select: { name: WebsiteRareValues.name,
id: true, creditValue: WebsiteRareValues.creditValue,
name: true, currencyValue: WebsiteRareValues.currencyValue,
creditValue: true, currencyType: WebsiteRareValues.currencyType,
currencyValue: true, furnitureIcon: WebsiteRareValues.furnitureIcon,
currencyType: true, itemId: WebsiteRareValues.itemId,
furnitureIcon: true, })
itemId: true, .from(WebsiteRareValues)
}, .where(eq(WebsiteRareValues.categoryId, categoryId))
}); .orderBy(asc(WebsiteRareValues.name));
} catch { } catch {
rares = []; rares = [];
} }