Files
Epicnabbo-Catalogus-Updated…/src/app/(site)/page.tsx
T
openhands 7a41775c7e fix: disable route prefetching app-wide to avoid spurious requests
Wrap next/link in a shared Link component that ships prefetch=false by
default, so no route is ever prefetched (viewport or hover) anymore, and
drop the DNS prefetch hint. Removes hidden background requests that were
the source of intermittent issues.
2026-08-29 19:27:13 +02:00

624 lines
18 KiB
TypeScript

import { count, desc, eq } from "drizzle-orm";
import { headers } from "next/headers";
import Image from "next/image";
import { redirect } from "next/navigation";
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 { Clock } from "@/components/clock";
import { LanguageSwitcher } from "@/components/language-switcher";
import Link from "@/components/link";
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() {
const [hotelName, logo] = await Promise.all([
resolveHotelName(),
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(() => []),
]);
return {
hotelName,
online,
users,
rooms,
articles,
recentUsers,
recentPhotos,
latestUsers,
logo,
};
}
export default async function Home() {
const session = await auth();
if (session?.user?.id) redirect("/me");
const tp = await getTranslations("pages.home");
const th = await getTranslations("header");
const {
hotelName,
online,
users,
rooms,
articles,
recentUsers,
recentPhotos,
latestUsers,
logo,
} = await getHotelData();
const captcha = await captchaConfig();
const nonce = (await headers()).get("x-nonce") ?? undefined;
return (
<div className="mx-auto w-full max-w-6xl flex flex-col gap-8 pb-16">
{/* ── Top Bar ── */}
<Reveal>
<div
className="relative flex items-center justify-between rounded-xl border px-5 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)",
}}
>
<div className="flex items-center 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="text-sm font-extrabold uppercase tracking-wide"
style={{ color: "var(--color-text-readable)" }}
>
{hotelName}
</span>
</div>
<div
className="flex items-center gap-1.5 rounded-lg px-2 py-1"
style={{
background:
"color-mix(in srgb, var(--color-text-muted) 4%, transparent)",
}}
>
<LanguageSwitcher />
<div
className="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 ── */}
<Reveal>
<div
className="relative overflow-hidden rounded-3xl border"
style={{
borderColor:
"color-mix(in srgb, var(--color-text-muted) 8%, 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 />
<div
className="relative flex flex-col items-center gap-8 px-6 py-12 md:flex-row md:px-12 md:py-16"
style={{ zIndex: 2 }}
>
<div className="flex-1 text-center md:text-left">
<div
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest mb-5 border backdrop-blur-sm"
style={{
background:
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
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-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
</span>
{th("online", { count: online, hotel: hotelName })}
</div>
<h1
className="text-4xl font-black leading-[1.05] tracking-tight mb-3 md:text-5xl lg:text-6xl"
style={{
color: "var(--color-text-readable)",
fontFamily: "var(--font-nunito)",
}}
>
<TypewriterText text={tp("welcome", { hotelName })} />
</h1>
<p
className="text-base leading-relaxed max-w-xl mb-7 md:text-lg"
style={{ color: "var(--color-text-muted)" }}
>
{th("tagline")}
</p>
<div className="flex flex-wrap gap-3 justify-center md:justify-start">
<Link
href="/register"
className="inline-flex items-center gap-2.5 font-extrabold text-sm px-7 py-3.5 rounded-xl transition-all duration-200 hover:scale-[1.03] active:scale-95 shadow-lg"
style={{
background: "var(--color-primary)",
color: "var(--color-primary-foreground)",
boxShadow:
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
}}
>
<Image
src="/assets/images/EnterHubbly.png"
alt=""
width={20}
height={20}
unoptimized
/>
{tp("register")}
</Link>
<Link
href="/login"
className="inline-flex items-center gap-2 font-bold text-sm px-7 py-3.5 rounded-xl border-2 transition-all duration-200 hover:scale-[1.03] active:scale-95 backdrop-blur-sm"
style={{
borderColor:
"color-mix(in srgb, var(--color-text-muted) 20%, transparent)",
color: "var(--color-text-readable)",
background:
"color-mix(in srgb, var(--color-surface) 50%, transparent)",
}}
>
{th("login")}
</Link>
</div>
</div>
<div className="shrink-0 relative animate-float">
<div
className="absolute -bottom-4 -left-4 w-48 h-48 rounded-full opacity-[0.15] blur-[60px]"
style={{ background: "var(--color-primary)" }}
/>
<Image
src="/assets/images/FrankwithBag.gif"
alt="Frank"
width={170}
height={230}
className="object-contain drop-shadow-2xl relative"
unoptimized
priority
/>
</div>
</div>
</div>
</Reveal>
{/* ── Stats ── */}
<Reveal>
<div className="grid grid-cols-3 gap-4 stagger-children">
{[
{
value: online,
label: tp("statsOnline"),
icon: "/assets/images/icons/online-friends.png",
},
{
value: users,
label: tp("statsCitizens"),
icon: "/assets/images/icons/friends.png",
},
{
value: rooms,
label: tp("statsRooms"),
icon: "/assets/images/icons/catalog.png",
},
].map((s) => (
<SurfaceCard
key={s.label}
className="relative p-5 text-center transition-all duration-300 hover:-translate-y-1 hover:shadow-xl"
>
<div
className="absolute -top-6 -right-6 w-20 h-20 rounded-full opacity-[0.06]"
style={{ background: "var(--color-primary)" }}
/>
<div className="relative">
<div
className="mx-auto mb-2.5 flex h-10 w-10 items-center justify-center rounded-xl"
style={{
background:
"color-mix(in srgb, var(--color-primary) 14%, transparent)",
}}
>
<Image
src={s.icon}
alt=""
width={24}
height={24}
unoptimized
/>
</div>
<div
className="mb-0.5 text-3xl font-black tracking-tight md:text-4xl"
style={{ color: "var(--color-primary)" }}
>
<AnimatedCounter value={s.value} />
</div>
<div
className="text-xs font-bold uppercase tracking-widest"
style={{ color: "var(--color-text-muted)" }}
>
{s.label}
</div>
</div>
</SurfaceCard>
))}
</div>
</Reveal>
{/* ── Login + News ── */}
<div className="flex flex-col gap-4 md:flex-row">
{/* Left: Login & Register */}
<Reveal className="flex w-full flex-col gap-4 md:w-1/3 md:flex-shrink-0">
<SurfaceCard
title={th("login")}
icon="/assets/images/icons/navigation/me.png"
bodyClassName="p-4"
>
<HomeLoginForm
captcha={{
provider: captcha.provider,
siteKey: captcha.siteKey || undefined,
field: captcha.field || undefined,
}}
nonce={nonce}
/>
</SurfaceCard>
<Link href="/register" className="relative block group">
<SurfaceCard className="transition-all duration-200 group-hover:shadow-lg">
<Image
src="/assets/images/EnterHubbly.png"
alt="Register"
width={300}
height={100}
className="block w-full"
unoptimized
/>
</SurfaceCard>
<span
className="pointer-events-none absolute inset-0 flex items-center text-2xl font-extrabold text-white md:text-3xl"
style={{
marginLeft: "18px",
fontFamily: "var(--font-nunito)",
textShadow: "0 2px 8px rgba(0,0,0,0.6)",
}}
>
{tp("register")}
</span>
</Link>
{latestUsers.length > 0 && (
<SurfaceCard
title={tp("latestUsers", { count: users.toLocaleString() })}
icon="/assets/images/icons/navigation/goody.png"
bodyClassName="p-4"
>
<div className="grid grid-cols-4 gap-2">
{latestUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-1"
>
<UserAvatarThumbnail
figure={u.look}
alt={u.username}
options={{ direction: 3 }}
/>
<span
className="max-w-[64px] truncate text-center text-[11px] font-bold leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</SurfaceCard>
)}
</Reveal>
{/* Right: News */}
<Reveal delay={0.15} className="flex w-full flex-col gap-4 md:w-2/3">
<SurfaceCard
title={tp("latestNews")}
icon="/assets/images/icons/article.gif"
actionHref="/news"
actionLabel={tp("allNews")}
>
{articles.length > 0 ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{articles.slice(0, 4).map((a) => (
<Link
key={a.slug}
href={`/news/${a.slug}`}
className="group relative w-full overflow-hidden rounded-xl transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
style={{
height: "200px",
backgroundColor: "var(--color-background)",
border:
"1px solid color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
}}
>
<div className="relative h-full w-full overflow-hidden rounded-xl">
<Image
src={a.image}
alt={a.title}
fill
sizes="(max-width: 768px) 100vw, 400px"
className="rounded-xl object-cover transition-all duration-300 group-hover:scale-105"
/>
<div className="absolute right-2.5 top-2.5 flex h-8 w-8 translate-x-1 items-center justify-center rounded-full bg-black/50 opacity-0 backdrop-blur-sm transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100">
<span className="text-sm font-bold text-white">→</span>
</div>
<div
className="absolute bottom-0 left-0 w-full p-2.5"
style={{
background:
"linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%)",
}}
>
<h3 className="truncate text-base font-bold text-white text-shadow-sm">
{a.title}
</h3>
<p
className="mt-0.5 text-[11px] font-semibold opacity-80"
style={{ color: "var(--color-text-muted)" }}
>
{formatDate(a.createdAt, "date", "")}
</p>
</div>
</div>
</Link>
))}
</div>
) : (
<p
className="py-8 text-center text-sm"
style={{ color: "var(--color-text-muted)" }}
>
{tp("noArticles")}
</p>
)}
</SurfaceCard>
</Reveal>
</div>
{/* ── Online users ── */}
{recentUsers.length > 0 && (
<Reveal>
<SurfaceCard
title={tp("recentUsers")}
icon="/assets/images/icons/navigation/goody.png"
actionHref="/community"
actionLabel={tp("allUsers")}
>
<div className="flex flex-wrap justify-center gap-2">
{recentUsers.slice(0, 12).map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-0.5 rounded-lg px-2 py-1.5 transition-all duration-200 hover:scale-110 hover:shadow-md"
style={{
background:
"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-[50px] truncate text-center text-[9px] font-bold leading-tight"
style={{ color: "var(--color-text-readable)" }}
>
{u.username}
</span>
</div>
))}
</div>
</SurfaceCard>
</Reveal>
)}
{/* ── Bottom CTA ── */}
<Reveal>
<div
className="relative overflow-hidden rounded-3xl border px-6 py-12 text-center"
style={{
background: `linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 8%, var(--color-surface)), color-mix(in srgb, var(--color-surface) 70%, transparent))`,
borderColor:
"color-mix(in srgb, var(--color-primary) 15%, transparent)",
}}
>
<h2
className="mb-2 text-2xl font-black tracking-tight md:text-3xl"
style={{
color: "var(--color-text-readable)",
fontFamily: "var(--font-nunito)",
}}
>
{tp("joinNow", { hotelName })}
</h2>
<p
className="mb-6 max-w-sm mx-auto text-sm"
style={{ color: "var(--color-text-muted)" }}
>
{tp("welcomeBody")}
</p>
<Link
href="/register"
className="inline-flex items-center gap-2.5 font-extrabold text-sm px-8 py-3.5 rounded-xl transition-all duration-200 hover:scale-[1.03] active:scale-95 shadow-lg"
style={{
background: "var(--color-primary)",
color: "var(--color-primary-foreground)",
boxShadow:
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
}}
>
<Image
src="/assets/images/EnterHubbly.png"
alt=""
width={20}
height={20}
unoptimized
/>
{tp("register")}
</Link>
</div>
</Reveal>
{/* ── Recent photos ── */}
{recentPhotos.length > 0 && (
<Reveal>
<SurfaceCard
title={tp("recentPhotos")}
icon="/assets/images/icons/camera.png"
>
<div className="mx-auto grid max-w-sm 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"
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 transition-transform duration-300 hover:scale-105"
unoptimized
/>
</Link>
))}
</div>
</SurfaceCard>
</Reveal>
)}
</div>
);
}