Auth (hand-built, uses the auth core): - /register + register() action: validates, hashes with argon2id (hashPassword), creates an emulator-compatible users row (accountCreated/ipRegister/ipCurrent/ look), redirect kept outside try so NEXT_REDIRECT propagates. Login/register cross-links; header shows Register (logged-out) and a Logout (signOut) button. Batch 5 (parallel agents): /admin/rooms (+[id]) search+detail, /admin/vouchers (CRUD), /admin/subscriptions (read), /admin/calendar (campaigns+rewards read), /marketplace (public). Header + admin nav extended. Verified: tsc exit 0, vitest 48/48, next build exit 0 (52 page routes).
134 lines
3.8 KiB
TypeScript
134 lines
3.8 KiB
TypeScript
import Link from "next/link";
|
|
import { auth, signOut } from "@/lib/auth";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { siteSettings } from "@/lib/services/site-settings";
|
|
|
|
export async function SiteHeader() {
|
|
const [hotelName, session] = await Promise.all([
|
|
siteSettings.get("hotel_name", "Atom"),
|
|
auth(),
|
|
]);
|
|
|
|
let credits = 0;
|
|
let duckets = 0;
|
|
let diamonds = 0;
|
|
if (session?.user?.id) {
|
|
try {
|
|
const id = Number(session.user.id);
|
|
const [user, currencies] = await Promise.all([
|
|
prisma.user.findUnique({ where: { id }, select: { credits: true } }),
|
|
prisma.usersCurrency.findMany({ where: { userId: id } }),
|
|
]);
|
|
credits = user?.credits ?? 0;
|
|
for (const c of currencies) {
|
|
if (c.type === 0) duckets = c.amount;
|
|
if (c.type === 5) diamonds = c.amount;
|
|
}
|
|
} catch {
|
|
// DB unavailable — render the header without live balances.
|
|
}
|
|
}
|
|
|
|
const isStaff = (session?.user?.rank ?? 0) >= 7;
|
|
|
|
return (
|
|
<header className="site-header">
|
|
<div className="bar">
|
|
<Link href="/" className="brand">
|
|
<span className="dot" />
|
|
{hotelName}
|
|
</Link>
|
|
<nav className="nav">
|
|
<Link className="nav-item" href="/">
|
|
Home
|
|
</Link>
|
|
<Link className="nav-item" href="/news">
|
|
News
|
|
</Link>
|
|
<Link className="nav-item" href="/shop">
|
|
Shop
|
|
</Link>
|
|
<Link className="nav-item" href="/marketplace">
|
|
Market
|
|
</Link>
|
|
<Link className="nav-item" href="/community">
|
|
Community
|
|
</Link>
|
|
<Link className="nav-item" href="/photos">
|
|
Photos
|
|
</Link>
|
|
<Link className="nav-item" href="/leaderboard">
|
|
Leaderboard
|
|
</Link>
|
|
<Link className="nav-item" href="/guilds">
|
|
Guilds
|
|
</Link>
|
|
<Link className="nav-item" href="/rares">
|
|
Rares
|
|
</Link>
|
|
<Link className="nav-item" href="/help">
|
|
Help
|
|
</Link>
|
|
{session?.user ? (
|
|
<Link className="nav-item" href="/client">
|
|
Hotel
|
|
</Link>
|
|
) : null}
|
|
{isStaff ? (
|
|
<Link className="nav-item" href="/admin">
|
|
Admin
|
|
</Link>
|
|
) : null}
|
|
</nav>
|
|
<span className="spacer" />
|
|
{session?.user ? (
|
|
<>
|
|
<div className="currencies">
|
|
<span className="currency">
|
|
<span className="coin credits">cr</span>
|
|
{credits}
|
|
</span>
|
|
<span className="currency">
|
|
<span className="coin duckets">d</span>
|
|
{duckets}
|
|
</span>
|
|
<span className="currency">
|
|
<span className="coin diamonds">◆</span>
|
|
{diamonds}
|
|
</span>
|
|
</div>
|
|
<Link className="nav-item" href="/redeem">
|
|
Redeem
|
|
</Link>
|
|
<Link className="nav-item" href="/settings">
|
|
Settings
|
|
</Link>
|
|
<Link className="nav-item" href={`/u/${session.user.name}`}>
|
|
{session.user.name}
|
|
</Link>
|
|
<form
|
|
action={async () => {
|
|
"use server";
|
|
await signOut({ redirectTo: "/" });
|
|
}}
|
|
>
|
|
<button type="submit" className="btn btn-outline">
|
|
Logout
|
|
</button>
|
|
</form>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Link className="nav-item" href="/register">
|
|
Register
|
|
</Link>
|
|
<Link className="btn btn-outline" href="/login">
|
|
Login
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|