diff --git a/src/app/(site)/login/page.tsx b/src/app/(site)/login/page.tsx index a5960f8d..2eff40e6 100644 --- a/src/app/(site)/login/page.tsx +++ b/src/app/(site)/login/page.tsx @@ -1,11 +1,13 @@ "use client"; +import { KeyRound, Lock, Mail } from "lucide-react"; import Link from "next/link"; import { signIn } from "next-auth/react"; import { useTranslations } from "next-intl"; import { type FormEvent, useState } from "react"; import { precheckLogin } from "@/actions/auth-precheck"; -import { ContentCard } from "@/components/public/ui"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; export default function LoginPage() { const t = useTranslations("pages.login"); @@ -51,108 +53,154 @@ export default function LoginPage() { } return ( -
- -
- setUsername(e.target.value)} - placeholder={t("usernamePlaceholder")} - autoComplete="username" - disabled={needs2fa} - /> - setPassword(e.target.value)} - placeholder={t("passwordPlaceholder")} - autoComplete="current-password" - disabled={needs2fa} - /> - {needs2fa ? ( - setCode(e.target.value)} - placeholder={t("codePlaceholder")} - inputMode="numeric" - autoComplete="one-time-code" - // biome-ignore lint/a11y/noAutofocus: focus the only relevant field in the 2FA step - autoFocus - /> - ) : null} - -
- - {!needs2fa ? ( - <> -
- - {t("or")} - -
-
- - -
- - ) : null} - - {error ? ( -

- {error} +

+
+
+
+ +
+

+ {t("title")} +

+

+ {needs2fa ? t("subtitle2fa") : t("subtitle")}

- ) : null} -

- {t("noAccount")} {t("createOne")} Β·{" "} - {t("forgotPassword")} -

- -
+ + + + + + {needs2fa ? t("subtitle2fa") : t("subtitle")} + + + {needs2fa ? "Enter your authentication code" : "Sign in to your account"} + + + +
+
+
+ + setUsername(e.target.value)} + placeholder={t("usernamePlaceholder")} + autoComplete="username" + disabled={needs2fa} + className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] px-10 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20" + /> +
+
+ + setPassword(e.target.value)} + placeholder={t("passwordPlaceholder")} + autoComplete="current-password" + disabled={needs2fa} + className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] px-10 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20" + /> +
+ {needs2fa ? ( + setCode(e.target.value)} + placeholder={t("codePlaceholder")} + inputMode="numeric" + autoComplete="one-time-code" + autoFocus + className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] px-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20" + /> + ) : null} +
+ + {error ? ( +
+

+ {error} +

+
+ ) : null} + + +
+ + {!needs2fa ? ( + <> +
+
+
+
+
+ + {t("or")} + +
+
+ +
+ + +
+ + ) : null} + +
+

+ {t("noAccount")}{" "} + + {t("createOne")} + + Β· + + {t("forgotPassword")} + +

+
+ + +
+ ); } diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 0ef5b04e..fe085925 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,4 +1,4 @@ -import { LogOut } from "lucide-react"; +import { LayoutDashboard, LogOut, Shield } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -35,7 +35,7 @@ export default async function AdminLayout({ }>
@@ -60,39 +60,52 @@ async function Sidebar({ data-admin className="max-lg:static sticky top-0 self-start h-full lg:h-screen overflow-y-auto bg-[var(--admin-sidebar-background)] text-[var(--admin-sidebar-text-readable)] shadow-xl flex flex-col" > -
+ {/* Sidebar Header */} +
{initial}
-
+
{staff.username}
- + + {t("rankLabel", { rank: staff.rank })}
-
+
+ {/* Navigation */} -
+ {/* Footer */} +
{t("backToSite")} @@ -136,25 +150,35 @@ async function Topbar({ const t = await getTranslations("pages.admin.nav"); return ( -
+
-

- {t("housekeeping")} -

- - - {t("hotelManagementPanel")} - +
+ +
+
+

+ {t("housekeeping")} +

+ + {t("hotelManagementPanel")} + +
- - - - {staff.username} - - - {t("rankLabel", { rank: staff.rank })} - +
+ + +
+
+
+ + {staff.username} + + + + {t("rankLabel", { rank: staff.rank })} + +
); diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 4123eb8f..17b9fbf2 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -4,6 +4,8 @@ import { Ban, Database, Newspaper, + Sparkles, + TrendingUp, Users, } from "lucide-react"; import Link from "next/link"; @@ -69,22 +71,36 @@ export default async function AdminDashboard() { } return ( -
-
-
- +
+ {/* Header */} +
+
+
+ +
+
+

+ {t("title")} +

+

+ {t("subtitle")} +

+
-
-

- {t("title")} -

-

- {t("subtitle")} -

+
+ + + System Online + + + + +
-
+ {/* Status Cards */} +
-
-
-

- {t("usersByRank")} -

+ {/* Analytics Grid */} +
+ {/* Users by Rank */} +
+
+
+ +
+

+ {t("usersByRank")} +

+
{rankCounts.length === 0 ? (

{dbOk ? t("noAccountsToBreakDown") : t("databaseDown")}

) : ( -
+
{rankCounts.map((r) => { const pct = maxRankCount > 0 @@ -132,21 +155,14 @@ export default async function AdminDashboard() {
- + {t("rankLabel", { rank: r.rank })} -
+
0 ? 4 : 0)}%`, }} @@ -162,48 +178,54 @@ export default async function AdminDashboard() { )}
-
-
-

- {t("recentStaffActivity")} -

+ {/* Recent Staff Activity */} +
+
+
+
+ +
+

+ {t("recentStaffActivity")} +

+
{t("viewAll")}
{activity.length === 0 ? ( -

+

{t("noStaffActivityLogged")}

) : ( -
+
{activity.map((a, i) => (
0 ? "border-t border-[var(--admin-border)]" : ""}`} + className={`flex items-center gap-3 rounded-xl px-3 py-2.5 transition-all duration-150 hover:bg-[var(--admin-accent)]/5 ${i > 0 ? "mt-1" : ""}`} > -
+
-
-
+
+
{nameById.get(Number(a.userId)) ?? `#${a.userId}`}
-
+
{a.description}
-
- +
+ {a.action} -
+
{fromDate(a.createdAt)}
diff --git a/src/app/globals.css b/src/app/globals.css index 5a8cd151..b91e0e22 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -14,10 +14,11 @@ --radius-md: 12px; --radius-lg: 16px; --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04); - --shadow-card-hover: 0 8px 24px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.06); + --shadow-card-hover: 0 8px 30px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.06); + --shadow-elevated: 0 20px 60px rgba(0, 0, 0, 0.12), 0 4px 16px rgba(0, 0, 0, 0.06); + --shadow-glow: 0 0 20px color-mix(in srgb, var(--color-primary) 30%, transparent), 0 0 60px color-mix(in srgb, var(--color-primary) 10%, transparent); } -/* Default theme variables. ThemeVars may override these at runtime. */ :root { --color-primary: #eeb425; --color-primary-hover: #d4a01f; @@ -86,7 +87,6 @@ --border-subtle: color-mix(in srgb, var(--color-text-muted) 18%, transparent); } -/* Dark mode colors + readable contrast vars are generated by ThemeVars. */ html.dark input, html.dark select, html.dark textarea { @@ -114,7 +114,6 @@ body { -moz-osx-font-smoothing: grayscale; } -/* Mobile-friendly improvements for every screen */ button, a, input, @@ -126,12 +125,10 @@ summary, touch-action: manipulation; } -/* Momentum scrolling on iOS */ * { -webkit-overflow-scrolling: touch; } -/* Prevent iOS zoom on input focus */ @media (max-width: 48rem) { input, select, @@ -140,7 +137,6 @@ summary, } } -/* Semantic colors: safe across every complete light/dark preset. */ .theme-text-muted { color: var(--color-text-muted-readable); } @@ -192,7 +188,6 @@ summary, background: transparent; } -/* ── Atom theme components ────────────────────────────────── */ @layer components { .nav-item { @apply flex h-auto md:h-[54px] items-center text-[13px] font-bold px-3 md:px-4 relative; @@ -202,12 +197,14 @@ summary, white-space: nowrap; transition: color 0.15s ease-in-out, - background-color 0.15s ease-in-out; + background-color 0.15s ease-in-out, + transform 0.15s ease; border-radius: 0.5rem; } .nav-item:hover { color: var(--color-primary-readable, var(--color-primary)); background-color: color-mix(in srgb, var(--color-primary) 8%, transparent); + transform: translateY(-1px); } .nav-item[aria-current="page"] { color: var(--color-primary-readable, var(--color-primary)); @@ -222,11 +219,13 @@ summary, color: var(--color-dropdown-text-readable, var(--color-text-readable)); transition: background-color 0.1s ease-in-out, - color 0.1s ease-in-out; + color 0.1s ease-in-out, + transform 0.1s ease; } .dropdown-item:hover { background-color: color-mix(in srgb, var(--color-primary) 12%, transparent); color: var(--color-primary-readable, var(--color-primary)); + transform: translateX(2px); } .dropdown-item:focus-visible { outline: 2px solid var(--color-primary-readable, var(--color-primary)); @@ -237,10 +236,10 @@ summary, border: 1px solid color-mix(in srgb, var(--color-text-muted) 14%, transparent); box-shadow: 0 8px 32px color-mix(in srgb, rgb(0, 0, 0) 20%, transparent); + border-radius: 16px; } } -/* ── Site background ── */ .site-bg { background: linear-gradient( @@ -262,7 +261,6 @@ html.dark .site-bg { background-blend-mode: overlay; } -/* Admin must not inherit the public wallpaper β€” solid HK canvas only. */ body.site-bg:has([data-admin]), html.dark body.site-bg:has([data-admin]) { background: var(--admin-canvas); @@ -270,14 +268,13 @@ html.dark body.site-bg:has([data-admin]) { background-blend-mode: normal; } - .article-body { line-height: 1.7; } .article-body img { max-width: 100%; height: auto; - border-radius: 8px; + border-radius: 12px; } .article-body p { margin: 0 0 0.85rem; @@ -307,15 +304,17 @@ html.dark body.site-bg:has([data-admin]) { transform: translateY(-2px); box-shadow: var(--shadow-card-hover); } -/* ── App component classes (used across the converted pages) ── */ + h1 { font-size: var(--size-heading-h1, 30px); font-weight: 800; margin: 0.2em 0 0.5em; + letter-spacing: -0.02em; } h2 { font-size: var(--size-heading-h2, 22px); font-weight: 700; + letter-spacing: -0.01em; } h3 { font-size: var(--size-heading-h3, 18px); @@ -343,6 +342,7 @@ a:hover { padding: 2rem 2.25rem; box-shadow: var(--shadow-card); margin-bottom: 1.5rem; + backdrop-filter: blur(8px); } .card { background: var(--color-surface); @@ -507,25 +507,21 @@ tbody tr:hover { background-size: contain; outline: 1px solid #6caff4; } -/* ── Admin panel (Housekeeping) ───────────────────────────────── - Modern, consistent styling for all admin content pages. */ + .admin-page { scroll-margin-top: 1rem; } -/* Hub chrome already shows the section title β€” soften page-level heroes. */ .admin-hub-panel > .space-y-6 > div:first-child > h1.text-3xl { font-size: 1.15rem; letter-spacing: -0.01em; } -/* Page heading */ .admin-page main > h1:first-child, .admin-page > h1:first-child { margin-top: 0; } -/* Cards used inside admin pages */ .admin-card { background: var(--admin-surface); color: var(--admin-text); @@ -533,10 +529,12 @@ tbody tr:hover { border-radius: 16px; box-shadow: var(--shadow-card); padding: 1.25rem; - transition: box-shadow 0.2s ease; + transition: box-shadow 0.2s ease, transform 0.2s ease; +} +.admin-card:hover { + box-shadow: var(--shadow-card-hover); } -/* Card with filter/search bar β€” compact horizontal layout */ .admin-filter-bar { display: flex; flex-wrap: wrap; @@ -550,7 +548,6 @@ tbody tr:hover { min-width: 200px; } -/* Compact stat row for list pages (counts, summaries) */ .admin-stat-row { display: flex; flex-wrap: wrap; @@ -569,7 +566,6 @@ tbody tr:hover { font-weight: 600; } -/* Tables */ .admin-page table { width: 100%; background: var(--admin-surface); @@ -610,7 +606,6 @@ tbody tr:hover { border-radius: 0 16px 0 0; } -/* Section heading inside admin pages */ .admin-section-title { font-size: 0.9rem; font-weight: 700; @@ -627,16 +622,13 @@ tbody tr:hover { gap: 0.15rem; } -/* Empty state */ .admin-empty { text-align: center; padding: 2.5rem 1rem; color: var(--admin-text-muted); font-size: 0.85rem; } -/* ── Public design system (faithful to the atom theme) ────────── - content-card = surface card with a primary-tinted header (icon + - title + subtitle) and a padded body β€” the signature atom block. */ + .content-card { display: flex; flex-direction: column; @@ -647,10 +639,11 @@ tbody tr:hover { border-radius: var(--radius-lg); box-shadow: var(--shadow-card); overflow: hidden; - transition: box-shadow 0.2s ease; + transition: box-shadow 0.2s ease, transform 0.2s ease; } .content-card:hover { box-shadow: var(--shadow-card-hover); + transform: translateY(-2px); } .content-card-head { display: flex; @@ -704,7 +697,6 @@ tbody tr:hover { padding: 1.1rem; } -/* Responsive card grids (atom: 1 / sm2 / md3 / lg4). */ .card-grid { display: grid; gap: 1rem; @@ -732,7 +724,6 @@ tbody tr:hover { } } -/* Stat tiles (value in the primary colour + muted label). */ .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); @@ -744,6 +735,11 @@ tbody tr:hover { border-radius: 12px; padding: 0.85rem 1rem; text-align: center; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} +.stat-block:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-card-hover); } .stat-block-value { font-size: 1.6rem; @@ -757,7 +753,6 @@ tbody tr:hover { color: var(--color-text-muted-readable, var(--color-text-muted)); } -/* Empty states */ .empty-state { text-align: center; padding: 2rem 1rem; @@ -767,7 +762,6 @@ tbody tr:hover { margin-bottom: 0.4rem; } -/* Online / offline pill */ .online-badge { display: inline-flex; align-items: center; @@ -795,7 +789,6 @@ tbody tr:hover { color-mix(in srgb, var(--color-success) 22%, transparent); } -/* Leaderboard rank chip */ .rank-badge { display: inline-grid; place-items: center; @@ -821,7 +814,6 @@ tbody tr:hover { border-color: #f97316; } -/* ── Radio sidebar ──────────────────────────────────────────── */ .radio-layout { display: flex; gap: 1.5rem; @@ -865,20 +857,37 @@ tbody tr:hover { } } -/* ── Skeleton shimmer animation ─────────────────────────────── */ @keyframes shimmer { - 0% { - opacity: 0.6; - } - 50% { - opacity: 1; - } - 100% { - opacity: 0.6; - } + 0% { opacity: 0.6; } + 50% { opacity: 1; } + 100% { opacity: 0.6; } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes slideUp { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 8px color-mix(in srgb, var(--color-primary) 20%, transparent); } + 50% { box-shadow: 0 0 20px color-mix(in srgb, var(--color-primary) 40%, transparent); } +} + +.animate-fade-in { + animation: fadeIn 0.4s ease-out; +} +.animate-slide-up { + animation: slideUp 0.5s ease-out; +} +.animate-pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; } -/* Footer CMS info popup */ .cms-info-trigger { appearance: none; border: 0; @@ -918,7 +927,7 @@ tbody tr:hover { width: min(440px, 100%); border: 1px solid color-mix(in srgb, var(--color-primary) 30%, color-mix(in srgb, var(--color-text-muted) 22%, transparent)); - border-radius: 8px; + border-radius: 16px; background: linear-gradient( 135deg, @@ -1049,7 +1058,6 @@ tbody tr:hover { } } -/* ── Mobile responsive overrides ──────────────────────────── */ @media (max-width: 48rem) { h1 { font-size: clamp(1.25rem, 5vw, var(--size-heading-h1, 30px)); @@ -1103,7 +1111,6 @@ tbody tr:hover { } } -/* make tables scroll on small screens */ @media (max-width: 640px) { table { display: block; @@ -1112,21 +1119,7 @@ tbody tr:hover { } } -/* ── Admin (HK) semantic token remap ────────────────────────────────────── - Shared shadcn UI primitives (Button, Badge, Input, Select, Card, Table, - Tabs, Dialog, Switch, Checkbox, Tooltip, …) are built on the site's - semantic tokens (--color-background, --color-primary, --color-popover, - --color-border, --color-ring, --color-muted-foreground, …). On admin - pages we remap those tokens onto the admin palette so every embedded - component renders with the admin theme and guaranteed contrast, without - touching any component file. - - Gated by `body:has([data-admin])` so the public site is completely - unaffected. This also covers Radix portals (Dialog/Select/Dropdown/ - Popover/Tooltip) which mount under . */ body:has([data-admin]) { - /* Full publicβ†’admin token remap so shadcn + body + leftover - public utilities all inherit the admin palette with contrast. */ --color-background: var(--admin-canvas); --color-foreground: var(--admin-text); --color-card: var(--admin-surface); @@ -1155,7 +1148,6 @@ body:has([data-admin]) { --color-border: var(--admin-border); --color-input: var(--admin-border); --color-ring: var(--admin-accent); - /* Public Atom tokens still referenced by body / .muted / inputs */ --color-text: var(--admin-text); --color-text-readable: var(--admin-text); --color-text-muted: var(--admin-text-muted); @@ -1173,7 +1165,6 @@ body:has([data-admin]) { font-family: var(--font-family); } -/* Admin forms / boxes follow admin tokens in both light and dark. */ body:has([data-admin]) input:not([type="checkbox"]):not([type="radio"]):not( [type="range"] ):not([type="file"]):not([type="color"]), @@ -1209,7 +1200,6 @@ body:has([data-admin]) .btn-outline:hover { background: color-mix(in srgb, var(--admin-accent) 12%, transparent); color: var(--admin-text); } -/* Override public html.dark input hardcodes inside admin. */ html.dark body:has([data-admin]) input:not([type="checkbox"]):not( [type="radio"] ):not([type="range"]):not([type="file"]):not([type="color"]), @@ -1219,7 +1209,6 @@ html.dark body:has([data-admin]) textarea { color: var(--admin-text); } -/* ── Details/summary open states for nav ──────────────────── */ details[open] > summary .details-open\:hidden { display: none; } diff --git a/src/components/admin/dashboard.tsx b/src/components/admin/dashboard.tsx index 8005234c..57ae11bc 100644 --- a/src/components/admin/dashboard.tsx +++ b/src/components/admin/dashboard.tsx @@ -14,7 +14,6 @@ export interface OnlineUser { type State = "ok" | "warn" | "danger" | "neutral"; -/** A single live-status tile (online users, emulator, DB, …). */ export function StatusCard({ label, value, @@ -28,31 +27,40 @@ export function StatusCard({ state?: State; icon?: ReactNode; }) { + const stateColors: Record = { + ok: "from-green-500/20 to-green-500/5 ring-green-500/20", + warn: "from-yellow-500/20 to-yellow-500/5 ring-yellow-500/20", + danger: "from-red-500/20 to-red-500/5 ring-red-500/20", + neutral: "from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 ring-[var(--admin-accent)]/10", + }; + return ( -
-
- - {label} - - {icon ? ( - - {icon} +
+
+
+
+ + {label} + {icon ? ( + + {icon} + + ) : null} +
+
+ {value} +
+ {hint ? ( +
+ {hint} +
) : null}
-
- {value} -
- {hint ? ( -
- {hint} -
- ) : null}
); } -/** A row in the system-diagnostics list with an ok/warn/error pill. */ export function DiagnosticRow({ label, detail, @@ -71,31 +79,23 @@ export function DiagnosticRow({ : "bg-[var(--admin-error-subtle)] shadow-[0_0_0_3px_rgba(239,68,68,0.22)]"; const badgeColor = state === "ok" - ? "bg-[var(--admin-success-subtle)]" + ? "bg-green-500/10 text-green-500 border-green-500/20" : state === "danger" - ? "bg-[var(--admin-error-subtle)]" - : "bg-[var(--admin-accent)]/18"; + ? "bg-red-500/10 text-red-500 border-red-500/20" + : "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"; return ( -
- - {label} - - {detail} - - +
+ + {label} + {detail} + {text}
); } -/** Small labelled key/value used in the server-info card. */ export function InfoItem({ label, value, @@ -105,13 +105,12 @@ export function InfoItem({ }) { return (
- {label} - {value} + {label} + {value}
); } -/** Live list of online users with room and activity tracking */ export function OnlineUsersWidget({ users, isLoading = false, @@ -122,7 +121,6 @@ export function OnlineUsersWidget({ const formatLastSeen = (date: Date) => { const now = new Date(); const diff = Math.floor((now.getTime() - date.getTime()) / 1000); - if (diff < 60) return "Just now"; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; @@ -130,46 +128,37 @@ export function OnlineUsersWidget({ }; return ( -
-
-

+
+
+

Online Now

- + {isLoading ? "..." : users.length} users
- -
+
{users.length === 0 ? ( -
- - No users online - +
+

No users online

) : ( users.map((user) => (
-
+
{user.username} - {user.roomId && user.roomName && ( -
- -
- )}
- -
-
+
+ -
+
{user.roomId && user.roomName ? ( - - 🏠 {user.roomName} + + {user.roomName} ) : ( - - ⏱️ {formatLastSeen(user.lastActivity)} + + {formatLastSeen(user.lastActivity)} )}
diff --git a/src/components/auth/home-login-form.tsx b/src/components/auth/home-login-form.tsx index f6258f2f..dd685c6c 100644 --- a/src/components/auth/home-login-form.tsx +++ b/src/components/auth/home-login-form.tsx @@ -47,71 +47,85 @@ export function HomeLoginForm() { } return ( -
-
- - setUsername(e.target.value)} - placeholder="" - className="focus:ring-0 relative z-10 w-full rounded border-4 border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200" - style={{ borderColor: "#e5e7eb", borderWidth: "4px" }} - required - disabled={needs2fa} - /> -
-
- - setPassword(e.target.value)} - placeholder="" - className="focus:ring-0 relative z-10 w-full rounded border-4 border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200" - style={{ borderColor: "#e5e7eb", borderWidth: "4px" }} - required - disabled={needs2fa} - /> -
- {needs2fa ? ( -
-
- ) : null} +
+
+ + setPassword(e.target.value)} + placeholder="Enter your password" + className="w-full rounded-lg border-2 px-3 py-2.5 text-sm transition-all duration-200 bg-[var(--color-background)]" + style={{ borderColor: "var(--input-border-color)" }} + required + disabled={needs2fa} + /> +
+ {needs2fa ? ( +
+ + setCode(e.target.value)} + placeholder="Enter your 2FA code" + inputMode="numeric" + className="w-full rounded-lg border-2 px-3 py-2.5 text-sm transition-all duration-200 bg-[var(--color-background)]" + style={{ borderColor: "var(--input-border-color)" }} + autoFocus + /> +
+ ) : null} +
+ {error ? ( -

+

{error}

) : null} + ); diff --git a/src/components/home/guest-view.tsx b/src/components/home/guest-view.tsx index e65b9082..6c284743 100644 --- a/src/components/home/guest-view.tsx +++ b/src/components/home/guest-view.tsx @@ -1,14 +1,16 @@ +import { ArrowRight, Lock, Newspaper, Sparkles, Users } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { HomeLoginForm } from "@/components/auth/home-login-form"; +import { Button } from "@/components/ui/button"; import { avatarImageUrl, excerpt } from "@/lib/format"; import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; export default async function GuestView() { const t = await getTranslations("pages.home"); - const _hotelName = await siteSettings.get("hotel_name", "Atom"); + const hotelName = await siteSettings.get("hotel_name", "Atom"); const imager = (await siteSettings.get( "habbo_imaging_url", @@ -24,7 +26,7 @@ export default async function GuestView() { try { articles = await prisma.websiteArticles.findMany({ orderBy: { createdAt: "desc" }, - take: 8, + take: 6, select: { slug: true, title: true, shortStory: true, image: true }, }); } catch {} @@ -39,203 +41,196 @@ export default async function GuestView() { } catch {} return ( -
-
-
-
-
-
-
+
+ {/* Hero Banner */} +
+
+
+
+
+
+ + Welcome to {hotelName} +
+

+ Join the Community +

+

+ Connect with friends, explore the hotel, and create unforgettable memories. Your adventure starts here. +

+
+ + + + + + +
-
- +
+
+
+

+ + Sign In +

+
+
+ +
+
- - -
- Register -
- - REGISTER NOW - -
- - {recentUsers.length > 0 && ( -
-
-

- Recent Users -

-
-
- {recentUsers.map((u) => ( -
- {u.username} -
- ))} -
-
- )}
-
-
-
-

- {t("latestNews") || "Latest News"} -

+
+ {/* Latest News */} +
+
+
+
+ +
+

{t("latestNews")}

+
+ + View all +
-
- {articles.length === 0 ? ( -

- {t("noArticles")} -

- ) : ( -
- {articles.map((a) => ( + + {articles.length === 0 ? ( +
+ +

{t("noArticles")}

+
+ ) : ( +
+ {articles.slice(0, 2).map((a, i) => ( + +
+ {a.title} +
+
+

+ {a.title} +

+ {a.shortStory && ( +

+ {excerpt(a.shortStory, 80)} +

+ )} +
+
+ + ))} + {articles.slice(2, 6).map((a, i) => ( + +
+ {a.title} +
+
+

{a.title}

+ {a.shortStory && ( +

+ {excerpt(a.shortStory, 60)} +

+ )} +
+ + + ))} +
+ )} +
+ + {/* Sidebar */} +
+ {/* Recent Users */} + {recentUsers.length > 0 && ( +
+
+ +

Recent Users

+
+
+ {recentUsers.map((u) => ( -
+
{a.title} -
-

- {a.title} -

- {a.shortStory && ( -

- {excerpt(a.shortStory, 80)} -

- )} -
+ + {u.username} + ))}
- )} -
+
+ )} + + {/* Quick Register CTA */} + +
+
+
+
+ +

+ Join {hotelName} Today +

+

+ Create your free account and start your adventure! +

+
+ Register Now +
+
+
+
diff --git a/src/components/home/user-view.tsx b/src/components/home/user-view.tsx index 1e85dd0c..def89b65 100644 --- a/src/components/home/user-view.tsx +++ b/src/components/home/user-view.tsx @@ -1,3 +1,4 @@ +import { ArrowRight, Gamepad2, Newspaper, Sparkles, Users } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; @@ -53,192 +54,109 @@ export default async function UserView({ .catch(() => []); return ( -
-
-
-
-
-
-
-
-
- -
+
+ {/* Main Content */} +
+ {/* Hero/Welcome Banner */} +
+
+
+
+
+
+
{username}
- -
- - {username} - -

- {motto || "Welcome back!"} -

+
+ + +
-
- - - +
+

+ Welcome back, {username} +

+

+ {motto || "Happy to see you again!"} +

+
+ +
+ + {t("enterHotel", { hotelName })} + +
+ + +
+ View Profile +
+ +
-
-
- {t("onlineFriends")} -
-
- {onlineFriends.length === 0 ? ( -

- {t("noFriendsOnline")} -

- ) : ( - onlineFriends.map((f) => ( - - {f.username} - - )) - )} + {/* Online Friends Bar */} +
+
+
+
+ + +
+ {t("onlineFriends")} +
+
+ {onlineFriends.length === 0 ? ( +

+ {t("noFriendsOnline")} +

+ ) : ( + onlineFriends.map((f) => ( + +
+ {f.username} +
+ + {f.username} + + + )) + )} +
-
+ {/* Sidebar */} +
{latestArticle ? ( -
-
-

- {t("latestArticle")} -

+
+
+ +

{t("latestArticle")}

@@ -246,27 +164,36 @@ export default async function UserView({ src={latestArticle.image} alt={latestArticle.title} fill - className="object-cover transition-transform duration-300 group-hover:scale-105" + className="object-cover transition-transform duration-500 group-hover:scale-110" unoptimized /> +
-
-

{latestArticle.title}

+
+

{latestArticle.title}

{latestArticle.shortStory && ( -

+

{excerpt(latestArticle.shortStory, 100)}

)} +
+ Read more +
) : null} + + {/* Quick Stats */} +
+ +

+ Explore the Hotel +

+

+ Check out the latest news, visit the marketplace, or chat with friends! +

+
); diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index bdfe2674..f824c4c7 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -15,13 +15,13 @@ export async function Navigation() { return (

)} - {/* Online badge */}
; theme-init.js applies the - * saved choice before paint. We sync React state after mount so SSR hydration - * does not desync from the real DOM class. - */ export function ThemeSwitcher({ variant = "nav", }: { @@ -28,7 +23,7 @@ export function ThemeSwitcher({ try { localStorage.setItem("theme", next ? "dark" : "light"); } catch { - /* private mode / storage disabled β€” runtime toggle still works */ + /* private mode / storage disabled */ } } @@ -43,10 +38,10 @@ export function ThemeSwitcher({ aria-label={label} title={title} suppressHydrationWarning - className="flex-none grid place-items-center w-9 h-9 rounded-xl border border-[var(--admin-border)] bg-[var(--admin-surface-elevated)] text-[var(--admin-text)] hover:bg-[var(--admin-accent)]/15 hover:text-[var(--admin-accent)] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]" + className="flex-none grid place-items-center w-9 h-9 rounded-xl border border-[var(--admin-border)] bg-[var(--admin-surface-elevated)] text-[var(--admin-text)] hover:bg-[var(--admin-accent)]/15 hover:text-[var(--admin-accent)] transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)] active:scale-95" style={{ visibility: ready ? "visible" : "hidden" }} > - {dark ? : } + {dark ? : } ); } @@ -55,15 +50,13 @@ export function ThemeSwitcher({ ); } diff --git a/src/components/top-header.tsx b/src/components/top-header.tsx index 1321a14e..72d79b7a 100644 --- a/src/components/top-header.tsx +++ b/src/components/top-header.tsx @@ -1,3 +1,4 @@ +import { LogOut, Shield, User } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { auth, signOut } from "@/lib/auth"; @@ -16,22 +17,20 @@ function Currency({ }) { const textColor = "var(--color-navbar-text-readable, var(--color-navbar-text))"; - const valueColor = - "var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))"; return ( -
+
{amount.toLocaleString()} {" "} - {label} + {label}
); @@ -60,9 +59,7 @@ export async function TopHeader() { if (c.type === 0) duckets = c.amount; if (c.type === 5) diamonds = c.amount; } - } catch { - // render without live balances - } + } catch {} const imager = (await siteSettings.get( @@ -75,9 +72,9 @@ export async function TopHeader() { "var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))"; return ( -
+
-
+
-
+
{isStaff ? (
+ Administration Admin
Admin panel @@ -128,7 +126,7 @@ export async function TopHeader() {
{session.user.name} @@ -147,9 +145,16 @@ export async function TopHeader() {
+
+
{session.user.name}
+
+ {session.user.email || "Player"} +
+
+ My dashboard + My profile @@ -176,13 +182,7 @@ export async function TopHeader() { > Login activity -
+
{ "use server"; @@ -192,16 +192,13 @@ export async function TopHeader() {