refactor(db): migrate app pages and APIs from Prisma facade to Drizzle
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
53b350057d
commit
7c39aef5d9
40 files changed
+1124
-692
No files matched your search
@@ -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));
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(() => []),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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
@@ -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(() => [])
|
||||
: [];
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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(() => []),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")} />
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user