From 469f69ddd7af73796d730a97973794f23a1e48b8 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 18 Sep 2026 13:27:08 +0200 Subject: [PATCH] feat(home): two-column hero with live status panel, explore nav, featured news --- src/app/(site)/page.tsx | 697 +++++++++++------- .../shared/public-avatar-contract.test.ts | 2 +- src/messages/en.json | 3 + src/messages/it.json | 3 + 4 files changed, 453 insertions(+), 252 deletions(-) diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx index 38c7a538..ab3a058e 100644 --- a/src/app/(site)/page.tsx +++ b/src/app/(site)/page.tsx @@ -34,7 +34,6 @@ 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"; @@ -56,51 +55,65 @@ async function getHotelData() { siteSettings.get("cms_logo", ""), ]); - 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(publicReadFailure("home.online")), - cached("total_users", 300_000, () => - db - .select({ total: count() }) - .from(User) - .then((rows) => rows[0]?.total ?? 0), - ).catch(publicReadFailure("home.users")), - cached("total_rooms", 300_000, () => - db - .select({ total: count() }) - .from(Rooms) - .then((rows) => rows[0]?.total ?? 0), - ).catch(publicReadFailure("home.rooms")), - getNewsList(4, { throwOnError: true }).catch( - publicReadFailure("home.news"), - ), - cached("home_online_users", 15_000, () => - db - .select({ username: User.username, look: User.look }) - .from(User) - .where(eq(User.online, "1")) - .limit(12), - ).catch(publicReadFailure("home.recent-users")), - cached("home_recent_photos", 60_000, () => - db - .select({ id: CameraWeb.id, url: CameraWeb.url }) - .from(CameraWeb) - .orderBy(desc(CameraWeb.timestamp)) - .limit(4), - ).catch(publicReadFailure("home.photos")), - ]); + const [ + online, + users, + rooms, + totalPhotos, + 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(publicReadFailure("home.online")), + cached("total_users", 300_000, () => + db + .select({ total: count() }) + .from(User) + .then((rows) => rows[0]?.total ?? 0), + ).catch(publicReadFailure("home.users")), + cached("total_rooms", 300_000, () => + db + .select({ total: count() }) + .from(Rooms) + .then((rows) => rows[0]?.total ?? 0), + ).catch(publicReadFailure("home.rooms")), + cached("total_photos", 300_000, () => + db + .select({ total: count() }) + .from(CameraWeb) + .then((rows) => rows[0]?.total ?? 0), + ).catch(publicReadFailure("home.photos-count")), + getNewsList(4, { throwOnError: true }).catch( + publicReadFailure("home.news"), + ), + cached("home_online_users", 15_000, () => + db + .select({ username: User.username, look: User.look }) + .from(User) + .where(eq(User.online, "1")) + .limit(12), + ).catch(publicReadFailure("home.recent-users")), + cached("home_recent_photos", 60_000, () => + db + .select({ id: CameraWeb.id, url: CameraWeb.url }) + .from(CameraWeb) + .orderBy(desc(CameraWeb.timestamp)) + .limit(4), + ).catch(publicReadFailure("home.photos")), + ]); return { hotelName, online, users, rooms, + totalPhotos, articles, recentUsers, recentPhotos, @@ -115,11 +128,13 @@ export default async function Home() { const tp = await getTranslations("pages.home"); const te = await getTranslations("publicData"); const th = await getTranslations("header"); + const tn = await getTranslations("nav"); const { hotelName, online, users, rooms, + totalPhotos, articles, recentUsers, recentPhotos, @@ -147,6 +162,72 @@ export default async function Home() { }, ]; + type NewsArticle = NonNullable[number]; + + const renderArticleCard = (a: NewsArticle, featured = false) => ( + +
+ {a.title} +
+ → +
+
+ {formatDate(a.createdAt, "date", "")} +
+
+ {featured && ( +

+ {tp("latestArticle")} +

+ )} +

+ {a.title} +

+

+ {formatDate(a.createdAt, "date", "")} +

+
+
+ + ); + return (
{/* ── Top Bar ── */} @@ -247,6 +328,13 @@ export default async function Home() { "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%)", }} /> +
-
-
- - - - - {online === null ? ( - {te("unavailable")} - ) : ( - - )} +
+
+

+ + {tp("welcome", { hotelName: "" })} + {" "} + + + +

+

+ {tp("welcomeBody")} +

+
+ + + {tp("register")} + + + {th("login")} + +
-

- - {tp("welcome", { hotelName: "" })} - {" "} - - - -

-

- {tp("welcomeBody")} -

-
- +
- - {tp("register")} - - - {th("login")} - +
@@ -469,10 +668,10 @@ export default async function Home() { live: false, }, { - value: online, - label: tp("statsOnline"), - icon: ICON_NAV_ME, - live: true, + value: totalPhotos, + label: tp("statsPhotos"), + icon: ICON_CAMERA, + live: false, }, { value: articles?.length ?? null, @@ -548,149 +747,145 @@ export default async function Home() {
- {/* ── Login + Online users ── */} -
- {tp("sectionCommunityLabel")} -

- {tp("sectionCommunityTitle")} -

-
-
- - - + + - {recentUsers === null ? ( - - - - ) : ( - recentUsers.length > 0 && ( + {/* ── Login + Online users ── */} + +
+
+ {tp("sectionCommunityLabel")} +

+ {tp("sectionCommunityTitle")} +

+
+
+ -
- {recentUsers.map((u) => ( -
- - - {u.username} - -
- ))} -
+
- ) - )} -
- {/* News */} - - - {articles === null ? ( - - ) : articles.length > 0 ? ( -
- {articles.slice(0, 4).map((a) => ( - + + + ) : ( + recentUsers.length > 0 && ( + -
- {a.title} -
- - → - -
-
- {formatDate(a.createdAt, "date", "")} -
-
-

- {a.title} -

-

+ {recentUsers.map((u) => ( +

- {formatDate(a.createdAt, "date", "")} -

-
+ + + {u.username} + +
+ ))}
- - ))} -
- ) : ( -

+ ) + )} + + + {/* News */} + + - {tp("noArticles")} -

- )} -
-
-
+ {articles === null ? ( + + ) : articles.length > 0 ? ( +
+ {renderArticleCard(articles[0], true)} +
+ {articles.slice(1, 4).map((a) => renderArticleCard(a))} +
+
+ ) : ( +

+ {tp("noArticles")} +

+ )} + + +
+
+ {/* ── Recent photos ── */} {recentPhotos === null ? ( @@ -701,7 +896,7 @@ export default async function Home() { recentPhotos.length > 0 && ( -
+
{recentPhotos.slice(0, 4).map((p) => ( ([ ["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", 1], + ["src/app/(site)/page.tsx", 2], ["src/app/(site)/radio/leaderboard/page.tsx", 1], ["src/app/(site)/radio/shouts/page.tsx", 1], ["src/app/(site)/rankings/page.tsx", 1], diff --git a/src/messages/en.json b/src/messages/en.json index abe6a76c..da9dbe9f 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -274,7 +274,10 @@ "statsOnline": "Online", "statsCitizens": "Citizens", "statsRooms": "Rooms", + "statsPhotos": "Photos", "statsArticles": "Articles", + "onlineNow": "Online now", + "exploreLabel": "Explore the hotel", "featuresAvatar": "Avatar", "featuresAvatarDesc": "Create your unique look", "featuresFurniture": "Furniture", diff --git a/src/messages/it.json b/src/messages/it.json index 427d5ecb..e69ff500 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -274,7 +274,10 @@ "statsOnline": "Online", "statsCitizens": "Cittadini", "statsRooms": "Stanze", + "statsPhotos": "Foto", "statsArticles": "Articoli", + "onlineNow": "Online adesso", + "exploreLabel": "Esplora l'hotel", "featuresAvatar": "Avatar", "featuresAvatarDesc": "Crea il tuo look unico", "featuresFurniture": "Arredi",