diff --git a/src/app/(site)/login/page.tsx b/src/app/(site)/login/page.tsx
index 0cc720b1..4b44e571 100644
--- a/src/app/(site)/login/page.tsx
+++ b/src/app/(site)/login/page.tsx
@@ -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 (
-
-
-
+
-
- {/* Left panel */}
-
-
-
-
-
-
-
-
-
-
- {th("online", { count: online, hotel: "" }).trim()}
-
-
- {t("welcomeBack")}
-
-
- {t("welcomeBackSub", { hotelName })}
-
-
-
-
- {recentUsers.length > 0 && (
-
-
- {recentUsers.map((u) => (
-
-
-
- {u.username}
-
-
- ))}
-
-
- )}
-
- {latestUsers.length > 0 && (
-
-
- {latestUsers.map((u) => (
-
-
-
- {u.username}
-
-
- ))}
-
-
- )}
-
-
+
{t("noAccount") ?? "No account?"}{" "}
{t("createOne") ?? "Create one"}
-
+ }
+ side={
+
+
+
+
+
+
+
+ {onlineLabel}
+
+
+ {t("welcomeBack")}
+
+ {hotelName}
+
+
+ {t("welcomeBackSub", { hotelName })}
+
+
- {/* Right: form */}
-
-
-
- {t("subtitle")}
-
-
-
+
+
+
+
+
+
+
{online}
+
+ {onlineLabel}
+
+ {recentUsers.length > 0 && (
+
+ {recentUsers.slice(0, 5).map((u) => (
+
+ ))}
+
+ )}
+
+
+
+
+ {totalUsers}
+
+
+
+
+ {thp("statsCitizens")}
+
+
+
+
+ }
+ >
+
+
+ {t("title")}
+
+
{t("subtitle")}
-
+
+
);
diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx
index 77e7c6f8..37fc5c45 100644
--- a/src/app/(site)/page.tsx
+++ b/src/app/(site)/page.tsx
@@ -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 (
- {/* ── Top Bar ── */}
-
-
-
- {logo ? (
-
- ) : (
-
- )}
-
- {hotelName}
-
-
-
-
-
-
{/* ── Hero ── */}
-
-
-
+
-
-
-
-
-
+
+
+
+ {/* Left: copy */}
+
+
+
+
+
+
+ {online === null ? (
+ {te("unavailable")}
+ ) : (
+
+ )}
- {online === null ? (
-
{te("unavailable")}
- ) : (
-
- )}
+
+
+ {tp("welcome", { hotelName: "" }).trim()}
+
+
+
+
+ {tp("welcomeBody")}
+
+
+
+
+
+ {tp("register")}
+
+
+ {th("login")}
+
+
-
-
-
-
- {tp("welcomeBody")}
-
-
-
+
+
+
- {tp("register")}
-
-
+ {tp("welcomeSubtitle")}
+
+
+
+
+
+
+
+
+
+ {tp("statsOnline")}
+
+
+
+
- {th("login")}
-
+
+
+
+
+ {tp("statsCitizens")}
+
+
-
+
+ {/* Stats strip */}
+
+
+ {stats.map((s) => (
+
+
+
+ {s.value === null ? (
+
+ {te("unavailable")}
+
+ ) : s.live ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {s.label}
+
+
+ ))}
+
+
+
{/* ── Features ── */}
@@ -321,21 +348,28 @@ export default async function Home() {
{features.map((f) => (
+
{f.desc}
@@ -354,60 +388,6 @@ export default async function Home() {
- {/* ── Stats + Login ── */}
-
-
- {[
- {
- 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) => (
-
-
- {s.value === null ? (
-
{te("unavailable")}
- ) : s.live ? (
-
- ) : (
-
- )}
-
-
-
- {s.label}
-
-
- ))}
-
-
-
{/* ── Login + Online users ── */}
diff --git a/src/app/(site)/register/page.tsx b/src/app/(site)/register/page.tsx
index ed784407..e035526d 100644
--- a/src/app/(site)/register/page.tsx
+++ b/src/app/(site)/register/page.tsx
@@ -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 (
-
-
+ const benefits = [
+ { title: th("featuresAvatar"), desc: th("featuresAvatarDesc") },
+ { title: th("featuresFurniture"), desc: th("featuresFurnitureDesc") },
+ { title: th("featuresCommunity"), desc: th("featuresCommunityDesc") },
+ ];
+ return (
+
-
- {/* Left: intro panel */}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {tpr("usersOnline", { count: online })}
+
+
+ {tpr("title")}
+
+ {hotelName}
+
+
+ {tpr("subtitle")}
+
+
+
+
+ {benefits.map((b) => (
+
+
-
- {tpr("usersOnline", { count: online })}
+
+
+
+
+ {b.title}
+
+
+ {b.desc}
+
-
- {tpr("title")} — {hotelName}
-
-
- {tpr("subtitle")}
-
+
+ ))}
+
+
+
+
+
+
+ {latestUsers.length > 0 && (
+ <>
+
+ Latest citizens
+
+
+ {latestUsers.slice(0, 5).map((u) => (
+
+ ))}
+
+ >
+ )}
-
-
- {recentUsers.length > 0 && (
-
-
- {recentUsers.map((u) => (
-
-
-
- {u.username}
-
-
- ))}
-
-
- )}
-
- {latestUsers.length > 0 && (
-
-
- {latestUsers.map((u) => (
-
-
-
- {u.username}
-
-
- ))}
-
-
- )}
-
-
- {tpr("alreadyHaveAccountPre")}{" "}
-
- {tpr("alreadyHaveAccountLink")}
-
-
-
-
- {/* Right: form */}
-
-
-
- {tpr("subtitle")}
-
-
-
-
-
+
+ }
+ >
+
+
);
diff --git a/src/app/globals.css b/src/app/globals.css
index fd00b5cb..146ac131 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -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;
+ }
+}
diff --git a/src/components/auth/auth-shell.tsx b/src/components/auth/auth-shell.tsx
new file mode 100644
index 00000000..86bd0e5a
--- /dev/null
+++ b/src/components/auth/auth-shell.tsx
@@ -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 (
+
+ {/* Ambient glows */}
+
+
+ {/* Header */}
+
+
+ {/* Body */}
+
+
{side}
+
+ {children}
+
+
+
+ {footer ? (
+
+ {footer}
+
+ ) : null}
+
+ );
+}
diff --git a/src/components/auth/brand-frank.tsx b/src/components/auth/brand-frank.tsx
new file mode 100644
index 00000000..46b0b6e1
--- /dev/null
+++ b/src/components/auth/brand-frank.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/components/auth/register-form.tsx b/src/components/auth/register-form.tsx
index e92c9207..070abeb7 100644
--- a/src/components/auth/register-form.tsx
+++ b/src/components/auth/register-form.tsx
@@ -113,52 +113,30 @@ export function RegisterForm({
/>
) : null}
{/* Header Banner */}
-
-
-
-
+
+
+
+
+ {t("accountDetails")}
+
+
+ {t("title")}
+
+
+ {t("subtitle")}
+
@@ -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) && (
+
+ {logo ? (
+
+ ) : (
+
+ )}
+
+ {hotelName}
+
+
+
+
+ );
+}