revert(site): restore classic Habbo landing with soft theme polish
CI / check (push) Successful in 2m27s
CI / deploy (push) Successful in 1m45s
CI / publish-container (push) Successful in 1m55s

Replace the premium dark-gaming redesign of home, login and register with
the original classic landing (Habbo sky background, AuthTopBar, SurfaceCard
layout). Keeps the theme background visible again and adds a subtle
theme-aware brand halo behind the hero/Frank plus a soft primary glow on
card hover.

Also upgrades dependencies: next 16.3.5, vite 8.3.0 (typescript 7.0.2 was
already latest). Temporarily lowers pnpm minimumReleaseAge to 60 min so the
fresh 16.3.5 release can be installed; restore to 1440 once it is 24h old.
This commit is contained in:
openhands committed 2026-09-12 16:14:21 +02:00
1 parent cc58f16230
commit d755ab7ce1
11 files changed
+742 -812

No files matched your search

+2 -1
View File
@@ -66,7 +66,7 @@
"motion": "13.2.0", "motion": "13.2.0",
"music-metadata": "11.15.0", "music-metadata": "11.15.0",
"mysql2": "3.24.3", "mysql2": "3.24.3",
"next": "16.3.4", "next": "16.3.5",
"next-auth": "5.0.0-beta.32", "next-auth": "5.0.0-beta.32",
"next-intl": "4.14.2", "next-intl": "4.14.2",
"otplib": "13.5.0", "otplib": "13.5.0",
@@ -102,6 +102,7 @@
"tailwindcss": "4.3.3", "tailwindcss": "4.3.3",
"tsx": "4.23.13", "tsx": "4.23.13",
"typescript": "7.0.2", "typescript": "7.0.2",
"vite": "8.3.0",
"vitest": "5.0.0" "vitest": "5.0.0"
} }
} }
+61 -58
View File
@@ -86,14 +86,14 @@ importers:
specifier: 3.24.3 specifier: 3.24.3
version: 3.24.3(@types/[email protected]) version: 3.24.3(@types/[email protected])
next: next:
specifier: 16.3.4 specifier: 16.3.5
version: 16.3.4(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]) version: 16.3.5(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
next-auth: next-auth:
specifier: 5.0.0-beta.32 specifier: 5.0.0-beta.32
version: 5.0.0-beta.32([email protected].4(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected]) version: 5.0.0-beta.32([email protected].5(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected])
next-intl: next-intl:
specifier: 4.14.2 specifier: 4.14.2
version: 4.14.2(@swc/[email protected])([email protected].4(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected]) version: 4.14.2(@swc/[email protected])([email protected].5(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected])
otplib: otplib:
specifier: 13.5.0 specifier: 13.5.0
version: 13.5.0 version: 13.5.0
@@ -188,9 +188,12 @@ importers:
typescript: typescript:
specifier: 7.0.2 specifier: 7.0.2
version: 7.0.2 version: 7.0.2
vite:
specifier: 8.3.0
version: 8.3.0(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
vitest: vitest:
specifier: 5.0.0 specifier: 5.0.0
version: 5.0.0(@types/[email protected])(@vitest/[email protected])([email protected](@noble/[email protected]))([email protected](@types/[email protected])([email protected]))(vite@8.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected])) version: 5.0.0(@types/[email protected])(@vitest/[email protected])([email protected](@noble/[email protected]))([email protected](@types/[email protected])([email protected]))(vite@8.3.0(@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
packages: packages:
@@ -1068,57 +1071,57 @@ packages:
resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@next/[email protected].4': '@next/[email protected].5':
resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} resolution: {integrity: sha512-NWEXVDMqoEo0ktmU6u0sE2Vg0LOcsD7NnOTJNo3/fEaTfsg+F1bMIxuDmQbda4e3yTIQwVdUREF2yIuMOusKtg==}
'@next/[email protected].4': '@next/[email protected].5':
resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} resolution: {integrity: sha512-pMmGgETfKvElucLHtVaeiMRbp2zUbvKx7b1yGko0liBz3cw1mKSggWN/Rp/wPz8z+E1O82u3r4L1Co+ZS5hokQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@next/[email protected].4': '@next/[email protected].5':
resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} resolution: {integrity: sha512-76VaGYvf6HPa5/w12yLkE3dXTn9AfdEviI79oEL3aZoAmRLc9rWitjWqyjViVysK/ht/y9YKzFkBrUdi/wGkow==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@next/[email protected].4': '@next/[email protected].5':
resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} resolution: {integrity: sha512-zKDELJ5jSQMHeO/hmXUQsAzagX4bQD4OiMi3pQ5FbUj+yK506oLVHnKA2YXMlbg1EHHqJYtyePOgByIDXD1lqw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@next/[email protected].4': '@next/[email protected].5':
resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} resolution: {integrity: sha512-7Vql0pgzCoHagv6+FNOZoqmJqA52c6zeVbhtS/47qFozO1MSx4ms7x7GHiciY8R5CDsSMKMQjJEryoJLcsBIbA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@next/[email protected].4': '@next/[email protected].5':
resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} resolution: {integrity: sha512-NH/xzehyHEFWE2nlcZon7TB/0+H4shfWCi7S1zka815XCOhJDYZhoeJtOYy0dh0WVRWACVXSyGNFFytoMxUhRg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@next/[email protected].4': '@next/[email protected].5':
resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} resolution: {integrity: sha512-lV4+EhWMfS8jcC+EH2nn/Cm5cn6XsgbE07bU9tMH8fCo0tNAqhyzi1b5wQ/Tn6NGFTvKDY65w3ZH95EjwBRAnQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@next/[email protected].4': '@next/[email protected].5':
resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} resolution: {integrity: sha512-/wKzAREX2RF++MhicjDbg8tGn2AiBIM0+EFeTFKoUEUbW5D6amCJehd5Z5G1H5/gxNdgnwoXMcHz24H/c2tGkQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@next/[email protected].4': '@next/[email protected].5':
resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} resolution: {integrity: sha512-LNdCHzgLFc+UeqMS84LzXPaeBRKyqDN9OMyFAr1OrB0XrNw78IRrEVtZvvA7245W/HsaoeVOQX9jPjPk8jojwA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -2654,8 +2657,8 @@ packages:
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
[email protected].4: [email protected].5:
resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} resolution: {integrity: sha512-MdtsTgzyfCPRLC6uJ1mN8ao7lyJ4BB0U6Inhnx3gta1UcCIdHK3yxLG0E8OWQteWD8/Q0qb8A5o7wJaL8M9y2w==}
engines: {node: '>=20.9.0'} engines: {node: '>=20.9.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -3126,13 +3129,13 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite@8.2.2: vite@8.3.0:
resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0 '@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.4.0 || ^0.5.0 '@vitejs/devtools': ^0.7.1
esbuild: ^0.27.0 || ^0.28.0 esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0' jiti: '>=1.21.0'
less: ^4.0.0 less: ^4.0.0
@@ -3821,30 +3824,30 @@ snapshots:
outvariant: 1.4.3 outvariant: 1.4.3
strict-event-emitter: 0.5.1 strict-event-emitter: 0.5.1
'@next/[email protected].4': {} '@next/[email protected].5': {}
'@next/[email protected].4': '@next/[email protected].5':
optional: true optional: true
'@next/[email protected].4': '@next/[email protected].5':
optional: true optional: true
'@next/[email protected].4': '@next/[email protected].5':
optional: true optional: true
'@next/[email protected].4': '@next/[email protected].5':
optional: true optional: true
'@next/[email protected].4': '@next/[email protected].5':
optional: true optional: true
'@next/[email protected].4': '@next/[email protected].5':
optional: true optional: true
'@next/[email protected].4': '@next/[email protected].5':
optional: true optional: true
'@next/[email protected].4': '@next/[email protected].5':
optional: true optional: true
'@noble/[email protected]': {} '@noble/[email protected]': {}
@@ -4413,7 +4416,7 @@ snapshots:
obug: 2.1.4 obug: 2.1.4
std-env: 4.2.0 std-env: 4.2.0
tinyrainbow: 3.1.1 tinyrainbow: 3.1.1
vitest: 5.0.0(@types/[email protected])(@vitest/[email protected])([email protected](@noble/[email protected]))([email protected](@types/[email protected])([email protected]))(vite@8.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected])) vitest: 5.0.0(@types/[email protected])(@vitest/[email protected])([email protected](@noble/[email protected]))([email protected](@types/[email protected])([email protected]))(vite@8.3.0(@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
'@vitest/[email protected]': {} '@vitest/[email protected]': {}
@@ -4421,7 +4424,7 @@ snapshots:
dependencies: dependencies:
'@vitest/istanbul-lib-coverage': 1.0.1 '@vitest/istanbul-lib-coverage': 1.0.1
'@vitest/[email protected]([email protected](@types/[email protected])([email protected]))(vite@8.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected]))': '@vitest/[email protected]([email protected](@types/[email protected])([email protected]))(vite@8.3.0(@types/[email protected])([email protected])([email protected])([email protected])([email protected]))':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.0 '@vitest/spy': 5.0.0
@@ -4429,7 +4432,7 @@ snapshots:
magic-string: 1.2.3 magic-string: 1.2.3
optionalDependencies: optionalDependencies:
msw: 2.15.0(@types/[email protected])([email protected]) msw: 2.15.0(@types/[email protected])([email protected])
vite: 8.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected]) vite: 8.3.0(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
'@vitest/[email protected]': {} '@vitest/[email protected]': {}
@@ -5030,15 +5033,15 @@ snapshots:
dependencies: dependencies:
content-type: 2.1.0 content-type: 2.1.0
[email protected]([email protected].4(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected]): [email protected]([email protected].5(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected]):
dependencies: dependencies:
'@auth/core': 0.41.3 '@auth/core': 0.41.3
next: 16.3.4(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]) next: 16.3.5(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
react: 19.2.8 react: 19.2.8
[email protected]: {} [email protected]: {}
[email protected](@swc/[email protected])([email protected].4(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected]): [email protected](@swc/[email protected])([email protected].5(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected]):
dependencies: dependencies:
'@eloqnt/config': 0.1.0 '@eloqnt/config': 0.1.0
'@eloqnt/format-json': 0.1.0 '@eloqnt/format-json': 0.1.0
@@ -5048,16 +5051,16 @@ snapshots:
'@swc/core': 1.16.2(@swc/[email protected]) '@swc/core': 1.16.2(@swc/[email protected])
icu-minify: 4.14.2 icu-minify: 4.14.2
negotiator: 1.1.0 negotiator: 1.1.0
next: 16.3.4(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]) next: 16.3.5(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
next-intl-swc-plugin-extractor: 4.14.2 next-intl-swc-plugin-extractor: 4.14.2
react: 19.2.8 react: 19.2.8
use-intl: 4.14.2([email protected]) use-intl: 4.14.2([email protected])
transitivePeerDependencies: transitivePeerDependencies:
- '@swc/helpers' - '@swc/helpers'
[email protected].4(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]): [email protected].5(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]):
dependencies: dependencies:
'@next/env': 16.3.4 '@next/env': 16.3.5
'@swc/helpers': 0.5.23 '@swc/helpers': 0.5.23
baseline-browser-mapping: 2.11.21 baseline-browser-mapping: 2.11.21
caniuse-lite: 1.0.30001810 caniuse-lite: 1.0.30001810
@@ -5066,14 +5069,14 @@ snapshots:
react-dom: 19.2.8([email protected]) react-dom: 19.2.8([email protected])
styled-jsx: 5.1.6([email protected]) styled-jsx: 5.1.6([email protected])
optionalDependencies: optionalDependencies:
'@next/swc-darwin-arm64': 16.3.4 '@next/swc-darwin-arm64': 16.3.5
'@next/swc-darwin-x64': 16.3.4 '@next/swc-darwin-x64': 16.3.5
'@next/swc-linux-arm64-gnu': 16.3.4 '@next/swc-linux-arm64-gnu': 16.3.5
'@next/swc-linux-arm64-musl': 16.3.4 '@next/swc-linux-arm64-musl': 16.3.5
'@next/swc-linux-x64-gnu': 16.3.4 '@next/swc-linux-x64-gnu': 16.3.5
'@next/swc-linux-x64-musl': 16.3.4 '@next/swc-linux-x64-musl': 16.3.5
'@next/swc-win32-arm64-msvc': 16.3.4 '@next/swc-win32-arm64-msvc': 16.3.5
'@next/swc-win32-x64-msvc': 16.3.4 '@next/swc-win32-x64-msvc': 16.3.5
'@playwright/test': 1.62.1 '@playwright/test': 1.62.1
sharp: 0.35.4(@types/[email protected]) sharp: 0.35.4(@types/[email protected])
transitivePeerDependencies: transitivePeerDependencies:
@@ -5532,7 +5535,7 @@ snapshots:
[email protected]: {} [email protected]: {}
vite@8.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected]): vite@8.3.0(@types/[email protected])([email protected])([email protected])([email protected])([email protected]):
dependencies: dependencies:
lightningcss: 1.33.0 lightningcss: 1.33.0
picomatch: 4.0.7 picomatch: 4.0.7
@@ -5547,10 +5550,10 @@ snapshots:
tsx: 4.23.13 tsx: 4.23.13
yaml: 2.9.0 yaml: 2.9.0
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@noble/[email protected]))([email protected](@types/[email protected])([email protected]))(vite@8.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected])): [email protected](@types/[email protected])(@vitest/[email protected])([email protected](@noble/[email protected]))([email protected](@types/[email protected])([email protected]))(vite@8.3.0(@types/[email protected])([email protected])([email protected])([email protected])([email protected])):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/mocker': 5.0.0([email protected](@types/[email protected])([email protected]))(vite@8.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected])) '@vitest/mocker': 5.0.0([email protected](@types/[email protected])([email protected]))(vite@8.3.0(@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
chai: 6.2.2 chai: 6.2.2
es-module-lexer: 2.3.2 es-module-lexer: 2.3.2
expect-type: 1.4.0 expect-type: 1.4.0
@@ -5561,7 +5564,7 @@ snapshots:
tinybench: 6.1.4 tinybench: 6.1.4
tinyexec: 1.3.0 tinyexec: 1.3.0
tinyglobby: 0.2.17 tinyglobby: 0.2.17
vite: 8.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected]) vite: 8.3.0(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 26.4.1 '@types/node': 26.4.1
+1 -1
View File
@@ -5,7 +5,7 @@ allowBuilds:
"@swc/core": true "@swc/core": true
msw: false msw: false
minimumReleaseAge: 1440 minimumReleaseAge: 60
overrides: overrides:
# drizzle-kit still uses an obsolete development-server dependency. # drizzle-kit still uses an obsolete development-server dependency.
+175 -121
View File
@@ -2,26 +2,21 @@ import { count, desc, eq } from "drizzle-orm";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Image from "next/image"; import Image from "next/image";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { AuthShell } from "@/components/auth/auth-shell"; import { AuthTopBar } from "@/components/auth/auth-top-bar";
import { BrandFrank } from "@/components/auth/brand-frank";
import { LoginForm } from "@/components/auth/login-form"; import { LoginForm } from "@/components/auth/login-form";
import Link from "@/components/link"; import Link from "@/components/link";
import { Reveal } from "@/components/motion-reveal"; import { Reveal } from "@/components/motion-reveal";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail"; import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { SurfaceCard } from "@/components/surface-card";
import { cached } from "@/lib/cache"; import { cached } from "@/lib/cache";
import { db, User } from "@/lib/db"; import { db, User } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha"; import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const metadata = {
title: "Sign in",
};
export default async function LoginPage() { export default async function LoginPage() {
const t = await getTranslations("pages.login"); const t = await getTranslations("pages.login");
const th = await getTranslations("header"); const th = await getTranslations("header");
const thp = await getTranslations("pages.home");
const [hotelName, cfg, logo] = await Promise.all([ const [hotelName, cfg, logo] = await Promise.all([
resolveHotelName(), resolveHotelName(),
captchaConfig(), captchaConfig(),
@@ -29,7 +24,7 @@ export default async function LoginPage() {
]); ]);
const nonce = (await headers()).get("x-nonce") ?? undefined; const nonce = (await headers()).get("x-nonce") ?? undefined;
const [online, totalUsers, recentUsers, latestUsers] = await Promise.all([ const [online, recentUsers, latestUsers] = await Promise.all([
cached("online_count", 10_000, () => cached("online_count", 10_000, () =>
db db
.select({ total: count() }) .select({ total: count() })
@@ -37,141 +32,200 @@ export default async function LoginPage() {
.where(eq(User.online, "1")) .where(eq(User.online, "1"))
.then((rows) => rows[0]?.total ?? 0), .then((rows) => rows[0]?.total ?? 0),
).catch(() => 0), ).catch(() => 0),
cached("total_users", 300_000, () =>
db
.select({ total: count() })
.from(User)
.then((rows) => rows[0]?.total ?? 0),
).catch(() => 0),
db db
.select({ username: User.username, look: User.look }) .select({ username: User.username, look: User.look })
.from(User) .from(User)
.where(eq(User.online, "1")) .where(eq(User.online, "1"))
.limit(6) .limit(8)
.catch(() => []), .catch(() => []),
db db
.select({ username: User.username, look: User.look }) .select({ username: User.username, look: User.look })
.from(User) .from(User)
.orderBy(desc(User.accountCreated)) .orderBy(desc(User.accountCreated))
.limit(6) .limit(8)
.catch(() => []), .catch(() => []),
]); ]);
const onlineLabel = th("online", { count: online, hotel: "" }).trim();
return ( return (
<div className="mx-auto w-full max-w-6xl flex flex-col gap-6 pb-16"> <div className="mx-auto w-full max-w-6xl flex flex-col gap-8 pb-16">
<AuthTopBar hotelName={hotelName} logo={logo} />
<Reveal> <Reveal>
<AuthShell <div className="flex flex-col gap-8 lg:flex-row">
hotelName={hotelName} {/* Left panel */}
logo={logo} <div className="lg:w-80 shrink-0 space-y-4">
footer={ <SurfaceCard
<p className="m-0 text-sm landing-text-muted"> className="card-glow relative p-6 text-center"
style={{
borderColor:
"color-mix(in srgb, var(--color-primary) 20%, transparent)",
background: `url(/assets/images/background.png) center/cover no-repeat`,
}}
>
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-surface) 85%, transparent) 0%, color-mix(in srgb, var(--color-surface) 40%, transparent) 100%)",
}}
/>
<div className="relative">
<div className="brand-halo left-1/2 top-8 h-52 w-52 -translate-x-1/2" />
<Image
src="/assets/images/FrankwithBag.gif"
alt="Frank"
width={130}
height={170}
className="relative object-contain mx-auto drop-shadow-xl animate-float"
unoptimized
priority
/>
<div
className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full text-xs font-bold uppercase tracking-wider mt-4 mb-3 border"
style={{
borderColor:
"color-mix(in srgb, var(--color-primary) 18%, transparent)",
background:
"color-mix(in srgb, var(--color-primary) 10%, transparent)",
color:
"var(--color-primary-readable, var(--color-primary))",
}}
>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span>
{th("online", { count: online, hotel: "" }).trim()}
</div>
<h1
className="text-xl font-black"
style={{
color: "var(--color-text-readable)",
fontFamily: "var(--font-nunito)",
}}
>
{t("welcomeBack")}
</h1>
<p
className="text-xs mt-1 mx-auto max-w-[200px] leading-relaxed"
style={{ color: "var(--color-text-muted)" }}
>
{t("welcomeBackSub", { hotelName })}
</p>
</div>
</SurfaceCard>
{recentUsers.length > 0 && (
<SurfaceCard
title={th("online", { count: online, hotel: "" }).trim()}
icon="/assets/images/icons/navigation/goody.png"
bodyClassName="p-4"
className="card-glow"
>
<div className="grid grid-cols-4 gap-1.5">
{recentUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-0.5 p-1.5 rounded-lg"
style={{
background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
}}
>
<UserAvatarThumbnail
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-md"
/>
<span
className="text-[8px] font-bold truncate w-full text-center leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</SurfaceCard>
)}
{latestUsers.length > 0 && (
<SurfaceCard
title="Newest citizens"
icon="/assets/images/icons/friends.png"
bodyClassName="p-4"
className="card-glow"
>
<div className="grid grid-cols-4 gap-1.5">
{latestUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-0.5 p-1.5 rounded-lg"
style={{
background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
}}
>
<UserAvatarThumbnail
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-md"
/>
<span
className="text-[8px] font-bold truncate w-full text-center leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</SurfaceCard>
)}
<p
className="text-xs text-center"
style={{ color: "var(--color-text-muted)" }}
>
{t("noAccount") ?? "No account?"}{" "} {t("noAccount") ?? "No account?"}{" "}
<Link <Link
href="/register" href="/register"
className="font-extrabold landing-text-primary hover:underline" className="font-extrabold hover:underline"
style={{
color: "var(--color-primary-readable, var(--color-primary))",
}}
> >
{t("createOne") ?? "Create one"} {t("createOne") ?? "Create one"}
</Link> </Link>
</p> </p>
}
side={
<div className="flex flex-1 flex-col">
<div className="text-left">
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 landing-chip px-3.5 py-1.5 text-[11px] font-extrabold uppercase tracking-widest landing-text-secondary">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--color-primary)] opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[var(--color-primary)]" />
</span>
{onlineLabel}
</span>
<h1 className="mt-5 text-3xl font-black leading-[1.08] tracking-tight landing-text-primary sm:text-4xl lg:text-5xl">
{t("welcomeBack")}
<br />
<span className="text-gradient">{hotelName}</span>
</h1>
<p className="mt-4 max-w-md text-sm leading-relaxed landing-text-muted sm:text-base">
{t("welcomeBackSub", { hotelName })}
</p>
</div>
<div className="my-6 flex justify-center sm:my-8">
<BrandFrank size={160} />
</div>
<div className="mt-auto grid grid-cols-2 gap-3">
<div className="rounded-2xl border border-white/10 landing-chip p-4">
<div className="landing-text-primary text-2xl font-black">
{online}
</div>
<p className="mt-1 text-[11px] font-bold uppercase tracking-widest text-[var(--color-primary)]">
{onlineLabel}
</p>
{recentUsers.length > 0 && (
<div className="mt-3 flex items-center -space-x-2.5">
{recentUsers.slice(0, 5).map((u) => (
<UserAvatarThumbnail
key={u.username}
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-full border-2 border-[#0b1020]"
/>
))}
</div>
)}
</div>
<div className="flex flex-col justify-center rounded-2xl border border-white/10 landing-chip p-4">
<div className="flex items-center gap-2">
<span className="landing-text-primary text-2xl font-black">
{totalUsers}
</span>
<Image
src="/assets/images/icons/friends.png"
alt=""
width={18}
height={18}
unoptimized
className="opacity-80"
/>
</div>
<p className="mt-1 text-[11px] font-bold uppercase tracking-widest landing-text-faint">
{thp("statsCitizens")}
</p>
{latestUsers.length > 0 && (
<div className="mt-3 flex items-center -space-x-2.5">
{latestUsers.slice(0, 5).map((u) => (
<UserAvatarThumbnail
key={u.username}
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-full border-2 border-[#0b1020]"
/>
))}
</div>
)}
</div>
</div>
</div>
}
>
<div className="mb-6">
<h2 className="m-0 text-2xl font-black tracking-tight landing-text-primary">
{t("title")}
</h2>
<p className="mt-1.5 text-sm landing-text-muted">{t("subtitle")}</p>
</div> </div>
<LoginForm
captcha={{ {/* Right: form */}
provider: cfg.provider, <div className="flex-1">
siteKey: cfg.siteKey || undefined, <SurfaceCard
field: cfg.field || undefined, title={t("title")}
}} icon="/assets/images/icons/navigation/me.png"
nonce={nonce} bodyClassName="p-6"
/> >
</AuthShell> <p
className="text-sm mb-6"
style={{ color: "var(--color-text-muted)" }}
>
{t("subtitle")}
</p>
<LoginForm
captcha={{
provider: cfg.provider,
siteKey: cfg.siteKey || undefined,
field: cfg.field || undefined,
}}
nonce={nonce}
/>
</SurfaceCard>
</div>
</div>
</Reveal> </Reveal>
</div> </div>
); );
+243 -219
View File
@@ -4,27 +4,8 @@ import { headers } from "next/headers";
import Image from "next/image"; import Image from "next/image";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; 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 { 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 { publicReadFailure } from "@/lib/services/public-read";
import { siteSettings } from "@/lib/services/site-settings";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Home", title: "Home",
@@ -38,6 +19,28 @@ export const metadata: Metadata = {
}, },
}; };
import { AmbientOrbs } from "@/components/ambient-orbs";
import { AnimatedCounter } from "@/components/animated-counter";
import { HomeLoginForm } from "@/components/auth/home-login-form";
import { Clock } from "@/components/clock";
import { LanguageSwitcher } from "@/components/language-switcher";
import Link from "@/components/link";
import { LiveOnlineCount } from "@/components/live-online-count";
import { LiveOnlineCounter } from "@/components/live-online-counter";
import { Reveal } from "@/components/motion-reveal";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { SurfaceCard } from "@/components/surface-card";
import { ThemeSwitcher } from "@/components/theme-switcher";
import { TypewriterText } from "@/components/typewriter-text";
import { auth } from "@/lib/auth";
import { cached } from "@/lib/cache";
import { CameraWeb, db, Rooms, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha";
import { getNewsList } from "@/lib/services/news-list";
import { siteSettings } from "@/lib/services/site-settings";
async function getHotelData() { async function getHotelData() {
const [hotelName, logo] = await Promise.all([ const [hotelName, logo] = await Promise.all([
resolveHotelName(), resolveHotelName(),
@@ -135,211 +138,185 @@ export default async function Home() {
}, },
]; ];
const stats = [
{
value: users,
label: tp("statsCitizens"),
icon: "/assets/images/icons/friends.png",
live: false,
},
{
value: rooms,
label: tp("statsRooms"),
icon: "/assets/images/icons/catalog.png",
live: false,
},
{
value: online,
label: tp("statsOnline"),
icon: "/assets/images/icons/navigation/me.png",
live: true,
},
{
value: articles?.length ?? null,
label: tp("statsArticles"),
icon: "/assets/images/icons/article.gif",
live: false,
},
];
return ( 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"> <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 ── */} {/* ── Hero ── */}
<Reveal> <Reveal>
<section className="landing-hero w-full overflow-hidden rounded-2xl border border-white/10 sm:rounded-3xl"> <div
className="relative overflow-hidden rounded-2xl sm:rounded-3xl border"
style={{
borderColor:
"color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
boxShadow:
"0 4px 24px rgba(0,0,0,0.08), 0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 6%, transparent)",
}}
>
<Image
src="/assets/images/next_header.jpg"
alt=""
fill
priority
sizes="100vw"
style={{ objectFit: "cover", objectPosition: "center 20%" }}
/>
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-surface) 92%, transparent) 0%, color-mix(in srgb, var(--color-surface) 45%, transparent) 50%, color-mix(in srgb, var(--color-surface) 12%, transparent) 100%)",
}}
/>
<AmbientOrbs /> <AmbientOrbs />
<LandingTopBar hotelName={hotelName} logo={logo} /> <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"
<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"> style={{ zIndex: 2 }}
{/* Left: copy */} >
<div className="flex flex-col items-start"> <div
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 landing-chip px-3.5 py-1.5 text-[11px] font-extrabold uppercase tracking-widest landing-text-secondary"> className="brand-halo left-1/2 top-4 h-48 w-48 -translate-x-1/2 sm:top-6 sm:h-64 sm:w-64"
<span className="relative flex h-2 w-2"> style={{ zIndex: -1 }}
<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)]" /> <div
</span> 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"
{online === null ? ( style={{
<span>{te("unavailable")}</span> background:
) : ( "color-mix(in srgb, var(--color-primary) 12%, transparent)",
<LiveOnlineCount initial={online} hotelName={hotelName} /> borderColor:
)} "color-mix(in srgb, var(--color-primary) 20%, transparent)",
color: "var(--color-primary-readable, var(--color-primary))",
}}
>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span> </span>
{online === null ? (
<h1 className="mt-6 text-4xl font-black leading-[1.02] tracking-tight landing-text-primary sm:text-5xl lg:text-6xl"> <span>{te("unavailable")}</span>
{tp("welcome", { hotelName: "" }).trim()} ) : (
<TypewriterText text={hotelName} className="text-gradient" /> <LiveOnlineCount initial={online} hotelName={hotelName} />
</h1> )}
<p className="mt-5 max-w-xl text-sm leading-relaxed landing-text-muted sm:text-base md:text-lg">
{tp("welcomeBody")}
</p>
<div className="mt-8 flex flex-wrap gap-3">
<Link
href="/register"
className="btn-shine inline-flex items-center gap-2 rounded-xl font-extrabold text-sm sm:text-base px-6 sm:px-8 py-3.5 sm:py-4 transition-all duration-200 hover:scale-[1.03] active:scale-95"
style={{
background: "var(--color-primary)",
color: "var(--color-primary-foreground)",
boxShadow:
"0 8px 32px color-mix(in srgb, var(--color-primary) 40%, transparent)",
}}
>
<Image
src="/assets/images/EnterHubbly.png"
alt=""
width={20}
height={14}
className="object-contain"
unoptimized
/>
{tp("register")}
</Link>
<Link
href="/login"
className="inline-flex items-center gap-2 rounded-xl border font-bold text-sm sm:text-base px-6 sm:px-8 py-3.5 sm:py-4 transition-all duration-200 hover:scale-[1.03] active:scale-95"
style={{
borderColor: "color-mix(in srgb, #ffffff 22%, transparent)",
color: "#ffffff",
background: "color-mix(in srgb, #ffffff 6%, transparent)",
}}
>
{th("login")}
</Link>
</div>
</div> </div>
<h1
{/* Right: visual */} className="text-2xl font-black leading-[1.05] tracking-tight mb-2 sm:text-3xl md:text-5xl lg:text-6xl"
<div className="relative hidden items-center justify-center md:flex"> style={{
<span className="hero-halo h-80 w-80" /> color: "var(--color-text-readable)",
<div fontFamily: "var(--font-nunito)",
className="relative z-10 w-full max-w-[26rem] -rotate-2 rounded-3xl border border-white/15 landing-chip p-7 shadow-2xl backdrop-blur-xl transition-transform duration-500 hover:rotate-0" }}
>
<TypewriterText text={tp("welcome", { hotelName })} />
</h1>
<p
className="text-sm leading-relaxed max-w-2xl mb-5 sm:text-base md:text-lg md:mb-8"
style={{ color: "var(--color-text-muted)" }}
>
{tp("welcomeBody")}
</p>
<div className="flex flex-wrap gap-2.5 sm:gap-3 justify-center">
<Link
href="/register"
className="inline-flex items-center gap-2 sm:gap-2.5 font-extrabold text-xs sm:text-sm px-5 sm:px-8 py-3 sm:py-4 rounded-xl transition-all duration-200 hover:scale-[1.03] active:scale-95 shadow-lg"
style={{ style={{
background: "var(--color-primary)",
color: "var(--color-primary-foreground)",
boxShadow: boxShadow:
"0 40px 90px -30px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.1)", "0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
}} }}
> >
<div
className="absolute inset-x-0 top-0 h-0.5 rounded-t-3xl"
style={{
background:
"linear-gradient(90deg, var(--color-primary), var(--color-accent))",
}}
/>
<Image <Image
src="/assets/images/EnterHubbly.png" src="/assets/images/EnterHubbly.png"
alt={hotelName} alt=""
width={415} width={20}
height={128} height={20}
className="h-auto w-full object-contain drop-shadow-[0_8px_20px_rgba(0,0,0,0.4)]"
unoptimized unoptimized
/> />
<p className="mt-4 text-center text-sm leading-relaxed landing-text-faint"> {tp("register")}
{tp("welcomeSubtitle")} </Link>
</p> <Link
<div className="mt-5 space-y-2.5"> href="/login"
<div className="h-2 rounded-full landing-chip-strong"> 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"
<div style={{
className="h-full rounded-full" borderColor:
style={{ "color-mix(in srgb, var(--color-text-muted) 20%, transparent)",
width: "78%", color: "var(--color-text-readable)",
background: background:
"linear-gradient(90deg, var(--color-primary), var(--color-accent))", "color-mix(in srgb, var(--color-surface) 50%, transparent)",
}} }}
/>
</div>
<div className="h-2 rounded-full landing-chip-strong">
<div
className="h-full w-1/2 rounded-full"
style={{
background:
"linear-gradient(90deg, var(--color-primary), var(--color-accent))",
}}
/>
</div>
</div>
</div>
<div className="absolute -left-4 top-8 z-20 rounded-2xl border border-white/15 bg-[#0b1020]/80 p-3.5 pl-4 backdrop-blur-xl animate-float">
<p className="m-0 text-lg font-black leading-none landing-text-primary">
<LiveOnlineCounter initial={online ?? 0} />
</p>
<p className="mt-1 text-[10px] font-bold uppercase tracking-widest landing-text-faint">
{tp("statsOnline")}
</p>
</div>
<div
className="absolute -right-4 bottom-8 z-20 rounded-2xl border border-white/15 bg-[#0b1020]/80 p-3.5 pl-4 backdrop-blur-xl animate-float"
style={{ animationDelay: "1.2s" }}
> >
<p className="m-0 text-lg font-black leading-none landing-text-primary"> {th("login")}
<AnimatedCounter value={users ?? 0} /> </Link>
</p>
<p className="mt-1 text-[10px] font-bold uppercase tracking-widest landing-text-faint">
{tp("statsCitizens")}
</p>
</div>
</div> </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 landing-chip p-4 transition-all duration-300 hover:-translate-y-0.5 hover:border-[color-mix(in_srgb,var(--color-primary)_40%,transparent)]"
>
<div className="flex items-center justify-between gap-2">
<span className="text-2xl font-black tracking-tight landing-text-primary sm:text-3xl">
{s.value === null ? (
<span className="text-sm font-bold landing-text-faint">
{te("unavailable")}
</span>
) : s.live ? (
<LiveOnlineCounter initial={s.value} />
) : (
<AnimatedCounter value={s.value} />
)}
</span>
<Image
src={s.icon}
alt=""
width={18}
height={18}
className="opacity-60"
unoptimized
/>
</div>
<p className="mt-1 text-[10px] font-bold uppercase tracking-widest landing-text-faint sm:text-xs">
{s.label}
</p>
</div>
))}
</div>
</div>
</section>
</Reveal> </Reveal>
{/* ── Features ── */} {/* ── Features ── */}
@@ -348,28 +325,21 @@ export default async function Home() {
{features.map((f) => ( {features.map((f) => (
<SurfaceCard <SurfaceCard
key={f.title} key={f.title}
className="group relative overflow-hidden p-5 sm:p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg" className="card-glow relative p-4 sm:p-5 text-center hover:-translate-y-1"
> >
<div <div
className="absolute inset-x-0 top-0 h-1 opacity-0 transition-opacity duration-300 group-hover:opacity-100" className="mx-auto mb-3 flex h-11 w-11 sm:h-12 sm:w-12 items-center justify-center rounded-xl"
style={{ style={{
background: background:
"linear-gradient(90deg, var(--color-primary), var(--color-accent))", "color-mix(in srgb, var(--color-primary) 14%, transparent)",
}}
/>
<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: boxShadow:
"inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 14%, transparent)", "inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent)",
}} }}
> >
<Image src={f.icon} alt="" width={24} height={24} unoptimized /> <Image src={f.icon} alt="" width={24} height={24} unoptimized />
</div> </div>
<h3 <h3
className="text-center text-base font-extrabold" className="text-sm sm:text-base font-extrabold mb-1"
style={{ style={{
color: "var(--color-text-readable)", color: "var(--color-text-readable)",
fontFamily: "var(--font-nunito)", fontFamily: "var(--font-nunito)",
@@ -378,7 +348,7 @@ export default async function Home() {
{f.title} {f.title}
</h3> </h3>
<p <p
className="mt-1 text-center text-sm leading-relaxed" className="text-xs sm:text-sm leading-relaxed"
style={{ color: "var(--color-text-muted)" }} style={{ color: "var(--color-text-muted)" }}
> >
{f.desc} {f.desc}
@@ -388,6 +358,60 @@ export default async function Home() {
</div> </div>
</Reveal> </Reveal>
{/* ── Stats + Login ── */}
<Reveal>
<div className="grid grid-cols-2 gap-3 sm:gap-4 md:grid-cols-4">
{[
{
value: users,
label: tp("statsCitizens"),
icon: "/assets/images/icons/friends.png",
live: false,
},
{
value: rooms,
label: tp("statsRooms"),
icon: "/assets/images/icons/catalog.png",
live: false,
},
{
value: online,
label: tp("statsOnline"),
icon: "/assets/images/icons/navigation/me.png",
live: true,
},
{
value: articles?.length ?? null,
label: tp("statsArticles"),
icon: "/assets/images/icons/article.gif",
live: false,
},
].map((s) => (
<SurfaceCard
key={s.label}
className="card-glow relative p-3 sm:p-5 text-center hover:-translate-y-1"
>
<div className="mb-0.5 text-2xl sm:text-3xl font-black tracking-tight md:text-4xl">
{s.value === null ? (
<span className="text-base">{te("unavailable")}</span>
) : s.live ? (
<LiveOnlineCounter initial={s.value} />
) : (
<AnimatedCounter value={s.value} />
)}
</div>
<div
className="text-[10px] sm:text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-1 sm:gap-1.5"
style={{ color: "var(--color-text-muted)" }}
>
<Image src={s.icon} alt="" width={14} height={14} unoptimized />
{s.label}
</div>
</SurfaceCard>
))}
</div>
</Reveal>
{/* ── Login + Online users ── */} {/* ── Login + Online users ── */}
<div className="flex flex-col gap-4 md:flex-row"> <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"> <Reveal className="flex w-full flex-col gap-4 md:w-1/3 md:flex-shrink-0">
+191 -132
View File
@@ -1,24 +1,21 @@
import { count, desc, eq } from "drizzle-orm"; import { count, desc, eq } from "drizzle-orm";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Image from "next/image";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { AuthShell } from "@/components/auth/auth-shell"; import { AuthTopBar } from "@/components/auth/auth-top-bar";
import { BrandFrank } from "@/components/auth/brand-frank";
import { RegisterForm } from "@/components/auth/register-form"; import { RegisterForm } from "@/components/auth/register-form";
import Link from "@/components/link";
import { Reveal } from "@/components/motion-reveal"; import { Reveal } from "@/components/motion-reveal";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail"; import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { SurfaceCard } from "@/components/surface-card";
import { cached } from "@/lib/cache"; import { cached } from "@/lib/cache";
import { db, User } from "@/lib/db"; import { db, User } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha"; import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
export const metadata = {
title: "Create account",
};
export default async function RegisterPage() { export default async function RegisterPage() {
const tpr = await getTranslations("pages.register"); const tpr = await getTranslations("pages.register");
const th = await getTranslations("pages.home");
const [hotelName, cfg, logo] = await Promise.all([ const [hotelName, cfg, logo] = await Promise.all([
resolveHotelName(), resolveHotelName(),
captchaConfig(), captchaConfig(),
@@ -26,7 +23,7 @@ export default async function RegisterPage() {
]); ]);
const nonce = (await headers()).get("x-nonce") ?? undefined; const nonce = (await headers()).get("x-nonce") ?? undefined;
const [online, latestUsers, recentUsers] = await Promise.all([ const [online, recentUsers, latestUsers] = await Promise.all([
cached("online_count", 10_000, () => cached("online_count", 10_000, () =>
db db
.select({ total: count() }) .select({ total: count() })
@@ -34,148 +31,210 @@ export default async function RegisterPage() {
.where(eq(User.online, "1")) .where(eq(User.online, "1"))
.then((rows) => rows[0]?.total ?? 0), .then((rows) => rows[0]?.total ?? 0),
).catch(() => 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, () => cached("register_latest_users", 30_000, () =>
db db
.select({ username: User.username, look: User.look }) .select({ username: User.username, look: User.look })
.from(User) .from(User)
.orderBy(desc(User.accountCreated)) .orderBy(desc(User.accountCreated))
.limit(6), .limit(8),
).catch(() => []), ).catch(() => []),
db
.select({ username: User.username, look: User.look })
.from(User)
.where(eq(User.online, "1"))
.limit(6)
.catch(() => []),
]); ]);
const benefits = [
{ title: th("featuresAvatar"), desc: th("featuresAvatarDesc") },
{ title: th("featuresFurniture"), desc: th("featuresFurnitureDesc") },
{ title: th("featuresCommunity"), desc: th("featuresCommunityDesc") },
];
return ( return (
<div className="mx-auto w-full max-w-6xl flex flex-col gap-6 pb-16"> <div className="mx-auto w-full max-w-6xl flex flex-col gap-8 pb-16">
<Reveal> <AuthTopBar hotelName={hotelName} logo={logo} />
<AuthShell
hotelName={hotelName}
logo={logo}
side={
<div className="flex flex-1 flex-col">
<div className="text-left">
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 landing-chip px-3.5 py-1.5 text-[11px] font-extrabold uppercase tracking-widest landing-text-secondary">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--color-primary)] opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[var(--color-primary)]" />
</span>
{tpr("usersOnline", { count: online })}
</span>
<h1 className="mt-5 text-3xl font-black leading-[1.08] tracking-tight landing-text-primary sm:text-4xl lg:text-5xl">
{tpr("title")}
<br />
<span className="text-gradient">{hotelName}</span>
</h1>
<p className="mt-4 max-w-md text-sm leading-relaxed landing-text-muted sm:text-base">
{tpr("subtitle")}
</p>
{recentUsers.length > 0 && (
<div className="mt-6 flex items-center gap-3">
<div className="flex -space-x-2.5">
{recentUsers.slice(0, 5).map((u) => (
<UserAvatarThumbnail
key={u.username}
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-full border-2 border-[#0b1020]"
/>
))}
</div>
<p className="m-0 text-[11px] font-extrabold uppercase tracking-widest landing-text-faint">
{tpr("whoIsOnline")}
</p>
</div>
)}
</div>
<div className="mt-6 hidden grid-cols-1 gap-2.5 sm:grid"> <Reveal>
{benefits.map((b) => ( <div className="flex flex-col gap-8 lg:flex-row lg:items-start">
<div {/* Left: intro panel */}
key={b.title} <div className="lg:w-96 shrink-0 space-y-4">
className="flex items-center gap-3 rounded-xl border border-white/10 landing-chip px-4 py-3" <SurfaceCard
> className="card-glow relative overflow-hidden p-6"
<span style={{
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full" borderColor:
"color-mix(in srgb, var(--color-primary) 20%, transparent)",
}}
>
<div
className="absolute inset-0"
style={{
background: `url(/assets/images/background.png) center/cover no-repeat`,
}}
/>
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-surface) 88%, transparent) 0%, color-mix(in srgb, var(--color-surface) 35%, transparent) 100%)",
}}
/>
<div className="relative">
<div className="flex items-center gap-4">
<div className="shrink-0 animate-float">
<div className="brand-halo left-1/2 top-1/2 h-52 w-52 -translate-x-1/2 -translate-y-1/2" />
<Image
src="/assets/images/FrankwithBag.gif"
alt="Frank"
width={110}
height={145}
className="relative object-contain drop-shadow-xl"
unoptimized
priority
/>
</div>
<div className="flex-1">
<div
className="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-2 border"
style={{ style={{
color: "var(--color-primary)", borderColor:
"color-mix(in srgb, var(--color-primary) 18%, transparent)",
background: background:
"color-mix(in srgb, var(--color-primary) 15%, transparent)", "color-mix(in srgb, var(--color-primary) 10%, transparent)",
color:
"var(--color-primary-readable, var(--color-primary))",
}} }}
> >
<svg <span className="w-2 h-2 rounded-full bg-primary" />
width="14" {tpr("usersOnline", { count: online })}
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<title>{b.title}</title>
<polyline points="20 6 9 17 4 12" />
</svg>
</span>
<div className="min-w-0">
<p className="m-0 text-sm font-extrabold landing-text-primary">
{b.title}
</p>
<p className="m-0 truncate text-xs landing-text-muted">
{b.desc}
</p>
</div> </div>
</div> <h1
))} className="text-lg font-black leading-tight"
</div> style={{
color: "var(--color-text-readable)",
<div className="mt-auto pt-6 sm:pt-8"> fontFamily: "var(--font-nunito)",
<div className="flex items-end justify-between gap-4"> }}
<BrandFrank size={130} /> >
<div className="mb-1 text-right"> {tpr("title")} — {hotelName}
{latestUsers.length > 0 && ( </h1>
<> <p
<p className="m-0 text-[11px] font-extrabold uppercase tracking-widest landing-text-faint"> className="text-xs mt-1.5 leading-relaxed"
Latest citizens style={{ color: "var(--color-text-muted)" }}
</p> >
<div className="mt-2 flex items-center justify-end -space-x-2.5"> {tpr("subtitle")}
{latestUsers.slice(0, 5).map((u) => ( </p>
<UserAvatarThumbnail
key={u.username}
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-full border-2 border-[#0b1020]"
/>
))}
</div>
</>
)}
</div> </div>
</div> </div>
</div> </div>
</div> </SurfaceCard>
}
> {recentUsers.length > 0 && (
<RegisterForm <SurfaceCard
hotelName={hotelName} title={tpr("whoIsOnline")}
captcha={{ icon="/assets/images/icons/navigation/goody.png"
provider: cfg.provider, bodyClassName="p-4"
siteKey: cfg.siteKey || undefined, className="card-glow"
}} >
nonce={nonce} <div className="grid grid-cols-4 gap-1.5">
/> {recentUsers.map((u) => (
</AuthShell> <div
key={u.username}
className="flex flex-col items-center gap-0.5 p-1.5 rounded-lg"
style={{
background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
}}
>
<UserAvatarThumbnail
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-md"
/>
<span
className="text-[8px] font-bold truncate w-full text-center leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</SurfaceCard>
)}
{latestUsers.length > 0 && (
<SurfaceCard
title="Newest citizens"
icon="/assets/images/icons/friends.png"
bodyClassName="p-4"
className="card-glow"
>
<div className="grid grid-cols-4 gap-1.5">
{latestUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-0.5 p-1.5 rounded-lg"
style={{
background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
}}
>
<UserAvatarThumbnail
figure={u.look}
alt=""
options={{ direction: 2 }}
className="rounded-md"
/>
<span
className="text-[10px] font-bold truncate w-full text-center leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</SurfaceCard>
)}
<p
className="text-sm text-center"
style={{ color: "var(--color-text-muted)" }}
>
{tpr("alreadyHaveAccountPre")}{" "}
<Link
href="/login"
className="font-extrabold hover:underline"
style={{
color: "var(--color-primary-readable, var(--color-primary))",
}}
>
{tpr("alreadyHaveAccountLink")}
</Link>
</p>
</div>
{/* Right: form */}
<div className="flex-1 lg:sticky lg:top-6">
<SurfaceCard
title={tpr("title")}
icon="/assets/images/icons/navigation/me.png"
bodyClassName="p-6"
>
<p
className="text-sm mb-6"
style={{ color: "var(--color-text-muted)" }}
>
{tpr("subtitle")}
</p>
<RegisterForm
hotelName={hotelName}
captcha={{
provider: cfg.provider,
siteKey: cfg.siteKey || undefined,
}}
nonce={nonce}
/>
</SurfaceCard>
</div>
</div>
</Reveal> </Reveal>
</div> </div>
); );
+16 -123
View File
@@ -1993,135 +1993,28 @@ details[open] > summary .details-open\:rotate-180 {
animation-delay: 0.4s; animation-delay: 0.4s;
} }
/* ── Premium landing kit (dark gaming style) ─────────────────── /* ── Classic-landing polish (theme-aware) ────────────────────────────
Shared primitives for the redesigned home / login / register Keeps the Habbo sky and theme colors; adds subtle depth + glow to the
pages. Everything stays theme-aware: brand colors are driven by restored landing pages. Everything is driven by theme CSS variables. */
--color-primary / --color-accent, and the "always-dark" hero .brand-halo {
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; position: absolute;
inset: 0;
pointer-events: none; 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; border-radius: 999px;
background: radial-gradient( background: radial-gradient(
circle, circle,
color-mix(in srgb, var(--color-primary) 55%, transparent) 0%, color-mix(in srgb, var(--color-primary) 30%, transparent) 0%,
transparent 68% transparent 70%
); );
filter: blur(6px); filter: blur(20px);
animation: hero-halo-pulse 5s ease-in-out infinite;
} }
@keyframes hero-halo-pulse { .card-glow {
0%, transition:
100% { box-shadow 0.3s ease,
opacity: 0.55; transform 0.3s ease,
transform: scale(1); border-color 0.3s ease;
}
50% {
opacity: 0.9;
transform: scale(1.07);
}
} }
.card-glow:hover {
@media (prefers-reduced-motion: reduce) { box-shadow:
.hero-halo { 0 12px 32px -10px color-mix(in srgb, var(--color-primary) 32%, transparent),
animation: none; 0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent);
}
}
/* White-on-dark overlay text/backgrounds for the landing canvas. Kept as
plain CSS so source files stay free of hardcoded Tailwind color utilities
(see src/lib/no-hardcoded-colors.test.ts). */
.landing-text {
color: #f8fafc;
}
.landing-text-primary {
color: #ffffff;
}
.landing-text-secondary {
color: rgb(255 255 255 / 0.86);
}
.landing-text-muted {
color: rgb(255 255 255 / 0.62);
}
.landing-text-faint {
color: rgb(255 255 255 / 0.5);
}
.landing-chip {
background: rgb(255 255 255 / 0.05);
}
.landing-chip-strong {
background: rgb(255 255 255 / 0.1);
}
.landing-divider {
background: rgb(255 255 255 / 0.15);
} }
-47
View File
@@ -1,47 +0,0 @@
import type { ReactNode } from "react";
import { AmbientOrbs } from "@/components/ambient-orbs";
import { LandingTopBar } from "@/components/landing-top-bar";
/**
* Premium dark-gaming shell shared by the login and register pages.
* Renders an always-dark hero canvas (brand glows + grid overlay) with a
* hotel header on top, a brand/side column on the left and the auth form
* (rendered by the page) on the right in a frosted glass panel.
*/
export function AuthShell({
hotelName,
logo,
side,
footer,
children,
}: {
hotelName: string;
logo?: string | null;
side: ReactNode;
footer?: ReactNode;
children: ReactNode;
}) {
return (
<section className="landing-hero w-full overflow-hidden rounded-2xl border border-white/10 sm:rounded-3xl">
{/* Ambient glows */}
<AmbientOrbs />
{/* Header */}
<LandingTopBar hotelName={hotelName} logo={logo} />
{/* Body */}
<div className="relative z-10 grid gap-8 px-4 pb-8 pt-6 sm:px-7 sm:pb-10 sm:pt-8 lg:grid-cols-[1fr_1.05fr] lg:gap-12">
<div className="flex flex-col">{side}</div>
<div className="glass-panel relative h-fit w-full rounded-2xl p-5 sm:p-7 lg:sticky lg:top-6">
{children}
</div>
</div>
{footer ? (
<div className="relative z-10 border-t border-white/10 px-4 py-4 text-center sm:px-7">
{footer}
</div>
) : null}
</section>
);
}
-32
View File
@@ -1,32 +0,0 @@
import Image from "next/image";
/**
* Floating brand art for the premium auth pages: the welcome character
* hovers over a pulsing primary halo with a faint slow-orbiting ring.
*/
export function BrandFrank({ size = 150 }: { size?: number }) {
return (
<div className="relative flex items-center justify-center">
<style>{`@keyframes spin-slow { to { transform: rotate(360deg); } }`}</style>
<span className="hero-halo h-40 w-40 sm:h-48 sm:w-48" />
<span
className="absolute h-52 w-52 rounded-full border border-dashed sm:h-64 sm:w-64"
style={{
borderColor: "color-mix(in srgb, #ffffff 16%, transparent)",
animation: "spin-slow 28s linear infinite",
}}
/>
<div className="animate-float relative z-10 drop-shadow-[0_20px_30px_rgba(0,0,0,0.45)]">
<Image
src="/assets/images/FrankwithBag.gif"
alt=""
width={size}
height={Math.round(size * 1.29)}
className="object-contain"
unoptimized
priority
/>
</div>
</div>
);
}
+53 -25
View File
@@ -113,30 +113,52 @@ export function RegisterForm({
/> />
) : null} ) : null}
{/* Header Banner */} {/* Header Banner */}
<div className="mb-7 flex items-center gap-4"> <div
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-white/10 landing-chip"> className="relative overflow-hidden bg-center bg-cover rounded-t-xl"
<Image style={{
src="/assets/images/icons/navigation/me.png" backgroundImage: "url(/assets/images/mybobba_banner.png)",
alt="" height: "160px",
width={22} }}
height={22} >
unoptimized <div
className="opacity-90" className="absolute inset-0 z-10"
/> style={{
</div> background:
<div className="min-w-0"> "linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.55) 100%)",
<p }}
className="text-[11px] font-extrabold uppercase tracking-widest" />
style={{ color: "var(--color-primary)" }} <div className="relative flex items-center justify-center w-full h-full z-20">
<div
className="relative animate-float"
style={{ width: "140px", height: "140px" }}
> >
{t("accountDetails")} <div
</p> className="absolute inset-0 rounded-full overflow-hidden"
<h2 className="m-0 text-2xl font-black tracking-tight landing-text-primary"> style={{ zIndex: -1 }}
{t("title")} >
</h2> <div
<p className="mt-0.5 truncate text-sm landing-text-muted"> className="w-full h-full"
{t("subtitle")} style={{
</p> background: "black",
filter: "blur(8px)",
transform: "scale(1.2)",
opacity: 0.6,
}}
/>
</div>
<Image
src="/assets/images/FrankwithBag.gif"
alt=""
fill
className="!static"
style={{
objectFit: "contain",
objectPosition: "center",
width: "100%",
height: "100%",
}}
/>
</div>
</div> </div>
</div> </div>
@@ -149,7 +171,12 @@ export function RegisterForm({
}; };
formAction(fd); formAction(fd);
}} }}
className="flex flex-col gap-6" className="p-6 flex flex-col gap-6"
style={{
backgroundColor:
"color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
borderRadius: "0 0 12px 12px",
}}
> >
{(error || state.error) && ( {(error || state.error) && (
<div <div
@@ -356,7 +383,8 @@ export function RegisterForm({
termsError ? "animate-shake" : "" termsError ? "animate-shake" : ""
}`} }`}
style={{ style={{
backgroundColor: "var(--color-background)", backgroundColor:
"color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
border: `1px solid ${ border: `1px solid ${
termsError termsError
? "var(--color-danger)" ? "var(--color-danger)"
-53
View File
@@ -1,53 +0,0 @@
import Image from "next/image";
import { Clock } from "@/components/clock";
import { LanguageSwitcher } from "@/components/language-switcher";
import { ThemeSwitcher } from "@/components/theme-switcher";
/**
* Frosted top bar shared by the home hero and the premium auth pages.
* Dark-styled to sit on the "always-dark" landing canvas.
*/
export function LandingTopBar({
hotelName,
logo,
}: {
hotelName: string;
logo?: string | null;
}) {
return (
<div className="relative z-10 flex items-center justify-between gap-3 border-b border-white/10 px-4 py-3 sm:px-7 sm:py-4">
<div className="flex min-w-0 items-center gap-2.5 sm:gap-3">
{logo ? (
<Image
src={logo}
alt={hotelName}
width={110}
height={30}
className="shrink-0 object-contain"
unoptimized
style={{ maxHeight: 30, width: "auto", height: "auto" }}
/>
) : (
<Image
src="/assets/images/icons/lighthouse.png"
alt={hotelName}
width={30}
height={30}
className="shrink-0"
unoptimized
/>
)}
<span className="hidden truncate text-sm font-extrabold uppercase tracking-widest landing-text-secondary sm:inline">
{hotelName}
</span>
</div>
<div className="flex shrink-0 items-center gap-1 rounded-xl border border-white/10 landing-chip px-1.5 py-1 sm:px-2">
<LanguageSwitcher />
<div className="hidden h-5 w-px landing-divider sm:block" />
<Clock />
<div className="h-5 w-px landing-divider" />
<ThemeSwitcher />
</div>
</div>
);
}