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>
|
||||
);
|
||||
|
||||
+219
-239
@@ -4,8 +4,27 @@ 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",
|
||||
@@ -19,28 +38,6 @@ 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(),
|
||||
@@ -138,181 +135,211 @@ 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>
|
||||
<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%)",
|
||||
}}
|
||||
/>
|
||||
<section className="landing-hero w-full overflow-hidden rounded-2xl border border-white/10 sm:rounded-3xl">
|
||||
<AmbientOrbs />
|
||||
<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="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" />
|
||||
<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 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>
|
||||
{online === null ? (
|
||||
<span>{te("unavailable")}</span>
|
||||
) : (
|
||||
<LiveOnlineCount initial={online} hotelName={hotelName} />
|
||||
)}
|
||||
</span>
|
||||
{online === null ? (
|
||||
<span>{te("unavailable")}</span>
|
||||
) : (
|
||||
<LiveOnlineCount initial={online} hotelName={hotelName} />
|
||||
)}
|
||||
|
||||
<h1 className="mt-6 text-4xl font-black leading-[1.02] tracking-tight text-white 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 text-white/60 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>
|
||||
</div>
|
||||
<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"
|
||||
|
||||
{/* 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 bg-white/5 p-7 shadow-2xl backdrop-blur-xl transition-transform duration-500 hover:rotate-0"
|
||||
style={{
|
||||
background: "var(--color-primary)",
|
||||
color: "var(--color-primary-foreground)",
|
||||
boxShadow:
|
||||
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
|
||||
"0 40px 90px -30px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.1)",
|
||||
}}
|
||||
>
|
||||
<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=""
|
||||
width={20}
|
||||
height={20}
|
||||
alt={hotelName}
|
||||
width={415}
|
||||
height={128}
|
||||
className="h-auto w-full object-contain drop-shadow-[0_8px_20px_rgba(0,0,0,0.4)]"
|
||||
unoptimized
|
||||
/>
|
||||
{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="mt-4 text-center text-sm leading-relaxed text-white/50">
|
||||
{tp("welcomeSubtitle")}
|
||||
</p>
|
||||
<div className="mt-5 space-y-2.5">
|
||||
<div className="h-2 rounded-full bg-white/10">
|
||||
<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 bg-white/10">
|
||||
<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 text-white">
|
||||
<LiveOnlineCounter initial={online ?? 0} />
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] font-bold uppercase tracking-widest text-white/50">
|
||||
{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" }}
|
||||
>
|
||||
{th("login")}
|
||||
</Link>
|
||||
<p className="m-0 text-lg font-black leading-none text-white">
|
||||
<AnimatedCounter value={users ?? 0} />
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] font-bold uppercase tracking-widest text-white/50">
|
||||
{tp("statsCitizens")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</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 bg-white/5 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 text-white sm:text-3xl">
|
||||
{s.value === null ? (
|
||||
<span className="text-sm font-bold text-white/50">
|
||||
{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 text-white/50 sm:text-xs">
|
||||
{s.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Reveal>
|
||||
|
||||
{/* ── Features ── */}
|
||||
@@ -321,21 +348,28 @@ export default async function Home() {
|
||||
{features.map((f) => (
|
||||
<SurfaceCard
|
||||
key={f.title}
|
||||
className="relative p-4 sm:p-5 text-center transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
className="group relative overflow-hidden p-5 sm:p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
<div
|
||||
className="mx-auto mb-3 flex h-11 w-11 sm:h-12 sm:w-12 items-center justify-center rounded-xl"
|
||||
className="absolute inset-x-0 top-0 h-1 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 14%, transparent)",
|
||||
"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)))",
|
||||
boxShadow:
|
||||
"inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent)",
|
||||
"inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 14%, transparent)",
|
||||
}}
|
||||
>
|
||||
<Image src={f.icon} alt="" width={24} height={24} unoptimized />
|
||||
</div>
|
||||
<h3
|
||||
className="text-sm sm:text-base font-extrabold mb-1"
|
||||
className="text-center text-base font-extrabold"
|
||||
style={{
|
||||
color: "var(--color-text-readable)",
|
||||
fontFamily: "var(--font-nunito)",
|
||||
@@ -344,7 +378,7 @@ export default async function Home() {
|
||||
{f.title}
|
||||
</h3>
|
||||
<p
|
||||
className="text-xs sm:text-sm leading-relaxed"
|
||||
className="mt-1 text-center text-sm leading-relaxed"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{f.desc}
|
||||
@@ -354,60 +388,6 @@ 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="relative p-3 sm:p-5 text-center transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
<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">
|
||||
|
||||
+108
-188
@@ -1,21 +1,24 @@
|
||||
import { count, desc, 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 { 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(),
|
||||
@@ -23,7 +26,7 @@ export default async function RegisterPage() {
|
||||
]);
|
||||
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
||||
|
||||
const [online, recentUsers, latestUsers] = await Promise.all([
|
||||
const [online, latestUsers] = await Promise.all([
|
||||
cached("online_count", 10_000, () =>
|
||||
db
|
||||
.select({ total: count() })
|
||||
@@ -31,207 +34,124 @@ 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(8),
|
||||
.limit(6),
|
||||
).catch(() => []),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-6xl flex flex-col gap-8 pb-16">
|
||||
<AuthTopBar hotelName={hotelName} logo={logo} />
|
||||
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>
|
||||
<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="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">
|
||||
<Image
|
||||
src="/assets/images/FrankwithBag.gif"
|
||||
alt="Frank"
|
||||
width={110}
|
||||
height={145}
|
||||
className="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"
|
||||
<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 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>
|
||||
{tpr("usersOnline", { count: online })}
|
||||
</span>
|
||||
<h1 className="mt-5 text-3xl font-black leading-[1.08] tracking-tight text-white 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 text-white/60 sm:text-base">
|
||||
{tpr("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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 bg-white/5 px-4 py-3"
|
||||
>
|
||||
<span
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full"
|
||||
style={{
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 18%, transparent)",
|
||||
color: "var(--color-primary)",
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 10%, transparent)",
|
||||
color:
|
||||
"var(--color-primary-readable, var(--color-primary))",
|
||||
"color-mix(in srgb, var(--color-primary) 15%, transparent)",
|
||||
}}
|
||||
>
|
||||
<span className="w-2 h-2 rounded-full bg-primary" />
|
||||
{tpr("usersOnline", { count: online })}
|
||||
<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 text-white">
|
||||
{b.title}
|
||||
</p>
|
||||
<p className="m-0 truncate text-xs text-white/55">
|
||||
{b.desc}
|
||||
</p>
|
||||
</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 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 text-white/45">
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SurfaceCard>
|
||||
|
||||
{recentUsers.length > 0 && (
|
||||
<SurfaceCard
|
||||
title={tpr("whoIsOnline")}
|
||||
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-[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>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RegisterForm
|
||||
hotelName={hotelName}
|
||||
captcha={{
|
||||
provider: cfg.provider,
|
||||
siteKey: cfg.siteKey || undefined,
|
||||
}}
|
||||
nonce={nonce}
|
||||
/>
|
||||
</AuthShell>
|
||||
</Reveal>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1992,3 +1992,108 @@ details[open] > summary .details-open\:rotate-180 {
|
||||
.stagger-children > *:nth-child(8) {
|
||||
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: "";
|
||||
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%
|
||||
);
|
||||
filter: blur(6px);
|
||||
animation: hero-halo-pulse 5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes hero-halo-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.55;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.9;
|
||||
transform: scale(1.07);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-halo {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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,52 +113,30 @@ export function RegisterForm({
|
||||
/>
|
||||
) : null}
|
||||
{/* Header Banner */}
|
||||
<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" }}
|
||||
<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 bg-white/5">
|
||||
<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="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>
|
||||
{t("accountDetails")}
|
||||
</p>
|
||||
<h2 className="m-0 text-2xl font-black tracking-tight text-white">
|
||||
{t("title")}
|
||||
</h2>
|
||||
<p className="mt-0.5 truncate text-sm text-white/55">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -171,12 +149,7 @@ export function RegisterForm({
|
||||
};
|
||||
formAction(fd);
|
||||
}}
|
||||
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",
|
||||
}}
|
||||
className="flex flex-col gap-6"
|
||||
>
|
||||
{(error || state.error) && (
|
||||
<div
|
||||
@@ -383,8 +356,7 @@ export function RegisterForm({
|
||||
termsError ? "animate-shake" : ""
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor:
|
||||
"color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
|
||||
backgroundColor: "var(--color-background)",
|
||||
border: `1px solid ${
|
||||
termsError
|
||||
? "var(--color-danger)"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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 text-white/85 sm:inline">
|
||||
{hotelName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1 rounded-xl border border-white/10 bg-white/5 px-1.5 py-1 sm:px-2">
|
||||
<LanguageSwitcher />
|
||||
<div className="hidden h-5 w-px bg-white/15 sm:block" />
|
||||
<Clock />
|
||||
<div className="h-5 w-px bg-white/15" />
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user