revert(site): restore classic Habbo landing with soft theme polish
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:
1 parent
cc58f16230
commit
d755ab7ce1
11 files changed
+742
-812
No files matched your search
+175
-121
@@ -2,26 +2,21 @@ import { count, desc, 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 { AuthTopBar } from "@/components/auth/auth-top-bar";
|
||||
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(),
|
||||
@@ -29,7 +24,7 @@ export default async function LoginPage() {
|
||||
]);
|
||||
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
||||
|
||||
const [online, totalUsers, recentUsers, latestUsers] = await Promise.all([
|
||||
const [online, recentUsers, latestUsers] = await Promise.all([
|
||||
cached("online_count", 10_000, () =>
|
||||
db
|
||||
.select({ total: count() })
|
||||
@@ -37,141 +32,200 @@ 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(6)
|
||||
.limit(8)
|
||||
.catch(() => []),
|
||||
db
|
||||
.select({ username: User.username, look: User.look })
|
||||
.from(User)
|
||||
.orderBy(desc(User.accountCreated))
|
||||
.limit(6)
|
||||
.limit(8)
|
||||
.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">
|
||||
<div className="mx-auto w-full max-w-6xl flex flex-col gap-8 pb-16">
|
||||
<AuthTopBar hotelName={hotelName} logo={logo} />
|
||||
|
||||
<Reveal>
|
||||
<AuthShell
|
||||
hotelName={hotelName}
|
||||
logo={logo}
|
||||
footer={
|
||||
<p className="m-0 text-sm landing-text-muted">
|
||||
<div className="flex flex-col gap-8 lg:flex-row">
|
||||
{/* Left panel */}
|
||||
<div className="lg:w-80 shrink-0 space-y-4">
|
||||
<SurfaceCard
|
||||
className="card-glow 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">
|
||||
<div className="brand-halo left-1/2 top-8 h-52 w-52 -translate-x-1/2" />
|
||||
<Image
|
||||
src="/assets/images/FrankwithBag.gif"
|
||||
alt="Frank"
|
||||
width={130}
|
||||
height={170}
|
||||
className="relative 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"
|
||||
className="card-glow"
|
||||
>
|
||||
<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"
|
||||
className="card-glow"
|
||||
>
|
||||
<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)" }}
|
||||
>
|
||||
{t("noAccount") ?? "No account?"}{" "}
|
||||
<Link
|
||||
href="/register"
|
||||
className="font-extrabold landing-text-primary hover:underline"
|
||||
className="font-extrabold hover:underline"
|
||||
style={{
|
||||
color: "var(--color-primary-readable, var(--color-primary))",
|
||||
}}
|
||||
>
|
||||
{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 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>
|
||||
{onlineLabel}
|
||||
</span>
|
||||
<h1 className="mt-5 text-3xl font-black leading-[1.08] tracking-tight landing-text-primary 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 landing-text-muted 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 landing-chip p-4">
|
||||
<div className="landing-text-primary text-2xl font-black">
|
||||
{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 landing-chip p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="landing-text-primary text-2xl font-black">
|
||||
{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 landing-text-faint">
|
||||
{thp("statsCitizens")}
|
||||
</p>
|
||||
{latestUsers.length > 0 && (
|
||||
<div className="mt-3 flex items-center -space-x-2.5">
|
||||
{latestUsers.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>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<h2 className="m-0 text-2xl font-black tracking-tight landing-text-primary">
|
||||
{t("title")}
|
||||
</h2>
|
||||
<p className="mt-1.5 text-sm landing-text-muted">{t("subtitle")}</p>
|
||||
</div>
|
||||
<LoginForm
|
||||
captcha={{
|
||||
provider: cfg.provider,
|
||||
siteKey: cfg.siteKey || undefined,
|
||||
field: cfg.field || undefined,
|
||||
}}
|
||||
nonce={nonce}
|
||||
/>
|
||||
</AuthShell>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
);
|
||||
|
||||
+243
-219
@@ -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">
|
||||
|
||||
+191
-132
@@ -1,24 +1,21 @@
|
||||
import { count, desc, 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 { AuthTopBar } from "@/components/auth/auth-top-bar";
|
||||
import { RegisterForm } from "@/components/auth/register-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: "Create account",
|
||||
};
|
||||
|
||||
export default async function RegisterPage() {
|
||||
const tpr = await getTranslations("pages.register");
|
||||
const th = await getTranslations("pages.home");
|
||||
const [hotelName, cfg, logo] = await Promise.all([
|
||||
resolveHotelName(),
|
||||
captchaConfig(),
|
||||
@@ -26,7 +23,7 @@ export default async function RegisterPage() {
|
||||
]);
|
||||
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
||||
|
||||
const [online, latestUsers, recentUsers] = await Promise.all([
|
||||
const [online, recentUsers, latestUsers] = await Promise.all([
|
||||
cached("online_count", 10_000, () =>
|
||||
db
|
||||
.select({ total: count() })
|
||||
@@ -34,148 +31,210 @@ export default async function RegisterPage() {
|
||||
.where(eq(User.online, "1"))
|
||||
.then((rows) => rows[0]?.total ?? 0),
|
||||
).catch(() => 0),
|
||||
cached("register_online_users", 10_000, () =>
|
||||
db
|
||||
.select({ username: User.username, look: User.look })
|
||||
.from(User)
|
||||
.where(eq(User.online, "1"))
|
||||
.limit(8),
|
||||
).catch(() => []),
|
||||
cached("register_latest_users", 30_000, () =>
|
||||
db
|
||||
.select({ username: User.username, look: User.look })
|
||||
.from(User)
|
||||
.orderBy(desc(User.accountCreated))
|
||||
.limit(6),
|
||||
.limit(8),
|
||||
).catch(() => []),
|
||||
db
|
||||
.select({ username: User.username, look: User.look })
|
||||
.from(User)
|
||||
.where(eq(User.online, "1"))
|
||||
.limit(6)
|
||||
.catch(() => []),
|
||||
]);
|
||||
|
||||
const benefits = [
|
||||
{ title: th("featuresAvatar"), desc: th("featuresAvatarDesc") },
|
||||
{ title: th("featuresFurniture"), desc: th("featuresFurnitureDesc") },
|
||||
{ title: th("featuresCommunity"), desc: th("featuresCommunityDesc") },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-6xl flex flex-col gap-6 pb-16">
|
||||
<Reveal>
|
||||
<AuthShell
|
||||
hotelName={hotelName}
|
||||
logo={logo}
|
||||
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 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>
|
||||
{tpr("usersOnline", { count: online })}
|
||||
</span>
|
||||
<h1 className="mt-5 text-3xl font-black leading-[1.08] tracking-tight landing-text-primary sm:text-4xl lg:text-5xl">
|
||||
{tpr("title")}
|
||||
<br />
|
||||
<span className="text-gradient">{hotelName}</span>
|
||||
</h1>
|
||||
<p className="mt-4 max-w-md text-sm leading-relaxed landing-text-muted sm:text-base">
|
||||
{tpr("subtitle")}
|
||||
</p>
|
||||
{recentUsers.length > 0 && (
|
||||
<div className="mt-6 flex items-center gap-3">
|
||||
<div className="flex -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>
|
||||
<p className="m-0 text-[11px] font-extrabold uppercase tracking-widest landing-text-faint">
|
||||
{tpr("whoIsOnline")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mx-auto w-full max-w-6xl flex flex-col gap-8 pb-16">
|
||||
<AuthTopBar hotelName={hotelName} logo={logo} />
|
||||
|
||||
<div className="mt-6 hidden grid-cols-1 gap-2.5 sm:grid">
|
||||
{benefits.map((b) => (
|
||||
<div
|
||||
key={b.title}
|
||||
className="flex items-center gap-3 rounded-xl border border-white/10 landing-chip px-4 py-3"
|
||||
>
|
||||
<span
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full"
|
||||
<Reveal>
|
||||
<div className="flex flex-col gap-8 lg:flex-row lg:items-start">
|
||||
{/* Left: intro panel */}
|
||||
<div className="lg:w-96 shrink-0 space-y-4">
|
||||
<SurfaceCard
|
||||
className="card-glow relative overflow-hidden p-6"
|
||||
style={{
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 20%, transparent)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
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) 88%, transparent) 0%, color-mix(in srgb, var(--color-surface) 35%, transparent) 100%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="shrink-0 animate-float">
|
||||
<div className="brand-halo left-1/2 top-1/2 h-52 w-52 -translate-x-1/2 -translate-y-1/2" />
|
||||
<Image
|
||||
src="/assets/images/FrankwithBag.gif"
|
||||
alt="Frank"
|
||||
width={110}
|
||||
height={145}
|
||||
className="relative object-contain drop-shadow-xl"
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div
|
||||
className="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-2 border"
|
||||
style={{
|
||||
color: "var(--color-primary)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 18%, transparent)",
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 15%, transparent)",
|
||||
"color-mix(in srgb, var(--color-primary) 10%, transparent)",
|
||||
color:
|
||||
"var(--color-primary-readable, var(--color-primary))",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<title>{b.title}</title>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="m-0 text-sm font-extrabold landing-text-primary">
|
||||
{b.title}
|
||||
</p>
|
||||
<p className="m-0 truncate text-xs landing-text-muted">
|
||||
{b.desc}
|
||||
</p>
|
||||
<span className="w-2 h-2 rounded-full bg-primary" />
|
||||
{tpr("usersOnline", { count: online })}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-6 sm:pt-8">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<BrandFrank size={130} />
|
||||
<div className="mb-1 text-right">
|
||||
{latestUsers.length > 0 && (
|
||||
<>
|
||||
<p className="m-0 text-[11px] font-extrabold uppercase tracking-widest landing-text-faint">
|
||||
Latest citizens
|
||||
</p>
|
||||
<div className="mt-2 flex items-center justify-end -space-x-2.5">
|
||||
{latestUsers.slice(0, 5).map((u) => (
|
||||
<UserAvatarThumbnail
|
||||
key={u.username}
|
||||
figure={u.look}
|
||||
alt=""
|
||||
options={{ direction: 2 }}
|
||||
className="rounded-full border-2 border-[#0b1020]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<h1
|
||||
className="text-lg font-black leading-tight"
|
||||
style={{
|
||||
color: "var(--color-text-readable)",
|
||||
fontFamily: "var(--font-nunito)",
|
||||
}}
|
||||
>
|
||||
{tpr("title")} — {hotelName}
|
||||
</h1>
|
||||
<p
|
||||
className="text-xs mt-1.5 leading-relaxed"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{tpr("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RegisterForm
|
||||
hotelName={hotelName}
|
||||
captcha={{
|
||||
provider: cfg.provider,
|
||||
siteKey: cfg.siteKey || undefined,
|
||||
}}
|
||||
nonce={nonce}
|
||||
/>
|
||||
</AuthShell>
|
||||
</SurfaceCard>
|
||||
|
||||
{recentUsers.length > 0 && (
|
||||
<SurfaceCard
|
||||
title={tpr("whoIsOnline")}
|
||||
icon="/assets/images/icons/navigation/goody.png"
|
||||
bodyClassName="p-4"
|
||||
className="card-glow"
|
||||
>
|
||||
<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"
|
||||
className="card-glow"
|
||||
>
|
||||
<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-[10px] font-bold truncate w-full text-center leading-tight"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
{u.username}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SurfaceCard>
|
||||
)}
|
||||
|
||||
<p
|
||||
className="text-sm text-center"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{tpr("alreadyHaveAccountPre")}{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="font-extrabold hover:underline"
|
||||
style={{
|
||||
color: "var(--color-primary-readable, var(--color-primary))",
|
||||
}}
|
||||
>
|
||||
{tpr("alreadyHaveAccountLink")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right: form */}
|
||||
<div className="flex-1 lg:sticky lg:top-6">
|
||||
<SurfaceCard
|
||||
title={tpr("title")}
|
||||
icon="/assets/images/icons/navigation/me.png"
|
||||
bodyClassName="p-6"
|
||||
>
|
||||
<p
|
||||
className="text-sm mb-6"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{tpr("subtitle")}
|
||||
</p>
|
||||
<RegisterForm
|
||||
hotelName={hotelName}
|
||||
captcha={{
|
||||
provider: cfg.provider,
|
||||
siteKey: cfg.siteKey || undefined,
|
||||
}}
|
||||
nonce={nonce}
|
||||
/>
|
||||
</SurfaceCard>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
);
|
||||
|
||||
+16
-123
@@ -1993,135 +1993,28 @@ details[open] > summary .details-open\:rotate-180 {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
/* ── Premium landing kit (dark gaming style) ───────────────────
|
||||
Shared primitives for the redesigned home / login / register
|
||||
pages. Everything stays theme-aware: brand colors are driven by
|
||||
--color-primary / --color-accent, and the "always-dark" hero
|
||||
canvas is mixed from the surface token so it blends with both
|
||||
light and dark hotel themes. */
|
||||
.landing-hero {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(
|
||||
60em 40em at 12% -10%,
|
||||
color-mix(in srgb, var(--color-primary) 22%, transparent) 0%,
|
||||
transparent 60%
|
||||
),
|
||||
radial-gradient(
|
||||
50em 36em at 108% 12%,
|
||||
color-mix(in srgb, var(--color-accent) 18%, transparent) 0%,
|
||||
transparent 62%
|
||||
),
|
||||
linear-gradient(
|
||||
175deg,
|
||||
color-mix(in srgb, var(--color-surface) 6%, #05070d) 0%,
|
||||
color-mix(in srgb, var(--color-surface) 8%, #0a0f1d) 55%,
|
||||
color-mix(in srgb, var(--color-background) 45%, #0a0f1d) 100%
|
||||
);
|
||||
color-scheme: dark;
|
||||
}
|
||||
.landing-hero::after {
|
||||
content: "";
|
||||
/* ── Classic-landing polish (theme-aware) ────────────────────────────
|
||||
Keeps the Habbo sky and theme colors; adds subtle depth + glow to the
|
||||
restored landing pages. Everything is driven by theme CSS variables. */
|
||||
.brand-halo {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to right,
|
||||
color-mix(in srgb, #ffffff 5%, transparent) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
color-mix(in srgb, #ffffff 5%, transparent) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 44px 44px;
|
||||
-webkit-mask-image: linear-gradient(to bottom, #000 30%, transparent 92%);
|
||||
mask-image: linear-gradient(to bottom, #000 30%, transparent 92%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Text with a brand gradient (primary → accent). */
|
||||
.text-gradient {
|
||||
background: linear-gradient(
|
||||
115deg,
|
||||
var(--color-primary) 10%,
|
||||
color-mix(in srgb, var(--color-primary) 55%, var(--color-accent)) 48%,
|
||||
var(--color-accent) 92%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* Primary-tinted glass card used for auth forms / panels. */
|
||||
.glass-panel {
|
||||
background: color-mix(in srgb, #0b1020 62%, transparent);
|
||||
backdrop-filter: blur(22px);
|
||||
-webkit-backdrop-filter: blur(22px);
|
||||
border: 1px solid color-mix(in srgb, #ffffff 10%, transparent);
|
||||
box-shadow:
|
||||
0 30px 80px -20px rgba(0, 0, 0, 0.55),
|
||||
inset 0 1px 0 color-mix(in srgb, #ffffff 9%, transparent);
|
||||
}
|
||||
|
||||
/* Soft animated halo behind floating hero art. */
|
||||
.hero-halo {
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
color-mix(in srgb, var(--color-primary) 55%, transparent) 0%,
|
||||
transparent 68%
|
||||
color-mix(in srgb, var(--color-primary) 30%, transparent) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
filter: blur(6px);
|
||||
animation: hero-halo-pulse 5s ease-in-out infinite;
|
||||
filter: blur(20px);
|
||||
}
|
||||
@keyframes hero-halo-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.55;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.9;
|
||||
transform: scale(1.07);
|
||||
}
|
||||
.card-glow {
|
||||
transition:
|
||||
box-shadow 0.3s ease,
|
||||
transform 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-halo {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* White-on-dark overlay text/backgrounds for the landing canvas. Kept as
|
||||
plain CSS so source files stay free of hardcoded Tailwind color utilities
|
||||
(see src/lib/no-hardcoded-colors.test.ts). */
|
||||
.landing-text {
|
||||
color: #f8fafc;
|
||||
}
|
||||
.landing-text-primary {
|
||||
color: #ffffff;
|
||||
}
|
||||
.landing-text-secondary {
|
||||
color: rgb(255 255 255 / 0.86);
|
||||
}
|
||||
.landing-text-muted {
|
||||
color: rgb(255 255 255 / 0.62);
|
||||
}
|
||||
.landing-text-faint {
|
||||
color: rgb(255 255 255 / 0.5);
|
||||
}
|
||||
.landing-chip {
|
||||
background: rgb(255 255 255 / 0.05);
|
||||
}
|
||||
.landing-chip-strong {
|
||||
background: rgb(255 255 255 / 0.1);
|
||||
}
|
||||
.landing-divider {
|
||||
background: rgb(255 255 255 / 0.15);
|
||||
.card-glow:hover {
|
||||
box-shadow:
|
||||
0 12px 32px -10px color-mix(in srgb, var(--color-primary) 32%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { AmbientOrbs } from "@/components/ambient-orbs";
|
||||
import { LandingTopBar } from "@/components/landing-top-bar";
|
||||
|
||||
/**
|
||||
* Premium dark-gaming shell shared by the login and register pages.
|
||||
* Renders an always-dark hero canvas (brand glows + grid overlay) with a
|
||||
* hotel header on top, a brand/side column on the left and the auth form
|
||||
* (rendered by the page) on the right in a frosted glass panel.
|
||||
*/
|
||||
export function AuthShell({
|
||||
hotelName,
|
||||
logo,
|
||||
side,
|
||||
footer,
|
||||
children,
|
||||
}: {
|
||||
hotelName: string;
|
||||
logo?: string | null;
|
||||
side: ReactNode;
|
||||
footer?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="landing-hero w-full overflow-hidden rounded-2xl border border-white/10 sm:rounded-3xl">
|
||||
{/* Ambient glows */}
|
||||
<AmbientOrbs />
|
||||
|
||||
{/* Header */}
|
||||
<LandingTopBar hotelName={hotelName} logo={logo} />
|
||||
|
||||
{/* Body */}
|
||||
<div className="relative z-10 grid gap-8 px-4 pb-8 pt-6 sm:px-7 sm:pb-10 sm:pt-8 lg:grid-cols-[1fr_1.05fr] lg:gap-12">
|
||||
<div className="flex flex-col">{side}</div>
|
||||
<div className="glass-panel relative h-fit w-full rounded-2xl p-5 sm:p-7 lg:sticky lg:top-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{footer ? (
|
||||
<div className="relative z-10 border-t border-white/10 px-4 py-4 text-center sm:px-7">
|
||||
{footer}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
/**
|
||||
* Floating brand art for the premium auth pages: the welcome character
|
||||
* hovers over a pulsing primary halo with a faint slow-orbiting ring.
|
||||
*/
|
||||
export function BrandFrank({ size = 150 }: { size?: number }) {
|
||||
return (
|
||||
<div className="relative flex items-center justify-center">
|
||||
<style>{`@keyframes spin-slow { to { transform: rotate(360deg); } }`}</style>
|
||||
<span className="hero-halo h-40 w-40 sm:h-48 sm:w-48" />
|
||||
<span
|
||||
className="absolute h-52 w-52 rounded-full border border-dashed sm:h-64 sm:w-64"
|
||||
style={{
|
||||
borderColor: "color-mix(in srgb, #ffffff 16%, transparent)",
|
||||
animation: "spin-slow 28s linear infinite",
|
||||
}}
|
||||
/>
|
||||
<div className="animate-float relative z-10 drop-shadow-[0_20px_30px_rgba(0,0,0,0.45)]">
|
||||
<Image
|
||||
src="/assets/images/FrankwithBag.gif"
|
||||
alt=""
|
||||
width={size}
|
||||
height={Math.round(size * 1.29)}
|
||||
className="object-contain"
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -113,30 +113,52 @@ export function RegisterForm({
|
||||
/>
|
||||
) : null}
|
||||
{/* Header Banner */}
|
||||
<div className="mb-7 flex items-center gap-4">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-white/10 landing-chip">
|
||||
<Image
|
||||
src="/assets/images/icons/navigation/me.png"
|
||||
alt=""
|
||||
width={22}
|
||||
height={22}
|
||||
unoptimized
|
||||
className="opacity-90"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p
|
||||
className="text-[11px] font-extrabold uppercase tracking-widest"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
<div
|
||||
className="relative overflow-hidden bg-center bg-cover rounded-t-xl"
|
||||
style={{
|
||||
backgroundImage: "url(/assets/images/mybobba_banner.png)",
|
||||
height: "160px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.55) 100%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex items-center justify-center w-full h-full z-20">
|
||||
<div
|
||||
className="relative animate-float"
|
||||
style={{ width: "140px", height: "140px" }}
|
||||
>
|
||||
{t("accountDetails")}
|
||||
</p>
|
||||
<h2 className="m-0 text-2xl font-black tracking-tight landing-text-primary">
|
||||
{t("title")}
|
||||
</h2>
|
||||
<p className="mt-0.5 truncate text-sm landing-text-muted">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
<div
|
||||
className="absolute inset-0 rounded-full overflow-hidden"
|
||||
style={{ zIndex: -1 }}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
background: "black",
|
||||
filter: "blur(8px)",
|
||||
transform: "scale(1.2)",
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/assets/images/FrankwithBag.gif"
|
||||
alt=""
|
||||
fill
|
||||
className="!static"
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
objectPosition: "center",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -149,7 +171,12 @@ export function RegisterForm({
|
||||
};
|
||||
formAction(fd);
|
||||
}}
|
||||
className="flex flex-col gap-6"
|
||||
className="p-6 flex flex-col gap-6"
|
||||
style={{
|
||||
backgroundColor:
|
||||
"color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
|
||||
borderRadius: "0 0 12px 12px",
|
||||
}}
|
||||
>
|
||||
{(error || state.error) && (
|
||||
<div
|
||||
@@ -356,7 +383,8 @@ export function RegisterForm({
|
||||
termsError ? "animate-shake" : ""
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
backgroundColor:
|
||||
"color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
|
||||
border: `1px solid ${
|
||||
termsError
|
||||
? "var(--color-danger)"
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import { Clock } from "@/components/clock";
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { ThemeSwitcher } from "@/components/theme-switcher";
|
||||
|
||||
/**
|
||||
* Frosted top bar shared by the home hero and the premium auth pages.
|
||||
* Dark-styled to sit on the "always-dark" landing canvas.
|
||||
*/
|
||||
export function LandingTopBar({
|
||||
hotelName,
|
||||
logo,
|
||||
}: {
|
||||
hotelName: string;
|
||||
logo?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative z-10 flex items-center justify-between gap-3 border-b border-white/10 px-4 py-3 sm:px-7 sm:py-4">
|
||||
<div className="flex min-w-0 items-center gap-2.5 sm:gap-3">
|
||||
{logo ? (
|
||||
<Image
|
||||
src={logo}
|
||||
alt={hotelName}
|
||||
width={110}
|
||||
height={30}
|
||||
className="shrink-0 object-contain"
|
||||
unoptimized
|
||||
style={{ maxHeight: 30, width: "auto", height: "auto" }}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src="/assets/images/icons/lighthouse.png"
|
||||
alt={hotelName}
|
||||
width={30}
|
||||
height={30}
|
||||
className="shrink-0"
|
||||
unoptimized
|
||||
/>
|
||||
)}
|
||||
<span className="hidden truncate text-sm font-extrabold uppercase tracking-widest landing-text-secondary sm:inline">
|
||||
{hotelName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1 rounded-xl border border-white/10 landing-chip px-1.5 py-1 sm:px-2">
|
||||
<LanguageSwitcher />
|
||||
<div className="hidden h-5 w-px landing-divider sm:block" />
|
||||
<Clock />
|
||||
<div className="h-5 w-px landing-divider" />
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user