206 lines
8.4 KiB
TypeScript
206 lines
8.4 KiB
TypeScript
import Link from "next/link";
|
||
import { prisma } from "@/lib/db";
|
||
import { Button } from "@/components/ui/button";
|
||
import { AddonCard } from "@/components/public/AddonCard";
|
||
import { HeroBanner } from "@/components/public/HeroBanner";
|
||
import { GameGallery } from "@/components/public/GameGallery";
|
||
import { ShutdownBanner } from "@/components/public/ShutdownBanner";
|
||
import { T } from "@/components/public/T";
|
||
import { ArticleCard } from "@/components/public/ArticleCard";
|
||
import { getSiteSettings } from "@/lib/site-settings";
|
||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||
import { Sparkles, Shield, Zap } from "lucide-react";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
export default async function HomePage() {
|
||
const siteSettings = await getSiteSettings();
|
||
const wowVersion = DEFAULT_WOW_VERSION;
|
||
const [featuredAddons, launcher, launcherDownloads, banners, galleryImages, latestArticles] =
|
||
await Promise.all([
|
||
prisma.addon.findMany({
|
||
where: { published: true },
|
||
include: {
|
||
releases: {
|
||
where: { isLatest: true, wowVersion },
|
||
select: { version: true },
|
||
},
|
||
screenshots: {
|
||
orderBy: { sortOrder: "asc" },
|
||
take: 1,
|
||
select: { imageUrl: true },
|
||
},
|
||
},
|
||
orderBy: { totalDownloads: "desc" },
|
||
take: 6,
|
||
}),
|
||
prisma.software.findUnique({
|
||
where: { slug: "nanami-launcher" },
|
||
include: {
|
||
versions: {
|
||
where: { isLatest: true, wowVersion },
|
||
take: 1,
|
||
},
|
||
},
|
||
}),
|
||
prisma.softwareVersion.aggregate({
|
||
where: { software: { slug: "nanami-launcher" }, wowVersion },
|
||
_sum: { downloadCount: true },
|
||
}),
|
||
prisma.bannerImage.findMany({
|
||
where: { enabled: true },
|
||
orderBy: { sortOrder: "asc" },
|
||
select: { imageUrl: true },
|
||
}),
|
||
prisma.galleryImage.findMany({
|
||
where: { enabled: true },
|
||
orderBy: { sortOrder: "asc" },
|
||
select: { imageUrl: true, title: true, titleEn: true },
|
||
}),
|
||
prisma.article.findMany({
|
||
where: { published: true },
|
||
orderBy: { createdAt: "desc" },
|
||
take: 3,
|
||
}),
|
||
]);
|
||
|
||
const launcherVersion = launcher?.versions[0]?.version ?? null;
|
||
const totalDownloads = launcherDownloads._sum.downloadCount ?? 0;
|
||
|
||
return (
|
||
<>
|
||
{siteSettings.shutdownBannerEnabled && siteSettings.shutdownAt && (
|
||
<ShutdownBanner
|
||
title={siteSettings.shutdownTitle || "旅途总有终点,\n但我们的故事未完待续"}
|
||
titleEn={
|
||
siteSettings.shutdownTitleEn ||
|
||
"Every journey reaches its end —\nbut our story is yet to be told"
|
||
}
|
||
subtitle={
|
||
siteSettings.shutdownSubtitle ||
|
||
"《Turtle WoW》即将关闭,服务器将于欧洲时间 5月15日 凌晨 00:00 正式关闭。"
|
||
}
|
||
subtitleEn={
|
||
siteSettings.shutdownSubtitleEn ||
|
||
"Turtle WoW is closing its gates. Servers will go dark at 00:00 European time on May 15."
|
||
}
|
||
shutdownAt={siteSettings.shutdownAt.toISOString()}
|
||
/>
|
||
)}
|
||
|
||
<HeroBanner
|
||
totalDownloads={totalDownloads || undefined}
|
||
launcherVersion={launcherVersion}
|
||
banners={banners}
|
||
/>
|
||
|
||
<GameGallery items={galleryImages} />
|
||
|
||
{/* Features */}
|
||
<section className="relative border-t border-amber-900/20 bg-gradient-to-b from-[#0d0b15] to-[#110f1a] dark:from-[#0d0b15] dark:to-[#110f1a]">
|
||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(168,85,247,0.06)_0%,transparent_70%)]" />
|
||
<div className="relative mx-auto max-w-6xl px-3 py-10 sm:px-4 sm:py-16">
|
||
<div className="grid gap-4 sm:gap-8 md:grid-cols-3">
|
||
<div className="flex flex-col items-center rounded-xl border border-amber-500/10 bg-white/5 p-6 text-center backdrop-blur transition-colors hover:border-amber-500/25">
|
||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-amber-500/10">
|
||
<Sparkles className="h-6 w-6 text-amber-400" />
|
||
</div>
|
||
<h3 className="mt-4 font-semibold text-amber-100">
|
||
<T section="home" k="featureDeepTitle" />
|
||
</h3>
|
||
<p className="mt-2 text-sm text-gray-400">
|
||
<T section="home" k="featureDeepDesc" />
|
||
</p>
|
||
</div>
|
||
<div className="flex flex-col items-center rounded-xl border border-amber-500/10 bg-white/5 p-6 text-center backdrop-blur transition-colors hover:border-amber-500/25">
|
||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-500/10">
|
||
<Shield className="h-6 w-6 text-purple-400" />
|
||
</div>
|
||
<h3 className="mt-4 font-semibold text-amber-100">
|
||
<T section="home" k="featureInstallTitle" />
|
||
</h3>
|
||
<p className="mt-2 text-sm text-gray-400">
|
||
<T section="home" k="featureInstallDesc" />
|
||
</p>
|
||
</div>
|
||
<div className="flex flex-col items-center rounded-xl border border-amber-500/10 bg-white/5 p-6 text-center backdrop-blur transition-colors hover:border-amber-500/25">
|
||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-cyan-500/10">
|
||
<Zap className="h-6 w-6 text-cyan-400" />
|
||
</div>
|
||
<h3 className="mt-4 font-semibold text-amber-100">
|
||
<T section="home" k="featureAITitle" />
|
||
</h3>
|
||
<p className="mt-2 text-sm text-gray-400">
|
||
<T section="home" k="featureAIDesc" />
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Latest Articles */}
|
||
{latestArticles.length > 0 && (
|
||
<section className="border-t border-amber-900/20 bg-gradient-to-b from-[#110f1a] to-[#0d0b15] dark:from-[#110f1a] dark:to-[#0d0b15]">
|
||
<div className="mx-auto max-w-6xl px-3 py-10 sm:px-4 sm:py-16">
|
||
<div className="mb-6 flex items-center justify-between sm:mb-8">
|
||
<h2 className="text-2xl font-bold text-amber-100">
|
||
<T section="home" k="latestArticles" />
|
||
</h2>
|
||
<Button
|
||
variant="outline"
|
||
className="border-amber-500/20 text-amber-200 hover:border-amber-500/40 hover:bg-amber-500/10 hover:text-amber-100"
|
||
render={<Link href="/articles" />}
|
||
>
|
||
<T section="home" k="viewMore" />
|
||
</Button>
|
||
</div>
|
||
<div className="grid gap-4 sm:gap-6 md:grid-cols-3">
|
||
{latestArticles.map((article) => (
|
||
<ArticleCard
|
||
key={article.id}
|
||
variant="compact"
|
||
article={{
|
||
id: article.id,
|
||
slug: article.slug,
|
||
title: article.title,
|
||
titleEn: article.titleEn,
|
||
summary: article.summary,
|
||
summaryEn: article.summaryEn,
|
||
coverImage: article.coverImage,
|
||
createdAt: article.createdAt.toISOString(),
|
||
}}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Featured Addons */}
|
||
{featuredAddons.length > 0 && (
|
||
<section className="border-t border-amber-900/20 bg-gradient-to-b from-[#110f1a] to-[#0d0b15] dark:from-[#110f1a] dark:to-[#0d0b15]">
|
||
<div className="mx-auto max-w-6xl px-3 py-10 sm:px-4 sm:py-16">
|
||
<div className="mb-6 flex items-center justify-between sm:mb-8">
|
||
<h2 className="text-2xl font-bold text-amber-100">
|
||
<T section="home" k="hotAddons" />
|
||
</h2>
|
||
<Button
|
||
variant="outline"
|
||
className="border-amber-500/20 text-amber-200 hover:border-amber-500/40 hover:bg-amber-500/10 hover:text-amber-100"
|
||
render={<Link href="/addons" />}
|
||
>
|
||
<T section="home" k="viewAll" />
|
||
</Button>
|
||
</div>
|
||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||
{featuredAddons.map((addon) => (
|
||
<AddonCard key={addon.id} addon={addon} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)}
|
||
</>
|
||
);
|
||
}
|