feat(site): redesign home, login and register with premium dark gaming theme
CI / check (push) Failing after 1m5s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped

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.
This commit is contained in:
openhands committed 2026-09-12 15:40:54 +02:00
1 parent 4d720ee03c
commit 741f01e897
8 files changed
+694 -657

No files matched your search

+105 -177
View File
@@ -1,22 +1,27 @@
import { count, desc, eq } from "drizzle-orm";
import { count, eq } from "drizzle-orm";
import { headers } from "next/headers";
import Image from "next/image";
import { getTranslations } from "next-intl/server";
import { AuthTopBar } from "@/components/auth/auth-top-bar";
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 { SurfaceCard } from "@/components/surface-card";
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(),
@@ -24,7 +29,7 @@ export default async function LoginPage() {
]);
const nonce = (await headers()).get("x-nonce") ?? undefined;
const [online, recentUsers, latestUsers] = await Promise.all([
const [online, totalUsers, recentUsers] = await Promise.all([
cached("online_count", 10_000, () =>
db
.select({ total: count() })
@@ -32,197 +37,120 @@ export default async function LoginPage() {
.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(8)
.catch(() => []),
db
.select({ username: User.username, look: User.look })
.from(User)
.orderBy(desc(User.accountCreated))
.limit(8)
.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-8 pb-16">
<AuthTopBar hotelName={hotelName} logo={logo} />
<div className="mx-auto w-full max-w-6xl flex flex-col gap-6 pb-16">
<Reveal>
<div className="flex flex-col gap-8 lg:flex-row">
{/* Left panel */}
<div className="lg:w-80 shrink-0 space-y-4">
<SurfaceCard
className="relative p-6 text-center"
style={{
borderColor:
"color-mix(in srgb, var(--color-primary) 20%, transparent)",
background: `url(/assets/images/background.png) center/cover no-repeat`,
}}
>
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-surface) 85%, transparent) 0%, color-mix(in srgb, var(--color-surface) 40%, transparent) 100%)",
}}
/>
<div className="relative">
<Image
src="/assets/images/FrankwithBag.gif"
alt="Frank"
width={130}
height={170}
className="object-contain mx-auto drop-shadow-xl animate-float"
unoptimized
priority
/>
<div
className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full text-xs font-bold uppercase tracking-wider mt-4 mb-3 border"
style={{
borderColor:
"color-mix(in srgb, var(--color-primary) 18%, transparent)",
background:
"color-mix(in srgb, var(--color-primary) 10%, transparent)",
color:
"var(--color-primary-readable, var(--color-primary))",
}}
>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span>
{th("online", { count: online, hotel: "" }).trim()}
</div>
<h1
className="text-xl font-black"
style={{
color: "var(--color-text-readable)",
fontFamily: "var(--font-nunito)",
}}
>
{t("welcomeBack")}
</h1>
<p
className="text-xs mt-1 mx-auto max-w-[200px] leading-relaxed"
style={{ color: "var(--color-text-muted)" }}
>
{t("welcomeBackSub", { hotelName })}
</p>
</div>
</SurfaceCard>
{recentUsers.length > 0 && (
<SurfaceCard
title={th("online", { count: online, hotel: "" }).trim()}
icon="/assets/images/icons/navigation/goody.png"
bodyClassName="p-4"
>
<div className="grid grid-cols-4 gap-1.5">
{recentUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-0.5 p-1.5 rounded-lg"
style={{
background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
}}
>
<UserAvatarThumbnail
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-md"
/>
<span
className="text-[8px] font-bold truncate w-full text-center leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</SurfaceCard>
)}
{latestUsers.length > 0 && (
<SurfaceCard
title="Newest citizens"
icon="/assets/images/icons/friends.png"
bodyClassName="p-4"
>
<div className="grid grid-cols-4 gap-1.5">
{latestUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-0.5 p-1.5 rounded-lg"
style={{
background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
}}
>
<UserAvatarThumbnail
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-md"
/>
<span
className="text-[8px] font-bold truncate w-full text-center leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</SurfaceCard>
)}
<p
className="text-xs text-center"
style={{ color: "var(--color-text-muted)" }}
>
<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 hover:underline"
style={{
color: "var(--color-primary-readable, var(--color-primary))",
}}
className="font-extrabold text-white hover:text-white/80 hover:underline"
>
{t("createOne") ?? "Create one"}
</Link>
</p>
</div>
}
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>
{/* Right: form */}
<div className="flex-1">
<SurfaceCard
title={t("title")}
icon="/assets/images/icons/navigation/me.png"
bodyClassName="p-6"
>
<p
className="text-sm mb-6"
style={{ color: "var(--color-text-muted)" }}
>
{t("subtitle")}
</p>
<LoginForm
captcha={{
provider: cfg.provider,
siteKey: cfg.siteKey || undefined,
field: cfg.field || undefined,
}}
nonce={nonce}
/>
</SurfaceCard>
<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>
</div>
<LoginForm
captcha={{
provider: cfg.provider,
siteKey: cfg.siteKey || undefined,
field: cfg.field || undefined,
}}
nonce={nonce}
/>
</AuthShell>
</Reveal>
</div>
);