revert(site): restore classic Habbo landing with soft theme polish
CI / check (push) Successful in 2m27s
CI / deploy (push) Successful in 1m45s
CI / publish-container (push) Successful in 1m55s

Replace the premium dark-gaming redesign of home, login and register with
the original classic landing (Habbo sky background, AuthTopBar, SurfaceCard
layout). Keeps the theme background visible again and adds a subtle
theme-aware brand halo behind the hero/Frank plus a soft primary glow on
card hover.

Also upgrades dependencies: next 16.3.5, vite 8.3.0 (typescript 7.0.2 was
already latest). Temporarily lowers pnpm minimumReleaseAge to 60 min so the
fresh 16.3.5 release can be installed; restore to 1440 once it is 24h old.
This commit is contained in:
openhands committed 2026-09-12 16:14:21 +02:00
1 parent cc58f16230
commit d755ab7ce1
11 files changed
+742 -812

No files matched your search

+243 -219
View File
@@ -4,27 +4,8 @@ import { headers } from "next/headers";
import Image from "next/image";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { AmbientOrbs } from "@/components/ambient-orbs";
import { AnimatedCounter } from "@/components/animated-counter";
import { HomeLoginForm } from "@/components/auth/home-login-form";
import { LandingTopBar } from "@/components/landing-top-bar";
import Link from "@/components/link";
import { LiveOnlineCount } from "@/components/live-online-count";
import { LiveOnlineCounter } from "@/components/live-online-counter";
import { Reveal } from "@/components/motion-reveal";
import { PublicLoadError } from "@/components/public/load-error";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { SurfaceCard } from "@/components/surface-card";
import { TypewriterText } from "@/components/typewriter-text";
import { auth } from "@/lib/auth";
import { cached } from "@/lib/cache";
import { CameraWeb, db, Rooms, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha";
import { getNewsList } from "@/lib/services/news-list";
import { publicReadFailure } from "@/lib/services/public-read";
import { siteSettings } from "@/lib/services/site-settings";
export const metadata: Metadata = {
title: "Home",
@@ -38,6 +19,28 @@ export const metadata: Metadata = {
},
};
import { AmbientOrbs } from "@/components/ambient-orbs";
import { AnimatedCounter } from "@/components/animated-counter";
import { HomeLoginForm } from "@/components/auth/home-login-form";
import { Clock } from "@/components/clock";
import { LanguageSwitcher } from "@/components/language-switcher";
import Link from "@/components/link";
import { LiveOnlineCount } from "@/components/live-online-count";
import { LiveOnlineCounter } from "@/components/live-online-counter";
import { Reveal } from "@/components/motion-reveal";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { SurfaceCard } from "@/components/surface-card";
import { ThemeSwitcher } from "@/components/theme-switcher";
import { TypewriterText } from "@/components/typewriter-text";
import { auth } from "@/lib/auth";
import { cached } from "@/lib/cache";
import { CameraWeb, db, Rooms, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha";
import { getNewsList } from "@/lib/services/news-list";
import { siteSettings } from "@/lib/services/site-settings";
async function getHotelData() {
const [hotelName, logo] = await Promise.all([
resolveHotelName(),
@@ -135,211 +138,185 @@ export default async function Home() {
},
];
const stats = [
{
value: users,
label: tp("statsCitizens"),
icon: "/assets/images/icons/friends.png",
live: false,
},
{
value: rooms,
label: tp("statsRooms"),
icon: "/assets/images/icons/catalog.png",
live: false,
},
{
value: online,
label: tp("statsOnline"),
icon: "/assets/images/icons/navigation/me.png",
live: true,
},
{
value: articles?.length ?? null,
label: tp("statsArticles"),
icon: "/assets/images/icons/article.gif",
live: false,
},
];
return (
<div className="mx-auto w-full max-w-6xl flex flex-col gap-6 px-1 sm:px-0 md:gap-8 pb-12 md:pb-16">
{/* ── Top Bar ── */}
<Reveal>
<div
className="relative flex items-center justify-between rounded-xl border px-3 py-2.5 sm:px-5 sm:py-3"
style={{
background:
"color-mix(in srgb, var(--color-surface) 60%, transparent)",
backdropFilter: "blur(14px)",
WebkitBackdropFilter: "blur(14px)",
borderColor:
"color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
boxShadow:
"0 1px 3px rgba(0,0,0,0.04), 0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 6%, transparent)",
}}
>
<div className="flex items-center gap-2 sm:gap-3">
{logo ? (
<Image
src={logo}
alt={hotelName}
width={100}
height={28}
className="shrink-0 object-contain"
unoptimized
style={{ maxHeight: 28, width: "auto", height: "auto" }}
/>
) : (
<Image
src="/assets/images/icons/lighthouse.png"
alt={hotelName}
width={28}
height={28}
className="shrink-0"
unoptimized
/>
)}
<span
className="hidden sm:inline text-sm font-extrabold uppercase tracking-wide"
style={{ color: "var(--color-text-readable)" }}
>
{hotelName}
</span>
</div>
<div
className="flex items-center gap-1 sm:gap-1.5 rounded-lg px-1.5 py-1 sm:px-2"
style={{
background:
"color-mix(in srgb, var(--color-text-muted) 4%, transparent)",
}}
>
<LanguageSwitcher />
<div
className="hidden sm:block w-px h-5"
style={{
background:
"color-mix(in srgb, var(--color-text-muted) 12%, transparent)",
}}
/>
<Clock />
<div
className="w-px h-5"
style={{
background:
"color-mix(in srgb, var(--color-text-muted) 12%, transparent)",
}}
/>
<ThemeSwitcher />
</div>
</div>
</Reveal>
{/* ── Hero ── */}
<Reveal>
<section className="landing-hero w-full overflow-hidden rounded-2xl border border-white/10 sm:rounded-3xl">
<div
className="relative overflow-hidden rounded-2xl sm:rounded-3xl border"
style={{
borderColor:
"color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
boxShadow:
"0 4px 24px rgba(0,0,0,0.08), 0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 6%, transparent)",
}}
>
<Image
src="/assets/images/next_header.jpg"
alt=""
fill
priority
sizes="100vw"
style={{ objectFit: "cover", objectPosition: "center 20%" }}
/>
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-surface) 92%, transparent) 0%, color-mix(in srgb, var(--color-surface) 45%, transparent) 50%, color-mix(in srgb, var(--color-surface) 12%, transparent) 100%)",
}}
/>
<AmbientOrbs />
<LandingTopBar hotelName={hotelName} logo={logo} />
<div className="relative z-10 grid items-center gap-10 px-4 py-10 sm:px-7 sm:py-14 lg:grid-cols-[1.15fr_0.85fr] lg:px-10 lg:py-16">
{/* Left: copy */}
<div className="flex flex-col items-start">
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 landing-chip px-3.5 py-1.5 text-[11px] font-extrabold uppercase tracking-widest landing-text-secondary">
<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>
{online === null ? (
<span>{te("unavailable")}</span>
) : (
<LiveOnlineCount initial={online} hotelName={hotelName} />
)}
<div
className="relative flex flex-col items-center gap-5 px-4 py-10 text-center sm:gap-8 sm:px-6 sm:py-12 md:px-12 md:py-20"
style={{ zIndex: 2 }}
>
<div
className="brand-halo left-1/2 top-4 h-48 w-48 -translate-x-1/2 sm:top-6 sm:h-64 sm:w-64"
style={{ zIndex: -1 }}
/>
<div
className="inline-flex items-center gap-2 px-3 py-1 sm:px-4 sm:py-1.5 rounded-full text-[10px] sm:text-xs font-bold uppercase tracking-widest mb-1 border backdrop-blur-sm"
style={{
background:
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
borderColor:
"color-mix(in srgb, var(--color-primary) 20%, 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>
<h1 className="mt-6 text-4xl font-black leading-[1.02] tracking-tight landing-text-primary sm:text-5xl lg:text-6xl">
{tp("welcome", { hotelName: "" }).trim()}
<TypewriterText text={hotelName} className="text-gradient" />
</h1>
<p className="mt-5 max-w-xl text-sm leading-relaxed landing-text-muted sm:text-base md:text-lg">
{tp("welcomeBody")}
</p>
<div className="mt-8 flex flex-wrap gap-3">
<Link
href="/register"
className="btn-shine inline-flex items-center gap-2 rounded-xl font-extrabold text-sm sm:text-base px-6 sm:px-8 py-3.5 sm:py-4 transition-all duration-200 hover:scale-[1.03] active:scale-95"
style={{
background: "var(--color-primary)",
color: "var(--color-primary-foreground)",
boxShadow:
"0 8px 32px color-mix(in srgb, var(--color-primary) 40%, transparent)",
}}
>
<Image
src="/assets/images/EnterHubbly.png"
alt=""
width={20}
height={14}
className="object-contain"
unoptimized
/>
{tp("register")}
</Link>
<Link
href="/login"
className="inline-flex items-center gap-2 rounded-xl border font-bold text-sm sm:text-base px-6 sm:px-8 py-3.5 sm:py-4 transition-all duration-200 hover:scale-[1.03] active:scale-95"
style={{
borderColor: "color-mix(in srgb, #ffffff 22%, transparent)",
color: "#ffffff",
background: "color-mix(in srgb, #ffffff 6%, transparent)",
}}
>
{th("login")}
</Link>
</div>
{online === null ? (
<span>{te("unavailable")}</span>
) : (
<LiveOnlineCount initial={online} hotelName={hotelName} />
)}
</div>
{/* Right: visual */}
<div className="relative hidden items-center justify-center md:flex">
<span className="hero-halo h-80 w-80" />
<div
className="relative z-10 w-full max-w-[26rem] -rotate-2 rounded-3xl border border-white/15 landing-chip p-7 shadow-2xl backdrop-blur-xl transition-transform duration-500 hover:rotate-0"
<h1
className="text-2xl font-black leading-[1.05] tracking-tight mb-2 sm:text-3xl md:text-5xl lg:text-6xl"
style={{
color: "var(--color-text-readable)",
fontFamily: "var(--font-nunito)",
}}
>
<TypewriterText text={tp("welcome", { hotelName })} />
</h1>
<p
className="text-sm leading-relaxed max-w-2xl mb-5 sm:text-base md:text-lg md:mb-8"
style={{ color: "var(--color-text-muted)" }}
>
{tp("welcomeBody")}
</p>
<div className="flex flex-wrap gap-2.5 sm:gap-3 justify-center">
<Link
href="/register"
className="inline-flex items-center gap-2 sm:gap-2.5 font-extrabold text-xs sm:text-sm px-5 sm:px-8 py-3 sm:py-4 rounded-xl transition-all duration-200 hover:scale-[1.03] active:scale-95 shadow-lg"
style={{
background: "var(--color-primary)",
color: "var(--color-primary-foreground)",
boxShadow:
"0 40px 90px -30px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.1)",
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
}}
>
<div
className="absolute inset-x-0 top-0 h-0.5 rounded-t-3xl"
style={{
background:
"linear-gradient(90deg, var(--color-primary), var(--color-accent))",
}}
/>
<Image
src="/assets/images/EnterHubbly.png"
alt={hotelName}
width={415}
height={128}
className="h-auto w-full object-contain drop-shadow-[0_8px_20px_rgba(0,0,0,0.4)]"
alt=""
width={20}
height={20}
unoptimized
/>
<p className="mt-4 text-center text-sm leading-relaxed landing-text-faint">
{tp("welcomeSubtitle")}
</p>
<div className="mt-5 space-y-2.5">
<div className="h-2 rounded-full landing-chip-strong">
<div
className="h-full rounded-full"
style={{
width: "78%",
background:
"linear-gradient(90deg, var(--color-primary), var(--color-accent))",
}}
/>
</div>
<div className="h-2 rounded-full landing-chip-strong">
<div
className="h-full w-1/2 rounded-full"
style={{
background:
"linear-gradient(90deg, var(--color-primary), var(--color-accent))",
}}
/>
</div>
</div>
</div>
<div className="absolute -left-4 top-8 z-20 rounded-2xl border border-white/15 bg-[#0b1020]/80 p-3.5 pl-4 backdrop-blur-xl animate-float">
<p className="m-0 text-lg font-black leading-none landing-text-primary">
<LiveOnlineCounter initial={online ?? 0} />
</p>
<p className="mt-1 text-[10px] font-bold uppercase tracking-widest landing-text-faint">
{tp("statsOnline")}
</p>
</div>
<div
className="absolute -right-4 bottom-8 z-20 rounded-2xl border border-white/15 bg-[#0b1020]/80 p-3.5 pl-4 backdrop-blur-xl animate-float"
style={{ animationDelay: "1.2s" }}
{tp("register")}
</Link>
<Link
href="/login"
className="inline-flex items-center gap-2 font-bold text-xs sm:text-sm px-5 sm:px-8 py-3 sm:py-4 rounded-xl border-2 transition-all duration-200 hover:scale-[1.03] active:scale-95 backdrop-blur-sm"
style={{
borderColor:
"color-mix(in srgb, var(--color-text-muted) 20%, transparent)",
color: "var(--color-text-readable)",
background:
"color-mix(in srgb, var(--color-surface) 50%, transparent)",
}}
>
<p className="m-0 text-lg font-black leading-none landing-text-primary">
<AnimatedCounter value={users ?? 0} />
</p>
<p className="mt-1 text-[10px] font-bold uppercase tracking-widest landing-text-faint">
{tp("statsCitizens")}
</p>
</div>
{th("login")}
</Link>
</div>
</div>
{/* Stats strip */}
<div className="relative z-10 border-t border-white/10 px-4 py-6 sm:px-7 lg:px-10">
<div className="grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-4">
{stats.map((s) => (
<div
key={s.label}
className="rounded-2xl border border-white/10 landing-chip p-4 transition-all duration-300 hover:-translate-y-0.5 hover:border-[color-mix(in_srgb,var(--color-primary)_40%,transparent)]"
>
<div className="flex items-center justify-between gap-2">
<span className="text-2xl font-black tracking-tight landing-text-primary sm:text-3xl">
{s.value === null ? (
<span className="text-sm font-bold landing-text-faint">
{te("unavailable")}
</span>
) : s.live ? (
<LiveOnlineCounter initial={s.value} />
) : (
<AnimatedCounter value={s.value} />
)}
</span>
<Image
src={s.icon}
alt=""
width={18}
height={18}
className="opacity-60"
unoptimized
/>
</div>
<p className="mt-1 text-[10px] font-bold uppercase tracking-widest landing-text-faint sm:text-xs">
{s.label}
</p>
</div>
))}
</div>
</div>
</section>
</div>
</Reveal>
{/* ── Features ── */}
@@ -348,28 +325,21 @@ export default async function Home() {
{features.map((f) => (
<SurfaceCard
key={f.title}
className="group relative overflow-hidden p-5 sm:p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
className="card-glow relative p-4 sm:p-5 text-center hover:-translate-y-1"
>
<div
className="absolute inset-x-0 top-0 h-1 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
className="mx-auto mb-3 flex h-11 w-11 sm:h-12 sm:w-12 items-center justify-center rounded-xl"
style={{
background:
"linear-gradient(90deg, var(--color-primary), var(--color-accent))",
}}
/>
<div
className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl transition-transform duration-300 group-hover:scale-110"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 18%, var(--color-surface)), color-mix(in srgb, var(--color-primary) 8%, var(--color-surface)))",
"color-mix(in srgb, var(--color-primary) 14%, transparent)",
boxShadow:
"inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 14%, transparent)",
"inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent)",
}}
>
<Image src={f.icon} alt="" width={24} height={24} unoptimized />
</div>
<h3
className="text-center text-base font-extrabold"
className="text-sm sm:text-base font-extrabold mb-1"
style={{
color: "var(--color-text-readable)",
fontFamily: "var(--font-nunito)",
@@ -378,7 +348,7 @@ export default async function Home() {
{f.title}
</h3>
<p
className="mt-1 text-center text-sm leading-relaxed"
className="text-xs sm:text-sm leading-relaxed"
style={{ color: "var(--color-text-muted)" }}
>
{f.desc}
@@ -388,6 +358,60 @@ export default async function Home() {
</div>
</Reveal>
{/* ── Stats + Login ── */}
<Reveal>
<div className="grid grid-cols-2 gap-3 sm:gap-4 md:grid-cols-4">
{[
{
value: users,
label: tp("statsCitizens"),
icon: "/assets/images/icons/friends.png",
live: false,
},
{
value: rooms,
label: tp("statsRooms"),
icon: "/assets/images/icons/catalog.png",
live: false,
},
{
value: online,
label: tp("statsOnline"),
icon: "/assets/images/icons/navigation/me.png",
live: true,
},
{
value: articles?.length ?? null,
label: tp("statsArticles"),
icon: "/assets/images/icons/article.gif",
live: false,
},
].map((s) => (
<SurfaceCard
key={s.label}
className="card-glow relative p-3 sm:p-5 text-center hover:-translate-y-1"
>
<div className="mb-0.5 text-2xl sm:text-3xl font-black tracking-tight md:text-4xl">
{s.value === null ? (
<span className="text-base">{te("unavailable")}</span>
) : s.live ? (
<LiveOnlineCounter initial={s.value} />
) : (
<AnimatedCounter value={s.value} />
)}
</div>
<div
className="text-[10px] sm:text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-1 sm:gap-1.5"
style={{ color: "var(--color-text-muted)" }}
>
<Image src={s.icon} alt="" width={14} height={14} unoptimized />
{s.label}
</div>
</SurfaceCard>
))}
</div>
</Reveal>
{/* ── Login + Online users ── */}
<div className="flex flex-col gap-4 md:flex-row">
<Reveal className="flex w-full flex-col gap-4 md:w-1/3 md:flex-shrink-0">