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.
This commit is contained in:
1 parent
4d720ee03c
commit
741f01e897
8 files changed
+694
-657
No files matched your search
+105
-177
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user