feat(guides): add structured image-first editor
This commit is contained in:
@@ -1,28 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound, permanentRedirect } from "next/navigation";
|
||||
import { notFound } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { GuideShell } from "@/components/public/guide-shell";
|
||||
import { loadPublicPage } from "@/lib/content/loaders";
|
||||
import { publicSections, StructuredGuideShell } from "@/components/public/structured-guide";
|
||||
import { getPublicGuide } from "@/lib/guides/queries";
|
||||
|
||||
type RouteParams = Promise<{ character: string; page: string }>;
|
||||
type Params = Promise<{ character: string; page: string }>;
|
||||
|
||||
export async function generateMetadata({ params }: { params: RouteParams }): Promise<Metadata> {
|
||||
export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
|
||||
await connection();
|
||||
const { character, page } = await params;
|
||||
const result = await loadPublicPage(character, page);
|
||||
if (result.type !== "page") return { title: "ไม่พบคู่มือ" };
|
||||
return {
|
||||
title: `${result.snapshot.page.title} — ${result.snapshot.character.nameTh}`,
|
||||
description: result.snapshot.character.description ?? undefined,
|
||||
};
|
||||
const guide = await getPublicGuide(character);
|
||||
return guide ? { title: `${page} — ${guide.name} Build Guide`, description: guide.overview } : { title: "ไม่พบ Guide" };
|
||||
}
|
||||
|
||||
export default async function GuidePage({ params }: { params: RouteParams }) {
|
||||
export default async function GuideSectionPage({ params }: { params: Params }) {
|
||||
await connection();
|
||||
const { character, page } = await params;
|
||||
const result = await loadPublicPage(character, page);
|
||||
if (result.type === "redirect") permanentRedirect(result.location);
|
||||
if (result.type === "not-found") notFound();
|
||||
return <GuideShell snapshot={result.snapshot} />;
|
||||
const guide = await getPublicGuide(character);
|
||||
if (!guide || !publicSections(guide).some((section) => section.slug === page)) notFound();
|
||||
return <StructuredGuideShell guide={guide} section={page} />;
|
||||
}
|
||||
|
||||
+16
-11
@@ -1,16 +1,21 @@
|
||||
import { notFound, permanentRedirect } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { loadPublicPage } from "@/lib/content/loaders";
|
||||
import { StructuredGuideShell } from "@/components/public/structured-guide";
|
||||
import { getPublicGuide } from "@/lib/guides/queries";
|
||||
|
||||
export default async function CharacterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ character: string }>;
|
||||
}) {
|
||||
type Params = Promise<{ character: string }>;
|
||||
|
||||
export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
|
||||
await connection();
|
||||
const { character } = await params;
|
||||
const result = await loadPublicPage(character);
|
||||
if (result.type === "redirect") permanentRedirect(result.location);
|
||||
notFound();
|
||||
const guide = await getPublicGuide((await params).character);
|
||||
return guide ? { title: `${guide.name} Build Guide`, description: guide.overview } : { title: "ไม่พบ Guide" };
|
||||
}
|
||||
|
||||
export default async function GuideOverviewPage({ params }: { params: Params }) {
|
||||
await connection();
|
||||
const guide = await getPublicGuide((await params).character);
|
||||
if (!guide) notFound();
|
||||
return <StructuredGuideShell guide={guide} />;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { EditorWorkspace } from "@/components/admin/editor-workspace";
|
||||
import { LiveRefresh } from "@/components/live-refresh";
|
||||
import { GuideSectionEditor } from "@/components/admin/guide-section-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
import { loadAdminNavigation, loadAdminPage } from "@/lib/content/loaders";
|
||||
import { getActiveCatalog, getGuideByCharacterKey, getPublicGuide } from "@/lib/guides/queries";
|
||||
|
||||
export default async function AdminEditorPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ character: string; page: string }>;
|
||||
}) {
|
||||
const sections = ["weapon", "artifact", "constellations", "team", "extra"] as const;
|
||||
|
||||
export default async function StructuredEditorPage({ params }: { params: Promise<{ character: string; page: string }> }) {
|
||||
await connection();
|
||||
if (!(await getAdminSession())) redirect("/admin/login");
|
||||
const { character, page } = await params;
|
||||
const [snapshot, navigation] = await Promise.all([
|
||||
loadAdminPage(character, page),
|
||||
loadAdminNavigation(),
|
||||
]);
|
||||
if (!snapshot) notFound();
|
||||
return (
|
||||
<>
|
||||
{process.env.BUZZ_DEMO_MODE === "true" ? null : (
|
||||
<LiveRefresh endpoint="/api/events/admin" />
|
||||
)}
|
||||
<EditorWorkspace
|
||||
key={`${snapshot.page.id}:${snapshot.page.version}`}
|
||||
snapshot={snapshot}
|
||||
navigation={navigation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
if (!sections.includes(page as (typeof sections)[number])) notFound();
|
||||
const guide = await getGuideByCharacterKey(character);
|
||||
if (!guide) notFound();
|
||||
const [catalog, content] = await Promise.all([getActiveCatalog(), getPublicGuide(guide.slug)]);
|
||||
if (!catalog || !content) notFound();
|
||||
return <main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6 lg:p-8"><header className="flex items-center gap-4"><Button variant="outline" size="icon" render={<Link href={`/admin/${character}`} />} nativeButton={false} aria-label="กลับหน้า Guide"><ArrowLeftIcon /></Button><div><p className="text-sm text-muted-foreground">{guide.name}</p><h1 className="font-heading text-3xl font-semibold">{guide.sections.find((section) => section.slug === page)?.title ?? page}</h1></div></header><GuideSectionEditor section={page as (typeof sections)[number]} guide={guide} weapons={catalog.weapons} artifacts={catalog.artifacts} characters={catalog.characters} initial={{ weapons: content.weapons, artifactGroups: content.artifactGroups.map((group) => ({ id: group.id, name: group.name, rows: group.rows.map((row) => ({ firstArtifactKey: row.firstArtifactKey, secondArtifactKey: row.secondArtifactKey, percent: row.percent })) })), artifactProfile: content.artifactProfile, constellations: content.constellations, teams: content.teams.map((team) => ({ title: team.title, rotationSeconds: team.rotationSeconds, combo: team.combo, members: team.members.map((member) => ({ characterKey: member.characterKey, damage: member.damage })) })), extras: content.extras.map((section) => ({ title: section.title, rows: section.rows.map((row) => ({ mediaId: row.mediaId, heading: row.heading, body: row.body, percent: row.percent, imageUrl: row.imageUrl })) })) }} /></main>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeftIcon, BoxesIcon, GemIcon, SparklesIcon, SwordsIcon, UsersIcon } from "lucide-react";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { GuideOverviewForm } from "@/components/admin/guide-overview-form";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
import { getGuideByCharacterKey } from "@/lib/guides/queries";
|
||||
|
||||
const sections = [
|
||||
{ slug: "weapon", title: "Weapons", description: "จัดอันดับอาวุธ Refinement และเปอร์เซ็นต์", icon: SwordsIcon },
|
||||
{ slug: "artifact", title: "Artifacts & Stats", description: "ชุด Artifact, Main/Sub Stats และ Talent", icon: GemIcon },
|
||||
{ slug: "constellations", title: "Constellations", description: "รายละเอียด C1–C6 และเปอร์เซ็นต์สะสม", icon: SparklesIcon },
|
||||
{ slug: "team", title: "Team Calc", description: "ตัวละคร 4 ตัว Damage share, DPS และ Combo", icon: UsersIcon },
|
||||
];
|
||||
|
||||
export default async function GuideOverviewPage({ params }: { params: Promise<{ character: string }> }) {
|
||||
await connection();
|
||||
if (!(await getAdminSession())) redirect("/admin/login");
|
||||
const { character } = await params;
|
||||
const guide = await getGuideByCharacterKey(character);
|
||||
if (!guide) notFound();
|
||||
return <main className="mx-auto flex w-full max-w-6xl flex-col gap-8 p-4 sm:p-6 lg:p-8"><header className="flex items-center gap-4"><Button variant="outline" size="icon" render={<Link href="/admin" />} nativeButton={false} aria-label="กลับ Dashboard"><ArrowLeftIcon /></Button><div className="relative size-16 overflow-hidden rounded-lg border"><Image src={guide.coverUrl} alt={guide.name} fill className="object-cover" /></div><div><p className="text-sm text-muted-foreground">Where do you want to edit?</p><h1 className="font-heading text-3xl font-semibold">{guide.name}</h1></div></header><GuideOverviewForm guide={guide} /><div className="grid gap-4 md:grid-cols-2">{sections.map(({ slug, title, description, icon: Icon }) => <Link key={slug} href={`/admin/${guide.characterKey}/${slug}`} className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"><Card className="h-full"><CardHeader><Icon className="size-6 text-primary" /><CardTitle>{title}</CardTitle><CardDescription>{description}</CardDescription></CardHeader></Card></Link>)}<Link href={`/admin/${guide.characterKey}/extra`} className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"><Card className="h-full"><CardHeader><BoxesIcon className="size-6 text-primary" /><CardTitle>Extra sections</CardTitle><CardDescription>ส่วนเสริมแบบภาพ หัวข้อ ข้อความสั้น และเปอร์เซ็นต์</CardDescription></CardHeader></Card></Link></div></main>;
|
||||
}
|
||||
@@ -14,6 +14,19 @@ import type { DataSourceSnapshot, PageSnapshot } from "@/lib/content/types";
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { builtInTemplates } from "@/lib/templates";
|
||||
import { persistTemplateApplication } from "@/lib/templates/persist";
|
||||
import {
|
||||
createGuide,
|
||||
enqueueCatalogSync,
|
||||
permanentlyDeleteGuide,
|
||||
restoreGuide,
|
||||
saveArtifacts,
|
||||
saveConstellations,
|
||||
saveExtraSections,
|
||||
saveTeams,
|
||||
saveWeapons,
|
||||
trashGuide,
|
||||
updateGuideBasics,
|
||||
} from "@/lib/guides/mutations";
|
||||
|
||||
const serializedBlockSchema = z.strictObject({
|
||||
id: z.string().uuid(),
|
||||
@@ -188,3 +201,84 @@ export async function applyEditorTemplate(input: unknown): Promise<
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type GuideActionResult =
|
||||
| { status: "saved"; version?: number; location?: string }
|
||||
| { status: "error"; message: string };
|
||||
|
||||
async function runGuideAction(action: () => Promise<{ version?: number }>): Promise<GuideActionResult> {
|
||||
try {
|
||||
const result = await action();
|
||||
return { status: "saved", version: result.version };
|
||||
} catch (cause) {
|
||||
if (cause instanceof z.ZodError) {
|
||||
return { status: "error", message: cause.issues.map((issue) => issue.message).join(" • ") };
|
||||
}
|
||||
return { status: "error", message: cause instanceof Error ? cause.message : "บันทึกไม่สำเร็จ" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createStructuredGuide(input: unknown): Promise<GuideActionResult> {
|
||||
await requireAdmin();
|
||||
try {
|
||||
const guide = await createGuide(input);
|
||||
return { status: "saved", version: guide.version, location: `/admin/${guide.characterKey}` };
|
||||
} catch (cause) {
|
||||
return { status: "error", message: cause instanceof Error ? cause.message : "สร้าง Guide ไม่สำเร็จ" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveWeaponSection(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => saveWeapons(guideId, input));
|
||||
}
|
||||
|
||||
export async function saveGuideOverview(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => updateGuideBasics(guideId, input));
|
||||
}
|
||||
|
||||
export async function saveArtifactSection(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => saveArtifacts(guideId, input));
|
||||
}
|
||||
|
||||
export async function saveConstellationSection(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => saveConstellations(guideId, input));
|
||||
}
|
||||
|
||||
export async function saveTeamSection(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => saveTeams(guideId, input));
|
||||
}
|
||||
|
||||
export async function saveExtraSectionList(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => saveExtraSections(guideId, input));
|
||||
}
|
||||
|
||||
export async function moveGuideToTrash(guideId: string) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => trashGuide(guideId));
|
||||
}
|
||||
|
||||
export async function restoreStructuredGuide(guideId: string) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => restoreGuide(guideId));
|
||||
}
|
||||
|
||||
export async function deleteStructuredGuide(guideId: string) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => permanentlyDeleteGuide(guideId));
|
||||
}
|
||||
|
||||
export async function startCatalogSync() {
|
||||
const admin = await requireAdmin();
|
||||
try {
|
||||
const job = await enqueueCatalogSync(admin.user.id);
|
||||
return { status: "saved" as const, jobId: job.id };
|
||||
} catch (cause) {
|
||||
return { status: "error" as const, message: cause instanceof Error ? cause.message : "เริ่ม Sync ไม่สำเร็จ" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { CreateGuideFlow } from "@/components/admin/create-guide-flow";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
import { getActiveCatalog, listGuideCards } from "@/lib/guides/queries";
|
||||
|
||||
export default async function CreateGuidePage() {
|
||||
await connection();
|
||||
if (!(await getAdminSession())) redirect("/admin/login");
|
||||
const [catalog, guides] = await Promise.all([getActiveCatalog(), listGuideCards()]);
|
||||
if (!catalog) redirect("/admin?sync=required");
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 p-4 sm:p-6 lg:p-8">
|
||||
<header className="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" render={<Link href="/admin" />} nativeButton={false} aria-label="กลับ Dashboard"><ArrowLeftIcon /></Button>
|
||||
<div><p className="text-sm text-muted-foreground">Catalog {catalog.version}</p><h1 className="font-heading text-3xl font-semibold">สร้าง Guide ใหม่</h1></div>
|
||||
</header>
|
||||
<CreateGuideFlow characters={catalog.characters} existingKeys={guides.map((guide) => guide.characterKey)} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+12
-80
@@ -1,91 +1,23 @@
|
||||
import Link from "next/link";
|
||||
import { EyeIcon, FileTextIcon, LayoutTemplateIcon, PlusIcon } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { AdminSidebar } from "@/components/admin/admin-sidebar";
|
||||
import { LiveRefresh } from "@/components/live-refresh";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { StructuredDashboard } from "@/components/admin/structured-dashboard";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
import { loadAdminNavigation } from "@/lib/content/loaders";
|
||||
import { getActiveCatalog, getLatestCatalogSyncJob, listGuideCards } from "@/lib/guides/queries";
|
||||
|
||||
export default async function AdminPage() {
|
||||
await connection();
|
||||
if (!(await getAdminSession())) redirect("/admin/login");
|
||||
const navigation = await loadAdminNavigation();
|
||||
|
||||
const [guides, trash, syncJob, catalog] = await Promise.all([
|
||||
listGuideCards(),
|
||||
listGuideCards(true),
|
||||
getLatestCatalogSyncJob(),
|
||||
getActiveCatalog(),
|
||||
]);
|
||||
return (
|
||||
<SidebarProvider>
|
||||
{process.env.BUZZ_DEMO_MODE === "true" ? null : (
|
||||
<LiveRefresh endpoint="/api/events/admin" />
|
||||
)}
|
||||
<AdminSidebar navigation={navigation} />
|
||||
<SidebarInset>
|
||||
<header className="flex min-h-14 items-center gap-3 border-b px-4">
|
||||
<SidebarTrigger />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="font-heading font-semibold">ภาพรวมเนื้อหา</h1>
|
||||
<p className="text-xs text-muted-foreground">ตัวละคร หน้า และสถานะการเผยแพร่</p>
|
||||
</div>
|
||||
<Button variant="outline" render={<Link href="/admin/templates" />}>
|
||||
<LayoutTemplateIcon />เทมเพลต
|
||||
</Button>
|
||||
</header>
|
||||
<main className="mx-auto w-full max-w-6xl space-y-8 p-4 sm:p-6 lg:p-8">
|
||||
<div className="flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-primary">คลังคู่มือ</p>
|
||||
<h2 className="mt-1 font-heading text-2xl font-semibold">ตัวละครทั้งหมด</h2>
|
||||
</div>
|
||||
<Button render={<Link href="/admin/templates" />}><PlusIcon />สร้างจากเทมเพลต</Button>
|
||||
</div>
|
||||
{navigation.characters.length > 0 ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{navigation.characters.map((character) => (
|
||||
<Card key={character.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{character.nameTh}</CardTitle>
|
||||
<CardDescription>{character.nameEn ?? character.slug}</CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant={character.visible ? "default" : "outline"}>
|
||||
{character.visible ? "เผยแพร่" : "ซ่อน"}
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{character.pages.map((page) => (
|
||||
<div key={page.id} className="flex min-h-11 items-center gap-2 rounded-lg border px-3">
|
||||
<FileTextIcon className="size-4 text-muted-foreground" />
|
||||
<Link href={`/admin/${character.slug}/${page.slug}`} className="min-w-0 flex-1 truncate text-sm font-medium hover:text-primary">
|
||||
{page.title}
|
||||
</Link>
|
||||
<Badge variant="outline">v{page.version}</Badge>
|
||||
{page.visible && character.visible ? (
|
||||
<Button variant="ghost" size="icon-xs" render={<Link href={`/${character.slug}/${page.slug}`} target="_blank" />} aria-label={`เปิด ${page.title}`}>
|
||||
<EyeIcon />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="border-dashed"><CardContent className="grid min-h-48 place-items-center text-center text-muted-foreground">ยังไม่มีตัวละคร เริ่มต้นด้วยเทมเพลตที่เตรียมไว้</CardContent></Card>
|
||||
)}
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 p-4 sm:p-6 lg:p-8">
|
||||
<header><p className="text-sm font-medium text-primary">Buzz Sheet Admin</p><h1 className="font-heading text-3xl font-semibold">Guide Dashboard</h1><p className="mt-2 text-muted-foreground">จัดการ Guide แบบภาพ-first และบันทึกขึ้นเว็บไซต์ทันที</p></header>
|
||||
<StructuredDashboard guides={guides} trash={trash} syncJob={syncJob} catalogVersion={catalog?.version ?? null} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { uploadPublicMedia } from "@/lib/media/repository";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const admin = await requireAdmin();
|
||||
const form = await request.formData();
|
||||
const file = form.get("file");
|
||||
if (!(file instanceof File)) {
|
||||
return NextResponse.json({ error: "กรุณาเลือกไฟล์รูปภาพ" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
return NextResponse.json(await uploadPublicMedia(file, admin.user.id), { status: 201 });
|
||||
} catch (cause) {
|
||||
return NextResponse.json(
|
||||
{ error: cause instanceof Error ? cause.message : "อัปโหลดไม่สำเร็จ" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-25
@@ -1,32 +1,12 @@
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { CharacterDirectory } from "@/components/public/character-directory";
|
||||
import { SiteHeader } from "@/components/public/site-header";
|
||||
import { LiveRefresh } from "@/components/live-refresh";
|
||||
import { loadPublicDirectory } from "@/lib/content/loaders";
|
||||
import { StructuredDirectory } from "@/components/public/structured-directory";
|
||||
import { SiteHeader } from "@/components/public/site-header";
|
||||
import { listGuideCards } from "@/lib/guides/queries";
|
||||
|
||||
export default async function HomePage() {
|
||||
await connection();
|
||||
const characters = await loadPublicDirectory();
|
||||
|
||||
return (
|
||||
<div className="min-h-svh bg-background">
|
||||
{process.env.BUZZ_DEMO_MODE === "true" ? null : (
|
||||
<LiveRefresh endpoint="/api/events/directory" />
|
||||
)}
|
||||
<SiteHeader />
|
||||
<main className="mx-auto w-full max-w-7xl px-4 py-10 sm:px-6 sm:py-14">
|
||||
<header className="mb-8 max-w-3xl space-y-3">
|
||||
<p className="text-sm font-medium text-primary">คลังคู่มือตัวละคร</p>
|
||||
<h1 className="font-heading text-3xl font-semibold tracking-tight sm:text-4xl">
|
||||
หาคู่มือที่ต้องการ แล้วเริ่มจัดทีมได้เลย
|
||||
</h1>
|
||||
<p className="text-base leading-7 text-muted-foreground sm:text-lg">
|
||||
ค้นหาตามชื่อ ธาตุ หรือบทบาท ทุกหน้าจัดรูปแบบมาให้อ่านง่ายทั้งมือถือและเดสก์ท็อป
|
||||
</p>
|
||||
</header>
|
||||
<CharacterDirectory characters={characters} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
const guides = await listGuideCards();
|
||||
return <div className="min-h-svh bg-background"><LiveRefresh endpoint="/api/events/directory" /><SiteHeader /><main className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 py-10 sm:px-6 sm:py-14"><header className="max-w-3xl"><p className="text-sm font-medium text-primary">Genshin Impact Build Guides</p><h1 className="mt-2 font-heading text-4xl font-semibold">เลือกตัวละคร แล้วจัด Build และ Team ได้เลย</h1><p className="mt-3 text-lg leading-8 text-muted-foreground">คู่มือแบบภาพ-first สำหรับ Weapons, Artifacts, Constellations และ Team Calc</p></header><StructuredDirectory guides={guides} /></main></div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user