Redesign login, register, me pages with premium layout and i18n
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 2m7s

This commit is contained in:
openhands committed 2026-07-22 14:42:01 +02:00
1 parent 063ac9ec40
commit 1ffd46713a
7 files changed
+621 -279

No files matched your search

+170 -9
View File
@@ -1,19 +1,180 @@
import { headers } from "next/headers";
import Image from "next/image";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { LoginForm } from "@/components/auth/login-form";
import { cached } from "@/lib/cache";
import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings";
export default async function LoginPage() {
const cfg = await captchaConfig();
const t = await getTranslations("pages.login");
const th = await getTranslations("header");
const [hotelName, imagerRaw, cfg] = await Promise.all([
siteSettings.get("hotel_name", "Hotel"),
siteSettings.get(
"habbo_imaging_url",
"https://www.habbo.com/habbo-imaging/avatarimage",
),
captchaConfig(),
]);
const imager = imagerRaw ?? "https://www.habbo.com/habbo-imaging/avatarimage";
const nonce = (await headers()).get("x-nonce") ?? undefined;
const [online, recentUsers] = await Promise.all([
cached("online_count", 10_000, () =>
prisma.user.count({ where: { online: "1" } }),
).catch(() => 0),
prisma.user
.findMany({
where: { online: "1" },
select: { username: true, look: true },
take: 8,
})
.catch(() => []),
]);
return (
<LoginForm
captcha={{
provider: cfg.provider,
siteKey: cfg.siteKey || undefined,
field: cfg.field || undefined,
}}
nonce={nonce}
/>
<div className="flex flex-col lg:flex-row gap-10">
{/* Left panel */}
<div className="lg:w-80 shrink-0 space-y-4">
<div
className="rounded-2xl border p-6 text-center"
style={{
borderColor:
"color-mix(in srgb, var(--color-primary) 20%, transparent)",
background: `linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 10%, var(--color-surface)), var(--color-surface))`,
}}
>
<Image
src="/assets/images/FrankwithBag.gif"
alt="Frank"
width={130}
height={170}
className="object-contain mx-auto drop-shadow-xl"
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="w-2 h-2 rounded-full bg-green-500" />
{th("online", { count: online, hotel: "" }).trim()}
</div>
<h1
className="text-xl font-black"
style={{ color: "var(--color-text-readable)" }}
>
{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: hotelName ?? "Hotel" })}
</p>
</div>
{recentUsers.length > 0 && (
<div
className="rounded-2xl border p-4"
style={{
background: "var(--color-surface)",
borderColor:
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
}}
>
<h3
className="text-xs font-bold uppercase tracking-widest mb-3"
style={{ color: "var(--color-text-muted)" }}
>
{th("online", { count: online, hotel: "" }).trim()}
</h3>
<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)",
}}
>
<Image
src={avatarImageUrl(imager, u.look, {
headOnly: true,
direction: 2,
size: "s",
})}
alt=""
width={26}
height={32}
className="rounded-md"
unoptimized
/>
<span
className="text-[8px] font-bold truncate w-full text-center leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</div>
)}
<p
className="text-xs text-center"
style={{ color: "var(--color-text-muted)" }}
>
No account?{" "}
<Link
href="/register"
className="font-extrabold hover:underline"
style={{
color: "var(--color-primary-readable, var(--color-primary))",
}}
>
Create one
</Link>
</p>
</div>
{/* Right: form */}
<div className="flex-1">
<div className="mb-6">
<h2
className="text-2xl font-black tracking-tight"
style={{ color: "var(--color-text-readable)" }}
>
{t("title")}
</h2>
<p
className="text-sm mt-0.5"
style={{ color: "var(--color-text-muted)" }}
>
{t("subtitle")}
</p>
</div>
<LoginForm
captcha={{
provider: cfg.provider,
siteKey: cfg.siteKey || undefined,
field: cfg.field || undefined,
}}
nonce={nonce}
/>
</div>
</div>
);
}
+10 -7
View File
@@ -2,11 +2,6 @@
import { useState } from "react";
/**
* Copies the user's referral link to the clipboard. Mirrors AtomCMS's
* copyCode() helper on the /me page, but resolves the link to an absolute URL
* (the server only knows a relative path, so we prepend the current origin).
*/
export default function CopyReferralButton({ value }: { value: string }) {
const [copied, setCopied] = useState(false);
@@ -18,7 +13,6 @@ export default function CopyReferralButton({ value }: { value: string }) {
try {
await navigator.clipboard.writeText(absolute);
} catch {
// Older browsers / insecure contexts: fall back to a hidden textarea.
const ta = document.createElement("textarea");
ta.value = absolute;
ta.style.position = "fixed";
@@ -37,7 +31,16 @@ export default function CopyReferralButton({ value }: { value: string }) {
}
return (
<button type="button" className="btn btn-secondary" onClick={copy}>
<button
type="button"
onClick={copy}
className="rounded-xl font-bold text-sm px-5 py-2.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shrink-0"
style={{
background: "color-mix(in srgb, var(--color-primary) 12%, transparent)",
color: "var(--color-primary-readable, var(--color-primary))",
border: `2px solid color-mix(in srgb, var(--color-primary) 20%, transparent)`,
}}
>
{copied ? "Copied!" : "Copy link"}
</button>
);
+202 -107
View File
@@ -1,13 +1,9 @@
import Image from "next/image";
import Link from "next/link";
import { redirect } from "next/navigation";
import type { CSSProperties, ReactNode } from "react";
import { claimReferral } from "@/actions/referral";
import {
ContentCard,
EmptyState,
OnlineBadge,
StatBlock,
} from "@/components/public/ui";
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
import { auth } from "@/lib/auth";
import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
@@ -20,7 +16,6 @@ export const metadata = { title: "My dashboard" };
type SearchParams = Promise<{ claimed?: string; error?: string }>;
/** Inline-styled feedback banner using the theme's CSS variables. */
function feedbackStyle(tone: "success" | "error" | "warning"): CSSProperties {
const accent =
tone === "error"
@@ -72,7 +67,6 @@ export default async function MePage({
let content: ReactNode;
try {
// --- Account + reward config -----------------------------------------
const [user, imagerBase, neededRaw, amountRaw, currencyRaw] =
await Promise.all([
prisma.user.findUnique({
@@ -118,9 +112,6 @@ export default async function MePage({
.catch(() => null),
]);
// The session was already validated above; a missing row here is an
// exceptional state, so surface the error card rather than redirect from
// inside the try (a redirect() throw would be swallowed by the catch).
if (!user) throw new Error("user-not-found");
const needed = Number.parseInt(neededRaw?.value ?? "5", 10) || 5;
@@ -129,7 +120,6 @@ export default async function MePage({
.trim()
.toLowerCase();
// --- Referral tally ---------------------------------------------------
const referrals = await prisma.userReferrals
.findFirst({
where: { userId },
@@ -141,9 +131,6 @@ export default async function MePage({
const canClaim = referralTotal >= needed;
const remaining = Math.max(0, needed - referralTotal);
// --- Online friends ---------------------------------------------------
// Friendships are bidirectional and stored once: the user can be either
// user_one_id or user_two_id; the OTHER column is the friend.
const friendships = await prisma.messengerFriendships
.findMany({
where: { OR: [{ userOneId: userId }, { userTwoId: userId }] },
@@ -181,13 +168,16 @@ export default async function MePage({
.slice(0, 10);
const referralLink = `/register?ref=${encodeURIComponent(user.username)}`;
const imager = imagerBase ?? "https://www.habbo.com/habbo-imaging/avatarimage";
const avatarFull = avatarImageUrl(imager, user.look, { size: "l", direction: 2 });
const avatarHead = avatarImageUrl(imager, user.look, { headOnly: true, direction: 2, size: "s" });
content = (
<>
{/* Feedback banners --------------------------------------------- */}
<div className="flex flex-col gap-8 pb-8">
{/* Feedback banners */}
{claimed ? (
<div role="status" style={feedbackStyle("success")}>
Woah! You have successfully claimed your reward — keep up the good
work!
Woah! You have successfully claimed your reward — keep up the good work!
</div>
) : null}
{error ? (
@@ -196,129 +186,222 @@ export default async function MePage({
</div>
) : null}
{/* Header card -------------------------------------------------- */}
<ContentCard
icon="🏠"
title={user.username}
subtitle={user.motto || "Welcome back!"}
/>
{/* Stat tiles --------------------------------------------------- */}
<div className="card-grid sm-2 lg-3">
<StatBlock
icon="💰"
value={user.credits.toLocaleString()}
label="Credits"
/>
<StatBlock icon="🎖️" value={`Rank ${user.rank}`} label="Hotel rank" />
<StatBlock icon="📅" value={registered} label="Registered" />
{/* ── Profile header ── */}
<div
className="rounded-2xl border p-6 flex flex-col sm:flex-row items-center sm:items-start gap-6"
style={{
background: `linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 10%, var(--color-surface)), var(--color-surface))`,
borderColor: "color-mix(in srgb, var(--color-primary) 20%, transparent)",
}}
>
<div className="relative shrink-0">
<div
className="absolute -inset-3 rounded-full opacity-[0.15] blur-[40px]"
style={{ background: "var(--color-primary)" }}
/>
<Image
src={avatarFull}
alt={`${user.username} avatar`}
width={120}
height={160}
className="object-contain drop-shadow-xl relative"
unoptimized
priority
/>
</div>
<div className="flex-1 text-center sm:text-left">
<div
className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full text-xs font-bold uppercase tracking-wider 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))",
}}
>
Rank {user.rank}
</div>
<h1
className="text-2xl md:text-3xl font-black tracking-tight"
style={{ color: "var(--color-text-readable)" }}
>
{user.username}
</h1>
<p className="text-sm mt-1" style={{ color: "var(--color-text-muted)" }}>
{user.motto || "No motto"}
</p>
</div>
</div>
{/* Online friends ----------------------------------------------- */}
<ContentCard
icon="🟢"
title="Online friends"
subtitle={
onlineFriends.length === 0
? "None of your friends are online right now"
: `${onlineFriends.length} of your ${friends.length} friends online`
}
action={<Link href="/friends">View all</Link>}
padded={onlineFriends.length === 0}
{/* ── Stat tiles ── */}
<div className="grid grid-cols-3 gap-4">
{[
{ value: user.credits.toLocaleString(), label: "Credits" },
{ value: `Rank ${user.rank}`, label: "Hotel rank" },
{ value: registered, label: "Registered" },
].map((s) => (
<div
key={s.label}
className="rounded-2xl border p-5 text-center transition-all duration-200 hover:-translate-y-0.5"
style={{
background: "var(--color-surface)",
borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
}}
>
<div
className="text-xl md:text-2xl font-black tracking-tight mb-0.5"
style={{ color: "var(--color-primary)" }}
>
{s.value}
</div>
<div
className="text-xs font-bold uppercase tracking-widest"
style={{ color: "var(--color-text-muted)" }}
>
{s.label}
</div>
</div>
))}
</div>
{/* ── Online friends ── */}
<div
className="rounded-2xl border p-5"
style={{
background: "var(--color-surface)",
borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
}}
>
<div className="flex items-center justify-between mb-4">
<div>
<h2
className="text-lg font-black tracking-tight"
style={{ color: "var(--color-text-readable)" }}
>
Online friends
</h2>
<p className="text-xs mt-0.5" style={{ color: "var(--color-text-muted)" }}>
{onlineFriends.length === 0
? "None of your friends are online right now"
: `${onlineFriends.length} of your ${friends.length} friends online`}
</p>
</div>
<Link
href="/friends"
className="text-xs font-bold hover:underline shrink-0"
style={{ color: "var(--color-primary)" }}
>
View all
</Link>
</div>
{onlineFriends.length === 0 ? (
<EmptyState icon="💤">
No friends online right now. Check back later!
</EmptyState>
<div className="text-center py-6">
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
No friends online right now. Check back later!
</p>
</div>
) : (
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{onlineFriends.map((friend) => {
const avatar = avatarImageUrl(imagerBase ?? "", friend.look, {
const avatar = avatarImageUrl(imager, friend.look, {
size: "s",
headOnly: true,
});
return (
<div
<Link
key={friend.id}
className="card hover"
href={`/u/${friend.username}`}
className="flex items-center gap-3 p-3 rounded-xl transition-all duration-200 hover:-translate-y-0.5"
style={{
display: "flex",
gap: "0.85rem",
alignItems: "center",
background: "color-mix(in srgb, var(--color-primary) 4%, transparent)",
textDecoration: "none",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="avatar"
<Image
src={avatar}
alt={`${friend.username} avatar`}
width={50}
width={40}
height={50}
className="rounded-lg shrink-0"
unoptimized
/>
<div style={{ minWidth: 0, flex: 1 }}>
<h3 style={{ margin: "0 0 0.3rem", fontSize: "1rem" }}>
<Link href={`/u/${friend.username}`}>
{friend.username}
</Link>
</h3>
<div className="min-w-0">
<p
className="muted"
style={{
margin: "0 0 0.4rem",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
className="text-sm font-extrabold truncate"
style={{ color: "var(--color-text-readable)" }}
>
{friend.username}
</p>
<p
className="text-[10px] font-semibold truncate mt-0.5"
style={{ color: "var(--color-text-muted)" }}
>
{friend.motto || "No motto"}
</p>
<OnlineBadge online />
<span
className="inline-flex items-center gap-1 text-[10px] font-bold mt-1"
style={{ color: "var(--color-success)" }}
>
<span className="w-1.5 h-1.5 rounded-full" style={{ background: "var(--color-success)" }} />
Online
</span>
</div>
</div>
</Link>
);
})}
</div>
)}
</ContentCard>
</div>
{/* Referral card ------------------------------------------------ */}
<ContentCard
icon="🤝"
title={`Refer a friend (${referralTotal}/${needed})`}
subtitle="Refer new users and earn in-game rewards"
{/* ── Referral card ── */}
<div
className="rounded-2xl border p-5"
style={{
background: "var(--color-surface)",
borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
}}
>
<div style={{ display: "grid", gap: "1rem" }}>
<p style={{ margin: 0 }}>
<div className="mb-4">
<h2
className="text-lg font-black tracking-tight"
style={{ color: "var(--color-text-readable)" }}
>
Refer a friend ({referralTotal}/{needed})
</h2>
<p className="text-xs mt-0.5" style={{ color: "var(--color-text-muted)" }}>
Refer new users and earn in-game rewards
</p>
</div>
<div className="flex flex-col gap-4">
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
{rewardAmount > 0
? `For every ${needed} users who register through your referral link you can claim a reward of ${rewardAmount.toLocaleString()} ${rewardCurrency}!`
: `Invite ${needed} users through your referral link to claim a reward!`}
</p>
<div role="note" style={feedbackStyle("warning")}>
⚠️ Boosting referrals by making your own accounts will lead to
removal of all progress, currency, inventory and a potential ban.
⚠️ Boosting referrals by making your own accounts will lead to removal of all progress, currency, inventory and a potential ban.
</div>
<div>
<label
htmlFor="referral-link"
className="muted"
style={{
display: "block",
marginBottom: "0.35rem",
fontSize: "0.85rem",
}}
className="text-xs font-bold mb-1.5 block"
style={{ color: "var(--color-text-muted)" }}
>
Your referral link
</label>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
<div className="flex gap-2 flex-wrap">
<input
id="referral-link"
type="text"
readOnly
defaultValue={referralLink}
className="input"
style={{ flex: "1 1 240px", minWidth: 0 }}
className="flex-1 min-w-0 rounded-xl border-2 px-4 py-2.5 text-sm font-medium"
style={{
backgroundColor: "var(--color-background)",
color: "var(--color-text-readable, var(--color-text))",
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
}}
/>
<CopyReferralButton value={referralLink} />
</div>
@@ -328,8 +411,12 @@ export default async function MePage({
<form action={claimReferral}>
<button
type="submit"
className="btn btn-primary"
style={{ width: "100%" }}
className="w-full rounded-xl font-extrabold text-sm py-3.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg"
style={{
background: "var(--color-primary)",
color: "var(--color-primary-foreground)",
boxShadow: "0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
}}
>
Claim your referral reward!
</button>
@@ -338,26 +425,34 @@ export default async function MePage({
<button
type="button"
disabled
className="btn"
style={{ width: "100%", opacity: 0.6, cursor: "not-allowed" }}
className="w-full rounded-xl font-extrabold text-sm py-3.5 opacity-60 cursor-not-allowed"
style={{
background: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)",
color: "var(--color-text-muted)",
}}
>
{`Refer ${remaining} more ${remaining === 1 ? "user" : "users"} to unlock your reward`}
</button>
)}
</div>
</ContentCard>
</>
</div>
</div>
);
} catch {
content = (
<ContentCard icon="⚠️" title="My dashboard">
<EmptyState icon="⚠️">
We couldn&apos;t load your dashboard right now. Please try again
shortly.
</EmptyState>
</ContentCard>
<div
className="rounded-2xl border p-6 text-center"
style={{
background: "var(--color-surface)",
borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
}}
>
<p className="text-sm font-bold" style={{ color: "var(--color-text-muted)" }}>
We couldn&apos;t load your dashboard right now. Please try again shortly.
</p>
</div>
);
}
return <main style={{ display: "grid", gap: "1.5rem" }}>{content}</main>;
return <main>{content}</main>;
}
+89 -38
View File
@@ -33,7 +33,7 @@ async function getHotelData() {
.findMany({
where: { online: "1" },
select: { username: true, look: true },
take: 12,
take: 30,
})
.catch(() => []),
prisma.cameraWeb
@@ -54,7 +54,8 @@ async function getHotelData() {
}
export default async function Home() {
const t = await getTranslations("header");
const tp = await getTranslations("pages.home");
const th = await getTranslations("header");
const {
hotelName,
imager,
@@ -68,6 +69,21 @@ export default async function Home() {
return (
<div className="flex flex-col gap-16 pb-16">
<style>{`
@keyframes train-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.train-track {
animation: train-scroll 40s linear infinite;
display: flex;
gap: 0.75rem;
width: max-content;
}
.train-track:hover {
animation-play-state: paused;
}
`}</style>
{/* ── 1. Welcome section ── */}
<div className="flex flex-col lg:flex-row items-center gap-10 lg:gap-16">
<div className="flex-1 text-center lg:text-left">
@@ -82,20 +98,13 @@ export default async function Home() {
}}
>
<span className="w-2 h-2 rounded-full bg-green-500" />
{online.toLocaleString()} online
{th("online", { count: online, hotel: hotelName ?? "Hotel" })}
</div>
<h1
className="text-4xl md:text-5xl lg:text-6xl font-black leading-[1.05] tracking-tight mb-4"
style={{ color: "var(--color-text-readable)" }}
>
Welcome to{" "}
<span
style={{
color: "var(--color-primary-readable, var(--color-primary))",
}}
>
{hotelName}
</span>
{tp("welcome", { hotelName: hotelName ?? "Hotel" })}
</h1>
<p
className="text-base md:text-lg leading-relaxed max-w-xl mb-8"
@@ -104,7 +113,7 @@ export default async function Home() {
"var(--color-text-muted-readable, var(--color-text-muted))",
}}
>
{t("tagline")}
{th("tagline")}
</p>
<div className="flex flex-wrap gap-3 justify-center lg:justify-start">
<Link
@@ -124,7 +133,7 @@ export default async function Home() {
height={20}
unoptimized
/>
Enter {hotelName}
{tp("enterHotel", { hotelName: hotelName ?? "Hotel" })}
</Link>
<Link
href="/client"
@@ -135,7 +144,7 @@ export default async function Home() {
color: "var(--color-text-readable)",
}}
>
Play now
{th("nitroClient")}
</Link>
</div>
</div>
@@ -181,7 +190,7 @@ export default async function Home() {
{u.username}
</p>
<p className="text-[10px] font-semibold text-green-500">
Online
{tp("statsOnline")}
</p>
</div>
</div>
@@ -193,9 +202,9 @@ export default async function Home() {
{/* ── 2. Stats ── */}
<div className="grid grid-cols-3 gap-4">
{[
{ value: online.toLocaleString(), label: "Online now", icon: "🟢" },
{ value: users.toLocaleString(), label: "Total users", icon: "👤" },
{ value: rooms.toLocaleString(), label: "Rooms", icon: "🏠" },
{ value: online.toLocaleString(), label: tp("statsOnline"), icon: "🟢" },
{ value: users.toLocaleString(), label: tp("statsCitizens"), icon: "👤" },
{ value: rooms.toLocaleString(), label: tp("statsRooms"), icon: "🏠" },
].map((s) => (
<div
key={s.label}
@@ -222,28 +231,70 @@ export default async function Home() {
))}
</div>
{/* ── 3. Features ── */}
{/* ── 3. User train ── */}
{recentUsers.length > 0 && (
<div
className="rounded-2xl border overflow-hidden py-3"
style={{
background: "var(--color-surface)",
borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
}}
>
<div className="train-track">
{[...recentUsers, ...recentUsers].map((u, i) => (
<div
key={`${u.username}-${i}`}
className="flex flex-col items-center gap-0.5 px-2 py-1.5 rounded-xl transition-all duration-200 hover:scale-110"
style={{
background: "color-mix(in srgb, var(--color-primary) 4%, transparent)",
}}
>
<Image
src={avatarImageUrl(imager, u.look, {
headOnly: true,
direction: 2,
size: "s",
})}
alt={u.username}
width={34}
height={42}
className="rounded-md"
unoptimized
/>
<span
className="text-[9px] font-bold truncate max-w-[54px] text-center leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</div>
)}
{/* ── 4. Features ── */}
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[
{
img: "/assets/images/icons/star.gif",
title: "Avatar",
desc: "Create your unique look with thousands of outfits",
title: tp("featuresAvatar"),
desc: tp("featuresAvatarDesc"),
},
{
img: "/assets/images/icons/catalog.png",
title: "Furniture",
desc: "Decorate rooms with hundreds of items",
title: tp("featuresFurniture"),
desc: tp("featuresFurnitureDesc"),
},
{
img: "/assets/images/icons/navigation/community.png",
title: "Community",
desc: "Make friends and join groups",
title: tp("featuresCommunity"),
desc: tp("featuresCommunityDesc"),
},
{
img: "/assets/images/icons/navigation/bb.png",
title: "Games",
desc: "Compete in battles and events",
title: tp("featuresGames"),
desc: tp("featuresGamesDesc"),
},
].map((f) => (
<div
@@ -280,7 +331,7 @@ export default async function Home() {
))}
</div>
{/* ── 4. News + Sidebar ── */}
{/* ── 5. News + Sidebar ── */}
<div className="grid lg:grid-cols-12 gap-6">
<div className="lg:col-span-8 space-y-4">
<div className="flex items-center justify-between">
@@ -288,14 +339,14 @@ export default async function Home() {
className="text-xl font-black tracking-tight"
style={{ color: "var(--color-text-readable)" }}
>
News
{tp("latestNews")}
</h2>
<Link
href="/news"
className="text-sm font-bold hover:underline"
style={{ color: "var(--color-primary)" }}
>
View all →
{tp("allNews")}
</Link>
</div>
{articles.length > 0 ? (
@@ -349,7 +400,7 @@ export default async function Home() {
</div>
) : (
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
No news yet.
{tp("noArticles")}
</p>
)}
</div>
@@ -369,7 +420,7 @@ export default async function Home() {
style={{ color: "var(--color-text-muted)" }}
>
<span className="w-2 h-2 rounded-full bg-green-500" />
Online now
{tp("recentUsers")}
</h3>
<div className="grid grid-cols-3 gap-2">
{recentUsers.slice(0, 9).map((u) => (
@@ -418,7 +469,7 @@ export default async function Home() {
className="text-xs font-bold uppercase tracking-widest mb-3"
style={{ color: "var(--color-text-muted)" }}
>
Photos
{tp("recentPhotos")}
</h3>
<div className="grid grid-cols-2 gap-2">
{recentPhotos.map((p) => (
@@ -461,13 +512,13 @@ export default async function Home() {
className="text-xs font-bold"
style={{ color: "var(--color-text-muted)" }}
>
See you inside!
{tp("joinNow", { hotelName: hotelName ?? "Hotel" })}
</p>
</div>
</aside>
</div>
{/* ── 5. CTA ── */}
{/* ── 6. CTA ── */}
<div
className="rounded-2xl border text-center px-6 py-10"
style={{
@@ -480,13 +531,13 @@ export default async function Home() {
className="text-2xl md:text-3xl font-black tracking-tight mb-2"
style={{ color: "var(--color-text-readable)" }}
>
Ready to join?
{tp("joinNow", { hotelName: hotelName ?? "Hotel" })}
</h2>
<p
className="text-sm mb-6 max-w-sm mx-auto"
style={{ color: "var(--color-text-muted)" }}
>
Create your free account and start your adventure today.
{tp("welcomeBody")}
</p>
<Link
href="/register"
@@ -505,7 +556,7 @@ export default async function Home() {
height={20}
unoptimized
/>
Create free account
{tp("register")}
</Link>
</div>
</div>
+10 -16
View File
@@ -10,7 +10,8 @@ import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings";
export default async function RegisterPage() {
const t = await getTranslations("header");
const th = await getTranslations("header");
const tpr = await getTranslations("pages.register");
const [hotelName, imagerRaw, cfg] = await Promise.all([
siteSettings.get("hotel_name", "Hotel"),
siteSettings.get(
@@ -67,26 +68,19 @@ export default async function RegisterPage() {
}}
>
<span className="w-2 h-2 rounded-full bg-green-500" />
{online.toLocaleString()} online
{tpr("usersOnline", { count: online })}
</div>
<h1
className="text-xl font-black"
style={{ color: "var(--color-text-readable)" }}
>
Join{" "}
<span
style={{
color: "var(--color-primary-readable, var(--color-primary))",
}}
>
{hotelName}
</span>
{tpr("title")} — {hotelName}
</h1>
<p
className="text-xs mt-1 mx-auto max-w-[200px] leading-relaxed"
style={{ color: "var(--color-text-muted)" }}
>
{t("tagline")}
{th("tagline")}
</p>
</div>
@@ -103,7 +97,7 @@ export default async function RegisterPage() {
className="text-xs font-bold uppercase tracking-widest mb-3"
style={{ color: "var(--color-text-muted)" }}
>
Who&apos;s online
{tpr("whoIsOnline")}
</h3>
<div className="grid grid-cols-4 gap-1.5">
{recentUsers.map((u) => (
@@ -143,7 +137,7 @@ export default async function RegisterPage() {
className="text-xs text-center"
style={{ color: "var(--color-text-muted)" }}
>
Already have an account?{" "}
{tpr("alreadyHaveAccountPre")}{" "}
<Link
href="/login"
className="font-extrabold hover:underline"
@@ -151,7 +145,7 @@ export default async function RegisterPage() {
color: "var(--color-primary-readable, var(--color-primary))",
}}
>
Sign in
{tpr("alreadyHaveAccountLink")}
</Link>
</p>
</div>
@@ -163,13 +157,13 @@ export default async function RegisterPage() {
className="text-2xl font-black tracking-tight"
style={{ color: "var(--color-text-readable)" }}
>
Create your account
{tpr("title")}
</h2>
<p
className="text-sm mt-0.5"
style={{ color: "var(--color-text-muted)" }}
>
Free and takes less than a minute
{tpr("subtitle")}
</p>
</div>
<RegisterForm