299 lines
8.3 KiB
TypeScript
299 lines
8.3 KiB
TypeScript
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import type { Session } from "next-auth";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { signOut } from "@/lib/auth";
|
|
import { avatarImageUrl } from "@/lib/format";
|
|
import { cached } from "@/lib/cache";
|
|
import { canAccess, getApiAdminContext, PERMS } from "@/lib/permissions";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { resolveHotelName } from "@/lib/hotel-name";
|
|
import { siteSettings } from "@/lib/services/site-settings";
|
|
|
|
function Currency({
|
|
icon,
|
|
amount,
|
|
label,
|
|
}: {
|
|
icon: string;
|
|
amount: number;
|
|
label: string;
|
|
}) {
|
|
const textColor =
|
|
"var(--color-navbar-text-readable, var(--color-navbar-text))";
|
|
const valueColor =
|
|
"var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))";
|
|
|
|
return (
|
|
<div className="flex gap-x-1 sm:gap-x-2 items-center">
|
|
<div
|
|
className={`h-[20px] w-[20px] sm:h-[25px] sm:w-[25px] rounded-full outline-offset-[3px] ${icon}`}
|
|
/>
|
|
<div style={{ color: textColor }}>
|
|
<span
|
|
className="font-semibold text-xs sm:text-sm"
|
|
style={{ color: valueColor }}
|
|
>
|
|
{amount.toLocaleString()}
|
|
</span>{" "}
|
|
<span className="hidden sm:inline text-xs">{label}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export async function TopHeader({ session }: { session: Session | null }) {
|
|
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 });
|
|
let showAdmin = false;
|
|
let showMod = false;
|
|
const ctx = await getApiAdminContext();
|
|
if (ctx) {
|
|
showAdmin = canAccess(
|
|
ctx.permissions,
|
|
PERMS.ADMIN_DASHBOARD,
|
|
ctx.session.user.rank,
|
|
);
|
|
showMod =
|
|
canAccess(ctx.permissions, PERMS.MOD_DASHBOARD, ctx.session.user.rank) ||
|
|
canAccess(
|
|
ctx.permissions,
|
|
PERMS.MODERATION_VIEW,
|
|
ctx.session.user.rank,
|
|
) ||
|
|
canAccess(ctx.permissions, PERMS.MOD_CFH_VIEW, ctx.session.user.rank) ||
|
|
canAccess(ctx.permissions, PERMS.MOD_ACTIONS, ctx.session.user.rank);
|
|
}
|
|
const textColor =
|
|
"var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))";
|
|
|
|
const t = await getTranslations("header");
|
|
const hotelName = await resolveHotelName();
|
|
|
|
let online: number;
|
|
try {
|
|
online = await cached("online_count", 10_000, () =>
|
|
prisma.user.count({ where: { online: "1" } }),
|
|
);
|
|
} catch {
|
|
online = 0;
|
|
}
|
|
|
|
return (
|
|
<div style={{ backgroundColor: "var(--color-navbar)" }}>
|
|
<div className="max-w-7xl min-h-[44px] sm:min-h-[60px] px-2 sm:px-4 flex items-center justify-between mx-auto">
|
|
<div className="flex flex-wrap items-center gap-x-2 sm:gap-x-6">
|
|
<div
|
|
className="flex items-center gap-2 px-3 py-1 rounded-full"
|
|
style={{
|
|
background: "rgba(255,255,255,0.08)",
|
|
border: "1px solid rgba(255,255,255,0.10)",
|
|
}}
|
|
>
|
|
<span className="relative flex h-2 w-2">
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
|
|
</span>
|
|
<span
|
|
className="text-xs sm:text-sm font-bold tracking-wide"
|
|
style={{ color: "rgba(255,255,255,0.9)" }}
|
|
>
|
|
{t("online", { count: online, hotel: hotelName })}
|
|
</span>
|
|
</div>
|
|
<Link
|
|
href="/client"
|
|
className="flex items-center gap-1.5 rounded-xl border py-1.5 px-3 font-extrabold text-xs transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg"
|
|
style={{
|
|
background: "var(--color-primary)",
|
|
color: "var(--color-primary-foreground)",
|
|
borderColor: "color-mix(in srgb, var(--color-primary) 20%, transparent)",
|
|
boxShadow: "0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
|
|
}}
|
|
>
|
|
<Image
|
|
src="/assets/images/EnterHubbly.png"
|
|
alt=""
|
|
width={14}
|
|
height={14}
|
|
unoptimized
|
|
/>
|
|
Enter
|
|
</Link>
|
|
<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-1 sm:gap-x-3">
|
|
{showAdmin || showMod ? (
|
|
<details className="relative">
|
|
<summary
|
|
className="list-none cursor-pointer font-semibold text-xs sm:text-sm [&::-webkit-details-marker]:hidden"
|
|
style={{ color: textColor }}
|
|
>
|
|
<span className="hidden sm:inline">Staff</span>
|
|
<span className="sm:hidden">Staff</span>
|
|
</summary>
|
|
<div
|
|
role="menu"
|
|
className="dropdown-menu absolute right-0 top-full mt-0.5 min-w-[180px] rounded-xl z-50 py-2"
|
|
>
|
|
{showMod ? (
|
|
<Link href="/mod" className="dropdown-item" role="menuitem">
|
|
Mod panel
|
|
</Link>
|
|
) : null}
|
|
{showAdmin ? (
|
|
<>
|
|
<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>
|
|
</>
|
|
) : null}
|
|
<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="hidden sm:inline -ml-2 font-semibold"
|
|
style={{ color: textColor }}
|
|
>
|
|
{session.user.name}
|
|
</span>
|
|
</summary>
|
|
<div
|
|
role="menu"
|
|
className="dropdown-menu fixed sm:absolute right-2 sm:right-0 top-auto sm: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>
|
|
);
|
|
}
|