Files
Epicnabbo-Catalogus-Updated…/src/app/(site)/login/page.tsx
T
openhands 741f01e897 feat(site): redesign home, login and register with premium dark gaming theme
Replaced the classic Habbo landing style on the home, login and register pages with a modern premium dark-gaming look: always-dark hero canvas with brand glows, grid overlay and ambient orbs, glass panels, gradient text and floating art. Adds shared LandingTopBar, AuthShell and BrandFrank components plus reusable premium CSS utilities. Build, typecheck and lint pass.
2026-09-12 15:40:54 +02:00

158 lines
5.2 KiB
TypeScript

import { count, eq } from "drizzle-orm";
import { headers } from "next/headers";
import Image from "next/image";
import { getTranslations } from "next-intl/server";
import { AuthShell } from "@/components/auth/auth-shell";
import { BrandFrank } from "@/components/auth/brand-frank";
import { LoginForm } from "@/components/auth/login-form";
import Link from "@/components/link";
import { Reveal } from "@/components/motion-reveal";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { cached } from "@/lib/cache";
import { db, User } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings";
export const metadata = {
title: "Sign in",
};
export default async function LoginPage() {
const t = await getTranslations("pages.login");
const th = await getTranslations("header");
const thp = await getTranslations("pages.home");
const [hotelName, cfg, logo] = await Promise.all([
resolveHotelName(),
captchaConfig(),
siteSettings.get("cms_logo", ""),
]);
const nonce = (await headers()).get("x-nonce") ?? undefined;
const [online, totalUsers, recentUsers] = await Promise.all([
cached("online_count", 10_000, () =>
db
.select({ total: count() })
.from(User)
.where(eq(User.online, "1"))
.then((rows) => rows[0]?.total ?? 0),
).catch(() => 0),
cached("total_users", 300_000, () =>
db
.select({ total: count() })
.from(User)
.then((rows) => rows[0]?.total ?? 0),
).catch(() => 0),
db
.select({ username: User.username, look: User.look })
.from(User)
.where(eq(User.online, "1"))
.limit(6)
.catch(() => []),
]);
const onlineLabel = th("online", { count: online, hotel: "" }).trim();
return (
<div className="mx-auto w-full max-w-6xl flex flex-col gap-6 pb-16">
<Reveal>
<AuthShell
hotelName={hotelName}
logo={logo}
footer={
<p className="m-0 text-sm text-white/60">
{t("noAccount") ?? "No account?"}{" "}
<Link
href="/register"
className="font-extrabold text-white hover:text-white/80 hover:underline"
>
{t("createOne") ?? "Create one"}
</Link>
</p>
}
side={
<div className="flex flex-1 flex-col">
<div className="text-left">
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3.5 py-1.5 text-[11px] font-extrabold uppercase tracking-widest text-white/80">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--color-primary)] opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[var(--color-primary)]" />
</span>
{onlineLabel}
</span>
<h1 className="mt-5 text-3xl font-black leading-[1.08] tracking-tight text-white sm:text-4xl lg:text-5xl">
{t("welcomeBack")}
<br />
<span className="text-gradient">{hotelName}</span>
</h1>
<p className="mt-4 max-w-md text-sm leading-relaxed text-white/60 sm:text-base">
{t("welcomeBackSub", { hotelName })}
</p>
</div>
<div className="my-6 flex justify-center sm:my-8">
<BrandFrank size={160} />
</div>
<div className="mt-auto grid grid-cols-2 gap-3">
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="text-2xl font-black text-white">{online}</div>
<p className="mt-1 text-[11px] font-bold uppercase tracking-widest text-[var(--color-primary)]">
{onlineLabel}
</p>
{recentUsers.length > 0 && (
<div className="mt-3 flex items-center -space-x-2.5">
{recentUsers.slice(0, 5).map((u) => (
<UserAvatarThumbnail
key={u.username}
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-full border-2 border-[#0b1020]"
/>
))}
</div>
)}
</div>
<div className="flex flex-col justify-center rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="flex items-center gap-2">
<span className="text-2xl font-black text-white">
{totalUsers}
</span>
<Image
src="/assets/images/icons/friends.png"
alt=""
width={18}
height={18}
unoptimized
className="opacity-80"
/>
</div>
<p className="mt-1 text-[11px] font-bold uppercase tracking-widest text-white/55">
{thp("statsCitizens")}
</p>
</div>
</div>
</div>
}
>
<div className="mb-6">
<h2 className="m-0 text-2xl font-black tracking-tight text-white">
{t("title")}
</h2>
<p className="mt-1.5 text-sm text-white/55">{t("subtitle")}</p>
</div>
<LoginForm
captcha={{
provider: cfg.provider,
siteKey: cfg.siteKey || undefined,
field: cfg.field || undefined,
}}
nonce={nonce}
/>
</AuthShell>
</Reveal>
</div>
);
}