- Remove production DB dump (db_backup_*.sql) and update.log from git tracking - Add DB backups to .gitignore - Replace all console.log/console.error with structured logger module - Translate Dutch error messages to English (link-discord.ts) - Remove dead code blocks (register-form.tsx false && pattern) - Add ESLint flat config with TypeScript, React, Next.js, jsx-a11y, and security plugins - Add Prettier config - Add eslint-plugin-security for security-aware linting - Fix all 119+ ESLint warnings across the codebase: - Resolve security/detect-object-injection with safe access patterns - Resolve security/detect-non-literal-fs-filename with path traversal validation - Replace <img> with next/image <Image> component - Remove unused variables and imports - Replace non-null assertions with proper type guards - Replace <a> with <Link> for internal navigation - Use next/script Script component for external scripts - Fix setState-in-useEffect anti-patterns (navbar-color-picker, logo-generator, theme-switcher) - Add lint and format scripts to package.json All checks: typecheck ✓, tests 58/58 ✓, lint 0 errors 0 warnings ✓
143 lines
5.7 KiB
TypeScript
143 lines
5.7 KiB
TypeScript
import Link from "next/link";
|
|
import Image from "next/image";
|
|
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-navbar-text, var(--color-text-muted))" }}>
|
|
<span className="font-semibold" style={{ color: "var(--color-navbar-text, 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-navbar)" }}>
|
|
<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-navbar-text, var(--color-text))" }}
|
|
>
|
|
Administration
|
|
</summary>
|
|
<div
|
|
role="menu"
|
|
className="dropdown-menu absolute right-0 top-full mt-0.5 min-w-[180px] rounded-xl z-50 py-2"
|
|
>
|
|
<Link href="/admin" className="dropdown-item" role="menuitem">
|
|
Admin panel
|
|
</Link>
|
|
<Link href="/admin/commandocentrum" className="dropdown-item" role="menuitem">
|
|
Commandocentrum
|
|
</Link>
|
|
<Link href="/admin/logs" className="dropdown-item" role="menuitem">
|
|
Logs
|
|
</Link>
|
|
<Link href="/logo" className="dropdown-item" role="menuitem">
|
|
Logo generator
|
|
</Link>
|
|
</div>
|
|
</details>
|
|
) : null}
|
|
|
|
<details className="relative">
|
|
<summary aria-label="User menu" className="list-none cursor-pointer flex items-center [&::-webkit-details-marker]:hidden">
|
|
<Image className="bg-no-repeat bg-center" src={avatar} alt="" width={54} height={62} unoptimized />
|
|
<span className="-ml-2 font-semibold" style={{ color: "var(--color-navbar-text, var(--color-text))" }}>{session.user.name}</span>
|
|
</summary>
|
|
<div
|
|
role="menu"
|
|
className="dropdown-menu absolute right-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-2"
|
|
>
|
|
<Link href="/me" className="dropdown-item" role="menuitem">
|
|
My dashboard
|
|
</Link>
|
|
<Link href={`/u/${session.user.name}`} className="dropdown-item" role="menuitem">
|
|
My profile
|
|
</Link>
|
|
<Link href="/settings" className="dropdown-item" role="menuitem">
|
|
User settings
|
|
</Link>
|
|
<Link href="/settings/2fa" className="dropdown-item" role="menuitem">
|
|
Two-factor
|
|
</Link>
|
|
<Link href="/settings/sessions" className="dropdown-item" role="menuitem">
|
|
Login activity
|
|
</Link>
|
|
<div className="h-px my-1 mx-3" style={{ backgroundColor: "color-mix(in srgb, var(--color-text-muted) 12%, transparent)" }} />
|
|
<form
|
|
action={async () => {
|
|
"use server";
|
|
await signOut({ redirectTo: "/" });
|
|
}}
|
|
>
|
|
<button
|
|
type="submit"
|
|
role="menuitem"
|
|
className="dropdown-item w-full text-center mx-1 rounded-lg"
|
|
style={{
|
|
backgroundColor: "color-mix(in srgb, var(--button-danger-color) 12%, transparent)",
|
|
color: "var(--button-danger-color)",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
width: "calc(100% - 8px)",
|
|
}}
|
|
>
|
|
Logout
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|