fix(public): distinguish unavailable data from empty results and missing pages
CI / check (push) Successful in 56s
CI / deploy (push) Successful in 1m30s
CI / publish-container (push) Successful in 44s

This commit is contained in:
Simo committed 2026-09-11 00:39:53 +02:00
1 parent db4acbb46e
commit 445ef13846
16 files changed
+608 -169

No files matched your search

+94 -69
View File
@@ -4,6 +4,8 @@ import { headers } from "next/headers";
import Image from "next/image";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { PublicLoadError } from "@/components/public/load-error";
import { publicReadFailure } from "@/lib/services/public-read";
export const metadata: Metadata = {
title: "Home",
@@ -53,34 +55,36 @@ async function getHotelData() {
.from(User)
.where(eq(User.online, "1"))
.then((rows) => rows[0]?.total ?? 0),
).catch(() => 0),
).catch(publicReadFailure("home.online")),
cached("total_users", 300_000, () =>
db
.select({ total: count() })
.from(User)
.then((rows) => rows[0]?.total ?? 0),
).catch(() => 0),
).catch(publicReadFailure("home.users")),
cached("total_rooms", 300_000, () =>
db
.select({ total: count() })
.from(Rooms)
.then((rows) => rows[0]?.total ?? 0),
).catch(() => 0),
getNewsList(4),
).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(() => []),
).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(() => []),
).catch(publicReadFailure("home.photos")),
]);
return {
@@ -100,6 +104,7 @@ export default async function Home() {
if (session?.user?.id) redirect("/me");
const tp = await getTranslations("pages.home");
const te = await getTranslations("publicData");
const th = await getTranslations("header");
const {
hotelName,
@@ -251,7 +256,11 @@ export default async function Home() {
<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>
<LiveOnlineCount initial={online} hotelName={hotelName} />
{online === null ? (
<span>{te("unavailable")}</span>
) : (
<LiveOnlineCount initial={online} hotelName={hotelName} />
)}
</div>
<h1
className="text-2xl font-black leading-[1.05] tracking-tight mb-2 sm:text-3xl md:text-5xl lg:text-6xl"
@@ -368,7 +377,7 @@ export default async function Home() {
live: true,
},
{
value: articles.length,
value: articles?.length ?? null,
label: tp("statsArticles"),
icon: "/assets/images/icons/article.gif",
live: false,
@@ -379,7 +388,9 @@ export default async function Home() {
className="relative p-3 sm:p-5 text-center transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
>
<div className="mb-0.5 text-2xl sm:text-3xl font-black tracking-tight md:text-4xl">
{s.live ? (
{s.value === null ? (
<span className="text-base">{te("unavailable")}</span>
) : s.live ? (
<LiveOnlineCounter initial={s.value} />
) : (
<AnimatedCounter value={s.value} />
@@ -415,36 +426,42 @@ export default async function Home() {
/>
</SurfaceCard>
{recentUsers.length > 0 && (
<SurfaceCard
title={tp("recentUsers")}
icon="/assets/images/icons/navigation/goody.png"
actionHref="/community"
actionLabel={tp("allUsers")}
bodyClassName="p-3.5 sm:p-4"
>
<div className="grid grid-cols-4 gap-1.5 sm:gap-2">
{recentUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-0.5 rounded-lg px-1 py-1.5 transition-colors hover:bg-[color-mix(in_srgb,var(--color-primary)_5%,transparent)]"
>
<UserAvatarThumbnail
figure={u.look}
alt={u.username}
options={{ direction: 2 }}
className="rounded-md"
/>
<span
className="max-w-[64px] truncate text-center text-[9px] sm:text-[10px] font-bold leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
{recentUsers === null ? (
<SurfaceCard title={tp("recentUsers")} bodyClassName="p-4">
<PublicLoadError href="/" />
</SurfaceCard>
) : (
recentUsers.length > 0 && (
<SurfaceCard
title={tp("recentUsers")}
icon="/assets/images/icons/navigation/goody.png"
actionHref="/community"
actionLabel={tp("allUsers")}
bodyClassName="p-3.5 sm:p-4"
>
<div className="grid grid-cols-4 gap-1.5 sm:gap-2">
{recentUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-0.5 rounded-lg px-1 py-1.5 transition-colors hover:bg-[color-mix(in_srgb,var(--color-primary)_5%,transparent)]"
>
<UserAvatarThumbnail
figure={u.look}
alt={u.username}
options={{ direction: 2 }}
className="rounded-md"
/>
<span
className="max-w-[64px] truncate text-center text-[9px] sm:text-[10px] font-bold leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</SurfaceCard>
)
)}
</Reveal>
@@ -456,7 +473,9 @@ export default async function Home() {
actionHref="/news"
actionLabel={tp("allNews")}
>
{articles.length > 0 ? (
{articles === null ? (
<PublicLoadError href="/" />
) : articles.length > 0 ? (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
{articles.slice(0, 4).map((a) => (
<Link
@@ -517,36 +536,42 @@ export default async function Home() {
</div>
{/* ── Recent photos ── */}
{recentPhotos.length > 0 && (
<Reveal>
<SurfaceCard
title={tp("recentPhotos")}
icon="/assets/images/icons/camera.png"
>
<div className="mx-auto grid max-w-md grid-cols-4 gap-2">
{recentPhotos.slice(0, 4).map((p) => (
<Link
key={p.id}
href="/photos"
className="block aspect-square overflow-hidden rounded-xl border transition-all duration-200 hover:shadow-md hover:scale-[1.02]"
style={{
borderColor:
"color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
}}
>
<Image
src={p.url}
alt=""
width={200}
height={200}
className="h-full w-full object-cover"
unoptimized
/>
</Link>
))}
</div>
</SurfaceCard>
</Reveal>
{recentPhotos === null ? (
<SurfaceCard title={tp("recentPhotos")} bodyClassName="p-4">
<PublicLoadError href="/" />
</SurfaceCard>
) : (
recentPhotos.length > 0 && (
<Reveal>
<SurfaceCard
title={tp("recentPhotos")}
icon="/assets/images/icons/camera.png"
>
<div className="mx-auto grid max-w-md grid-cols-4 gap-2">
{recentPhotos.slice(0, 4).map((p) => (
<Link
key={p.id}
href="/photos"
className="block aspect-square overflow-hidden rounded-xl border transition-all duration-200 hover:shadow-md hover:scale-[1.02]"
style={{
borderColor:
"color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
}}
>
<Image
src={p.url}
alt=""
width={200}
height={200}
className="h-full w-full object-cover"
unoptimized
/>
</Link>
))}
</div>
</SurfaceCard>
</Reveal>
)
)}
</div>
);