Files
Epicnabbo-Catalogus-Updated…/src/components/top-header.tsx
T
simoleo89 4dfe698009 Close remaining web gaps: rich profile, login history, lightbox/slider, flash client, admin chatlog/DJ/chart/WYSIWYG, niche API
- Rich profile (/u/[username]): wallet (credits/duckets/diamonds), friends
  grid (messenger_friendships), and owned rooms sections.
- Login history: new website_login_logs table (model + migration 0007),
  recorded on every successful sign-in (ip + user-agent), surfaced on a new
  /settings/sessions page (with failed-attempt list from failed_logins).
- Photos lightbox + home article slider (client components, no Swiper dep).
- /client/flash launcher (SSO ticket like the Nitro page).
- Admin: private chatlogs section in /admin/logs, /admin/radio/moderation
  (shout moderation), a "users by rank" inline bar chart on the dashboard,
  and a TinyMCE rich-text editor on the article admin forms.
- Niche API: /api/values/[id], /api/guilds(+/[id]), /api/radio/auto-play.

Verified live (prod, amx_test): login recorded → /settings/sessions shows
it with device; profile renders wallet/friends/rooms; dashboard chart +
private-chat logs + /client/flash + /api/guilds all OK. Reverted test data.
tsc 0, vitest 49/49, next build 0.
2026-06-29 18:26:34 +02:00

135 lines
4.9 KiB
TypeScript

import Link from "next/link";
import { auth, signOut } from "@/lib/auth";
import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
function Currency({ icon, amount, label }: { icon: string; amount: number; label: string }) {
return (
<div className="flex gap-x-3 sm:gap-x-2 items-center">
<div className={`h-[25px] w-[25px] rounded-full outline-offset-[3px] ${icon}`} />
<div style={{ color: "var(--color-text-muted)" }}>
<span className="font-semibold" style={{ color: "var(--color-text)" }}>
{amount.toLocaleString()}
</span>{" "}
<span>{label}</span>
</div>
</div>
);
}
export async function TopHeader() {
const session = await auth();
if (!session?.user?.id) return null;
const id = Number(session.user.id);
let credits = 0;
let duckets = 0;
let diamonds = 0;
let look = "";
try {
const [user, currencies] = await Promise.all([
prisma.user.findUnique({ where: { id }, select: { credits: true, look: true } }),
prisma.usersCurrency.findMany({ where: { userId: id } }),
]);
credits = user?.credits ?? 0;
look = user?.look ?? "";
for (const c of currencies) {
if (c.type === 0) duckets = c.amount;
if (c.type === 5) diamonds = c.amount;
}
} catch {
// render without live balances
}
const imager =
(await siteSettings.get("habbo_imaging_url", "https://www.habbo.com/habbo-imaging/avatarimage")) ??
"";
const avatar = avatarImageUrl(imager, look, { headOnly: true, direction: 2 });
const isStaff = (session.user.rank ?? 0) >= 7;
return (
<div style={{ backgroundColor: "var(--color-surface)" }}>
<div className="max-w-7xl min-h-[60px] px-4 flex items-center justify-between mx-auto">
<div className="flex gap-x-6">
<Currency icon="nav-credit-icon" amount={credits} label="Credits" />
<Currency icon="nav-ducket-icon" amount={duckets} label="Duckets" />
<Currency icon="nav-diamond-icon" amount={diamonds} label="Diamonds" />
</div>
<div className="flex items-center gap-x-3">
{isStaff ? (
<details className="relative">
<summary
className="list-none cursor-pointer font-semibold [&::-webkit-details-marker]:hidden"
style={{ color: "var(--color-text)" }}
>
Administration
</summary>
<div
className="absolute right-0 mt-1 min-w-[180px] rounded-md shadow-lg z-50 py-1"
style={{ backgroundColor: "var(--color-dropdown)" }}
>
<Link href="/admin" className="dropdown-item">
Admin panel
</Link>
<Link href="/admin/commandocentrum" className="dropdown-item">
Commandocentrum
</Link>
<Link href="/admin/logs" className="dropdown-item">
Logs
</Link>
</div>
</details>
) : null}
<details className="relative">
<summary className="list-none cursor-pointer flex items-center [&::-webkit-details-marker]:hidden">
{/* biome-ignore lint/performance/noImgElement: external imager */}
<img className="w-[54px] h-[62px] bg-no-repeat bg-center" src={avatar} alt="" />
<span className="-ml-2 font-semibold">{session.user.name}</span>
</summary>
<div
className="absolute right-0 mt-1 min-w-[180px] rounded-md shadow-lg z-50 py-1"
style={{ backgroundColor: "var(--color-dropdown)" }}
>
<Link href="/me" className="dropdown-item">
My dashboard
</Link>
<Link href={`/u/${session.user.name}`} className="dropdown-item">
My profile
</Link>
<Link href="/settings" className="dropdown-item">
User settings
</Link>
<Link href="/settings/2fa" className="dropdown-item">
Two-factor
</Link>
<Link href="/settings/sessions" className="dropdown-item">
Login activity
</Link>
<form
action={async () => {
"use server";
await signOut({ redirectTo: "/" });
}}
>
<button
type="submit"
className="dropdown-item w-full text-center"
style={{
backgroundColor: "var(--button-danger-color)",
color: "var(--button-danger-text-color)",
}}
>
Logout
</button>
</form>
</div>
</details>
</div>
</div>
</div>
);
}