Add social login (Discord/Google) + batch-6 pages
Auth: NextAuth Discord + Google providers (enabled when env id+secret set); OAuth signIn allowed only if a hotel account matches the email; jwt binds the session to that account (id/rank/username). Login page gets social buttons. Batch 6 (parallel agents): /friends (messenger_friendships), /guilds/[id]/forum (threads), /admin/navigation (navigator config), /admin/maintenance (toggle maintenance settings), /admin/alerts (alert_logs + send hotel alert via RCON). Header (Friends) + admin nav (Alerts/Maintenance/Navigator) extended. Verified: tsc exit 0, vitest 48/48, next build exit 0 (57 page routes).
This commit is contained in:
1 parent
73f0f54624
commit
486ce51559
12 files changed
+911
-3
No files matched your search
@@ -0,0 +1,127 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function FriendsPage() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) redirect("/login");
|
||||
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
// Friendships are bidirectional and stored once: the session user can appear
|
||||
// as either user_one_id or user_two_id, with the *other* column being the
|
||||
// friend. 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 },
|
||||
})
|
||||
.catch(() => []);
|
||||
|
||||
const friendIds = Array.from(
|
||||
new Set(
|
||||
friendships
|
||||
.map((f) => (f.userOneId === userId ? f.userTwoId : f.userOneId))
|
||||
.filter((id) => id && id !== userId),
|
||||
),
|
||||
);
|
||||
|
||||
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" },
|
||||
})
|
||||
.catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Friends</h1>
|
||||
<p className="muted" style={{ marginTop: "-0.25rem" }}>
|
||||
{friends.length === 0
|
||||
? "You have no friends added yet."
|
||||
: `You have ${friends.length} friend${friends.length === 1 ? "" : "s"}.`}
|
||||
</p>
|
||||
|
||||
{friends.length === 0 ? (
|
||||
<div className="card">
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
Add friends in the hotel and they will show up here.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid cols-3">
|
||||
{friends.map((friend) => {
|
||||
const avatar = avatarImageUrl(imagerBase ?? "", friend.look, {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
});
|
||||
const isOnline = friend.online === "1";
|
||||
return (
|
||||
<div
|
||||
key={friend.id}
|
||||
className="card hover"
|
||||
style={{ display: "flex", gap: "0.85rem", alignItems: "center" }}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
className="avatar"
|
||||
src={avatar}
|
||||
alt={`${friend.username} avatar`}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<p style={{ margin: "0 0 0.2rem", display: "flex", alignItems: "center", gap: "0.4rem" }}>
|
||||
<span
|
||||
aria-hidden
|
||||
title={isOnline ? "Online" : "Offline"}
|
||||
style={{
|
||||
width: 9,
|
||||
height: 9,
|
||||
borderRadius: "999px",
|
||||
flexShrink: 0,
|
||||
background: isOnline
|
||||
? "var(--color-accent)"
|
||||
: "var(--color-text-muted)",
|
||||
}}
|
||||
/>
|
||||
<Link href={`/u/${friend.username}`}>
|
||||
<strong>{friend.username}</strong>
|
||||
</Link>
|
||||
</p>
|
||||
<p
|
||||
className="muted"
|
||||
style={{
|
||||
margin: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{friend.motto || "No motto"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user