revert(site): restore classic Habbo landing with soft theme polish
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:
1 parent
cc58f16230
commit
d755ab7ce1
11 files changed
+742
-812
No files matched your search
+2
-1
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+61
-58
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
|
||||||
}
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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)"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user