feat: add localization and site settings

This commit is contained in:
rucky
2026-05-12 09:58:25 +08:00
parent 9dc6c0dcce
commit fa7aedb8e7
67 changed files with 5221 additions and 888 deletions

View File

@@ -1,24 +1,35 @@
import Link from "next/link";
import Image from "next/image";
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 { Sparkles, Shield, Zap, Calendar } from "lucide-react";
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 { getServerWowVersion } from "@/lib/get-server-wow";
import { Sparkles, Shield, Zap } from "lucide-react";
export const dynamic = "force-dynamic";
export default async function HomePage() {
const siteSettings = await getSiteSettings();
const wowVersion = await getServerWowVersion();
const [featuredAddons, launcher, launcherDownloads, banners, galleryImages, latestArticles] =
await Promise.all([
prisma.addon.findMany({
where: { published: true },
include: {
releases: {
where: { isLatest: true },
where: { isLatest: true, wowVersion },
select: { version: true },
},
screenshots: {
orderBy: { sortOrder: "asc" },
take: 1,
select: { imageUrl: true },
},
},
orderBy: { totalDownloads: "desc" },
take: 6,
@@ -27,13 +38,13 @@ export default async function HomePage() {
where: { slug: "nanami-launcher" },
include: {
versions: {
where: { isLatest: true },
where: { isLatest: true, wowVersion },
take: 1,
},
},
}),
prisma.softwareVersion.aggregate({
where: { software: { slug: "nanami-launcher" } },
where: { software: { slug: "nanami-launcher" }, wowVersion },
_sum: { downloadCount: true },
}),
prisma.bannerImage.findMany({
@@ -44,7 +55,7 @@ export default async function HomePage() {
prisma.galleryImage.findMany({
where: { enabled: true },
orderBy: { sortOrder: "asc" },
select: { imageUrl: true, title: true },
select: { imageUrl: true, title: true, titleEn: true },
}),
prisma.article.findMany({
where: { published: true },
@@ -58,6 +69,25 @@ export default async function HomePage() {
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}
@@ -75,9 +105,11 @@ export default async function HomePage() {
<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"></h3>
<h3 className="mt-4 font-semibold text-amber-100">
<T section="home" k="featureDeepTitle" />
</h3>
<p className="mt-2 text-sm text-gray-400">
1.18.0
<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">
@@ -85,10 +117,10 @@ export default async function HomePage() {
<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">
Nanami
<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">
@@ -96,10 +128,10 @@ export default async function HomePage() {
<Zap className="h-6 w-6 text-cyan-400" />
</div>
<h3 className="mt-4 font-semibold text-amber-100">
AI
<T section="home" k="featureAITitle" />
</h3>
<p className="mt-2 text-sm text-gray-400">
<T section="home" k="featureAIDesc" />
</p>
</div>
</div>
@@ -111,48 +143,33 @@ export default async function HomePage() {
<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"></h2>
<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) => (
<Link
<ArticleCard
key={article.id}
href={`/articles/${article.slug}`}
className="group overflow-hidden rounded-xl border border-amber-500/10 bg-white/[0.03] transition-colors hover:border-amber-500/25"
>
{article.coverImage && (
<div className="relative aspect-[16/9] overflow-hidden">
<Image
src={article.coverImage}
alt={article.title}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, 33vw"
/>
</div>
)}
<div className="p-4">
<h3 className="mb-1.5 font-semibold text-amber-100 group-hover:text-amber-200 line-clamp-1">
{article.title}
</h3>
{article.summary && (
<p className="mb-2 line-clamp-2 text-sm text-gray-400">
{article.summary}
</p>
)}
<div className="flex items-center gap-1.5 text-xs text-gray-500">
<Calendar className="h-3 w-3" />
{new Date(article.createdAt).toLocaleDateString("zh-CN")}
</div>
</div>
</Link>
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>
@@ -164,13 +181,15 @@ export default async function HomePage() {
<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"></h2>
<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">