From a6e69fe00e4bbd198a358f04b13c11b3bc9d4233 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 29 Aug 2026 21:26:30 +0200 Subject: [PATCH 1/3] perf: declutter home page by removing duplicated sections and queries The landing page showed the same information more than once. Drop the second avatar grid (latest users) and its DB query, move the online users grid into the left column, remove the register banner card and the bottom join CTA so registration is only offered once in the hero, and show the online count a single time in the hero badge instead of also in the stats row. The online users query now fetches 12 rows instead of 30, saving bandwidth on every uncached render. The avatar presentation contract test now expects one thumbnail call site on the home page. --- src/app/(site)/page.tsx | 226 ++++-------------- .../shared/public-avatar-contract.test.ts | 2 +- 2 files changed, 53 insertions(+), 175 deletions(-) diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx index 479bcbac..f69f9cb9 100644 --- a/src/app/(site)/page.tsx +++ b/src/app/(site)/page.tsx @@ -29,57 +29,43 @@ async function getHotelData() { siteSettings.get("cms_logo", ""), ]); - const [ - online, - users, - rooms, - articles, - recentUsers, - recentPhotos, - latestUsers, - ] = await Promise.all([ - cached("online_count", 10_000, () => - db - .select({ total: count() }) - .from(User) - .where(eq(User.online, "1")) - .then((rows) => rows[0]?.total ?? 0), - ).catch(() => 0), - cached("total_users", 300_000, () => - db - .select({ total: count() }) - .from(User) - .then((rows) => rows[0]?.total ?? 0), - ).catch(() => 0), - cached("total_rooms", 300_000, () => - db - .select({ total: count() }) - .from(Rooms) - .then((rows) => rows[0]?.total ?? 0), - ).catch(() => 0), - getNewsList(4), - cached("home_online_users", 15_000, () => - db - .select({ username: User.username, look: User.look }) - .from(User) - .where(eq(User.online, "1")) - .limit(30), - ).catch(() => []), - cached("home_recent_photos", 60_000, () => - db - .select({ id: CameraWeb.id, url: CameraWeb.url }) - .from(CameraWeb) - .orderBy(desc(CameraWeb.timestamp)) - .limit(4), - ).catch(() => []), - cached("home_latest_users", 60_000, () => - db - .select({ username: User.username, look: User.look }) - .from(User) - .orderBy(desc(User.accountCreated)) - .limit(8), - ).catch(() => []), - ]); + const [online, users, rooms, articles, recentUsers, recentPhotos] = + await Promise.all([ + cached("online_count", 10_000, () => + db + .select({ total: count() }) + .from(User) + .where(eq(User.online, "1")) + .then((rows) => rows[0]?.total ?? 0), + ).catch(() => 0), + cached("total_users", 300_000, () => + db + .select({ total: count() }) + .from(User) + .then((rows) => rows[0]?.total ?? 0), + ).catch(() => 0), + cached("total_rooms", 300_000, () => + db + .select({ total: count() }) + .from(Rooms) + .then((rows) => rows[0]?.total ?? 0), + ).catch(() => 0), + getNewsList(4), + cached("home_online_users", 15_000, () => + db + .select({ username: User.username, look: User.look }) + .from(User) + .where(eq(User.online, "1")) + .limit(12), + ).catch(() => []), + cached("home_recent_photos", 60_000, () => + db + .select({ id: CameraWeb.id, url: CameraWeb.url }) + .from(CameraWeb) + .orderBy(desc(CameraWeb.timestamp)) + .limit(4), + ).catch(() => []), + ]); return { hotelName, @@ -89,7 +75,6 @@ async function getHotelData() { articles, recentUsers, recentPhotos, - latestUsers, logo, }; } @@ -108,7 +93,6 @@ export default async function Home() { articles, recentUsers, recentPhotos, - latestUsers, logo, } = await getHotelData(); @@ -303,13 +287,8 @@ export default async function Home() { {/* ── Stats ── */} -
+
{[ - { - value: online, - label: tp("statsOnline"), - icon: "/assets/images/icons/online-friends.png", - }, { value: users, label: tp("statsCitizens"), @@ -365,7 +344,7 @@ export default async function Home() { {/* ── Login + News ── */}
- {/* Left: Login & Register */} + {/* Left: Login & Online users */} - - - Register - - - {tp("register")} - - - - {latestUsers.length > 0 && ( + {recentUsers.length > 0 && (
- {latestUsers.map((u) => ( + {recentUsers.map((u) => (
{u.username} @@ -501,91 +464,6 @@ export default async function Home() {
- {/* ── Online users ── */} - {recentUsers.length > 0 && ( - - -
- {recentUsers.slice(0, 12).map((u) => ( -
- - - {u.username} - -
- ))} -
-
-
- )} - - {/* ── Bottom CTA ── */} - -
-

- {tp("joinNow", { hotelName })} -

-

- {tp("welcomeBody")} -

- - - {tp("register")} - -
-
- {/* ── Recent photos ── */} {recentPhotos.length > 0 && ( diff --git a/src/components/shared/public-avatar-contract.test.ts b/src/components/shared/public-avatar-contract.test.ts index de5681e8..c3367bae 100644 --- a/src/components/shared/public-avatar-contract.test.ts +++ b/src/components/shared/public-avatar-contract.test.ts @@ -10,7 +10,7 @@ const compactCallSites = new Map([ ["src/app/(site)/login/page.tsx", 2], ["src/app/(site)/me/page.tsx", 1], ["src/app/(site)/messages/page.tsx", 2], - ["src/app/(site)/page.tsx", 2], + ["src/app/(site)/page.tsx", 1], ["src/app/(site)/radio/leaderboard/page.tsx", 1], ["src/app/(site)/radio/shouts/page.tsx", 1], ["src/app/(site)/rankings/page.tsx", 1], From 3baac5e885be4067751b099eaf475862d1b79c2d Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 29 Aug 2026 21:30:44 +0200 Subject: [PATCH 2/3] chore: update nextjs and react to latest versionb to fix cve eploits --- package.json | 2 +- pnpm-lock.yaml | 94 +++++++++++++++++++++++++------------------------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index eb68cf61..4ccf709f 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "motion": "13.1.1", "music-metadata": "11.15.0", "mysql2": "3.23.4", - "next": "16.3.2", + "next": "16.3.3", "next-auth": "5.0.0-beta.32", "next-intl": "4.13.7", "otplib": "13.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f69623d9..c628e4a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,14 +92,14 @@ importers: specifier: 3.23.4 version: 3.23.4(@types/node@26.2.0) next: - specifier: 16.3.2 - version: 16.3.2(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 16.3.3 + version: 16.3.3(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-auth: specifier: 5.0.0-beta.32 - version: 5.0.0-beta.32(next@16.3.2(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + version: 5.0.0-beta.32(next@16.3.3(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) next-intl: specifier: 4.13.7 - version: 4.13.7(@swc/helpers@0.5.23)(next@16.3.2(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2) + version: 4.13.7(@swc/helpers@0.5.23)(next@16.3.3(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2) otplib: specifier: 13.4.1 version: 13.4.1 @@ -815,57 +815,57 @@ packages: '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 - '@next/env@16.3.2': - resolution: {integrity: sha512-8k4YoG8cM7LWlkfzGNYCRBbFNlernLiMw4s0btVl+CmmWqn3VpYypA72/5Feb1UWdxe6tHqr5KHP4p4Y4m9luA==} + '@next/env@16.3.3': + resolution: {integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==} - '@next/swc-darwin-arm64@16.3.2': - resolution: {integrity: sha512-ib5Llm93YCKoKWDh6ZaHq6QWTuOZ2bRkSnUwMmX8dsRIOkBNL1vVlSiUKSfixPL9SSh9pvukzqajk/klkn5vqg==} + '@next/swc-darwin-arm64@16.3.3': + resolution: {integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.3.2': - resolution: {integrity: sha512-qd98fX2+I5nYJDioW2o7nSjoxM5KvWdeDefM80igia4+C/qSIEhH4MhTE+hO/7qKM7W37/Mq+dOWp8UePSyLHw==} + '@next/swc-darwin-x64@16.3.3': + resolution: {integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.3.2': - resolution: {integrity: sha512-vqsgb6FAOzcrCccsLXiKtAy5t8EzO+uOazuFaSkQxeY0tNONG3vpHYy8pyBafcI5SNFPTeyard6yTr6SzNGo2A==} + '@next/swc-linux-arm64-gnu@16.3.3': + resolution: {integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.3.2': - resolution: {integrity: sha512-xIe1eujfHUB2XcxHGddxJyu6TJRPjC5NpIkQYB/32ESkt5VkQyIAjmLRS38c+s6QY+qjtY/4KarVDzXRuD7lZQ==} + '@next/swc-linux-arm64-musl@16.3.3': + resolution: {integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.3.2': - resolution: {integrity: sha512-Fe0SA2j8X0kmc3aveuHD7UktO3AE2+mH3LguP60vGbz7u0z+MrDXbeb5iZFYAwR7EzzzXJ2Yk966w9mGTFMqfA==} + '@next/swc-linux-x64-gnu@16.3.3': + resolution: {integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.3.2': - resolution: {integrity: sha512-TFBipb+gyesI/2Ve4zVu7kGltBWN/R466G5/1gtt2lECfc22G1pjkTxu68Q9aFcOaXiRGTQfvDbQQFe7mYgxiQ==} + '@next/swc-linux-x64-musl@16.3.3': + resolution: {integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.3.2': - resolution: {integrity: sha512-rVtmnNpBYIosDnKD/96dKxFsJnwnn1WRGG/HioSe8XCm2ksSHNrd2R6+hSjvTBxeMNhJ9pYeu/90cWB1nQLuNA==} + '@next/swc-win32-arm64-msvc@16.3.3': + resolution: {integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.3.2': - resolution: {integrity: sha512-H4Y2o2/JcHu8LtwzD5CXfHhwxwz8gfsx2HXDEw46Mtev5xHnEmB7HNtZtmriw5ReUOjRtcDqo7XSbU01FT9NlA==} + '@next/swc-win32-x64-msvc@16.3.3': + resolution: {integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2544,8 +2544,8 @@ packages: typescript: optional: true - next@16.3.2: - resolution: {integrity: sha512-/ZCaubUy17Lld1SiPWxuPbCk2ihqAxF2QNQaPZeEaEb7t1I58qhsJN187D7AfpapHAqUPXH0f/thtdW9dWgWFg==} + next@16.3.3: + resolution: {integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -3444,30 +3444,30 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.3.2': {} + '@next/env@16.3.3': {} - '@next/swc-darwin-arm64@16.3.2': + '@next/swc-darwin-arm64@16.3.3': optional: true - '@next/swc-darwin-x64@16.3.2': + '@next/swc-darwin-x64@16.3.3': optional: true - '@next/swc-linux-arm64-gnu@16.3.2': + '@next/swc-linux-arm64-gnu@16.3.3': optional: true - '@next/swc-linux-arm64-musl@16.3.2': + '@next/swc-linux-arm64-musl@16.3.3': optional: true - '@next/swc-linux-x64-gnu@16.3.2': + '@next/swc-linux-x64-gnu@16.3.3': optional: true - '@next/swc-linux-x64-musl@16.3.2': + '@next/swc-linux-x64-musl@16.3.3': optional: true - '@next/swc-win32-arm64-msvc@16.3.2': + '@next/swc-win32-arm64-msvc@16.3.3': optional: true - '@next/swc-win32-x64-msvc@16.3.2': + '@next/swc-win32-x64-msvc@16.3.3': optional: true '@noble/hashes@2.3.0': {} @@ -4664,22 +4664,22 @@ snapshots: dependencies: content-type: 2.1.0 - next-auth@5.0.0-beta.32(next@16.3.2(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): + next-auth@5.0.0-beta.32(next@16.3.3(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): dependencies: '@auth/core': 0.41.3 - next: 16.3.2(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.3.3(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 next-intl-swc-plugin-extractor@4.13.7: {} - next-intl@4.13.7(@swc/helpers@0.5.23)(next@16.3.2(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2): + next-intl@4.13.7(@swc/helpers@0.5.23)(next@16.3.3(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@7.0.2): dependencies: '@formatjs/intl-localematcher': 0.8.13 '@parcel/watcher': 2.6.0 '@swc/core': 1.15.47(@swc/helpers@0.5.23) icu-minify: 4.13.7 negotiator: 1.1.0 - next: 16.3.2(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.3.3(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-intl-swc-plugin-extractor: 4.13.7 po-parser: 2.2.0 react: 19.2.8 @@ -4689,9 +4689,9 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - next@16.3.2(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.3(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.3.2 + '@next/env': 16.3.3 '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.11.16 caniuse-lite: 1.0.30001809 @@ -4700,14 +4700,14 @@ snapshots: react-dom: 19.2.8(react@19.2.8) styled-jsx: 5.1.6(react@19.2.8) optionalDependencies: - '@next/swc-darwin-arm64': 16.3.2 - '@next/swc-darwin-x64': 16.3.2 - '@next/swc-linux-arm64-gnu': 16.3.2 - '@next/swc-linux-arm64-musl': 16.3.2 - '@next/swc-linux-x64-gnu': 16.3.2 - '@next/swc-linux-x64-musl': 16.3.2 - '@next/swc-win32-arm64-msvc': 16.3.2 - '@next/swc-win32-x64-msvc': 16.3.2 + '@next/swc-darwin-arm64': 16.3.3 + '@next/swc-darwin-x64': 16.3.3 + '@next/swc-linux-arm64-gnu': 16.3.3 + '@next/swc-linux-arm64-musl': 16.3.3 + '@next/swc-linux-x64-gnu': 16.3.3 + '@next/swc-linux-x64-musl': 16.3.3 + '@next/swc-win32-arm64-msvc': 16.3.3 + '@next/swc-win32-x64-msvc': 16.3.3 sharp: 0.35.3(@types/node@26.2.0) transitivePeerDependencies: - '@babel/core' From f2ac4745d4228bd73409e3d6155fac48e4a07600 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 29 Aug 2026 21:56:52 +0200 Subject: [PATCH 3/3] feat: redesign home and register pages for cleaner overview Rebuild the home landing layout into a streamlined, more scannable design: a centered focused hero, feature cards, a compact stats row, and clearer news/users sections. Rework the register page into a balanced two-column layout with a tidier intro panel and sticky form. Add statsArticles translation key across locale files; verified with tsc and biome. --- src/app/(site)/page.tsx | 260 +++++++++++++++++-------------- src/app/(site)/register/page.tsx | 102 ++++++------ src/messages/bg.json | 1 + src/messages/cs.json | 1 + src/messages/da.json | 1 + src/messages/de.json | 1 + src/messages/el.json | 1 + src/messages/en.json | 1 + src/messages/es.json | 1 + src/messages/fr.json | 1 + src/messages/hr.json | 1 + src/messages/hu.json | 1 + src/messages/it.json | 1 + src/messages/nl.json | 1 + src/messages/no.json | 1 + src/messages/pl.json | 1 + src/messages/pt.json | 1 + src/messages/ro.json | 1 + src/messages/ru.json | 1 + src/messages/sk.json | 1 + src/messages/sr.json | 1 + src/messages/sv.json | 1 + src/messages/tr.json | 1 + src/messages/uk.json | 1 + 24 files changed, 220 insertions(+), 164 deletions(-) diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx index f69f9cb9..cef6a1ac 100644 --- a/src/app/(site)/page.tsx +++ b/src/app/(site)/page.tsx @@ -99,6 +99,24 @@ export default async function Home() { const captcha = await captchaConfig(); const nonce = (await headers()).get("x-nonce") ?? undefined; + const features = [ + { + title: tp("featuresAvatar"), + desc: tp("featuresAvatarDesc"), + icon: "/assets/images/icons/friends.png", + }, + { + title: tp("featuresFurniture"), + desc: tp("featuresFurnitureDesc"), + icon: "/assets/images/icons/catalog.png", + }, + { + title: tp("featuresCommunity"), + desc: tp("featuresCommunityDesc"), + icon: "/assets/images/icons/navigation/goody.png", + }, + ]; + return (
{/* ── Top Bar ── */} @@ -196,98 +214,118 @@ export default async function Home() { />
-
-
- - - - - {th("online", { count: online, hotel: hotelName })} -
-

- -

-

- {th("tagline")} -

-
- - - {tp("register")} - - - {th("login")} - -
+
+ + + + + {th("online", { count: online, hotel: hotelName })}
-
-
- Frank +

+ +

+

+ {tp("welcomeBody")} +

+
+ + + {tp("register")} + + + {th("login")} +
- {/* ── Stats ── */} + {/* ── Features ── */} -
+
+ {features.map((f) => ( + +
+ +
+

+ {f.title} +

+

+ {f.desc} +

+
+ ))} +
+ + + {/* ── Stats + Login ── */} + +
{[ { value: users, @@ -299,52 +337,38 @@ export default async function Home() { label: tp("statsRooms"), icon: "/assets/images/icons/catalog.png", }, + { + value: online, + label: tp("statsOnline"), + icon: "/assets/images/icons/navigation/me.png", + }, + { + value: articles.length, + label: tp("statsArticles"), + icon: "/assets/images/icons/article.gif", + }, ].map((s) => ( +
+ +
-
-
- -
-
- -
-
- {s.label} -
+ className="text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-1.5" + style={{ color: "var(--color-text-muted)" }} + > + + {s.label}
))}
- {/* ── Login + News ── */} + {/* ── Login + Online users ── */}
- {/* Left: Login & Online users */} - {/* Right: News */} + {/* News */} -
+
{recentPhotos.slice(0, 4).map((p) => ( -
- {/* Left panel */} -
+
+ {/* Left: intro panel */} +
+
- Frank -
- - {tpr("usersOnline", { count: online })} +
+
+ Frank +
+
+
+ + {tpr("usersOnline", { count: online })} +
+

+ {tpr("title")} — {hotelName} +

+

+ {tpr("subtitle")} +

+
-

- {tpr("title")} — {hotelName} -

-

- {th("tagline")} -

@@ -251,7 +261,7 @@ export default async function RegisterPage() { )}

{tpr("alreadyHaveAccountPre")}{" "} @@ -268,7 +278,7 @@ export default async function RegisterPage() {

{/* Right: form */} -
+