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