feat(public): render responsive character guides

This commit is contained in:
2026-08-28 19:21:57 +00:00 Unverified
parent 139a9081ed
commit 6c9152eebd
15 changed files with 1429 additions and 61 deletions
+17
View File
@@ -0,0 +1,17 @@
import { SiteHeader } from "@/components/public/site-header";
import { Skeleton } from "@/components/ui/skeleton";
export default function GuideLoading() {
return (
<div className="min-h-svh">
<SiteHeader />
<div className="border-b p-4"><Skeleton className="mx-auto h-16 max-w-7xl" /></div>
<main className="mx-auto max-w-5xl space-y-5 px-4 py-10">
<Skeleton className="h-72 w-full" />
<Skeleton className="h-8 w-1/3" />
<Skeleton className="h-28 w-full" />
<Skeleton className="h-80 w-full" />
</main>
</div>
);
}
+28
View File
@@ -0,0 +1,28 @@
import type { Metadata } from "next";
import { notFound, permanentRedirect } from "next/navigation";
import { connection } from "next/server";
import { GuideShell } from "@/components/public/guide-shell";
import { loadPublicPage } from "@/lib/content/loaders";
type RouteParams = Promise<{ character: string; page: string }>;
export async function generateMetadata({ params }: { params: RouteParams }): 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,
};
}
export default async function GuidePage({ params }: { params: RouteParams }) {
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} />;
}
+16
View File
@@ -0,0 +1,16 @@
import { notFound, permanentRedirect } from "next/navigation";
import { connection } from "next/server";
import { loadPublicPage } from "@/lib/content/loaders";
export default async function CharacterPage({
params,
}: {
params: Promise<{ character: string }>;
}) {
await connection();
const { character } = await params;
const result = await loadPublicPage(character);
if (result.type === "redirect") permanentRedirect(result.location);
notFound();
}
+19
View File
@@ -0,0 +1,19 @@
import { DirectorySkeleton } from "@/components/public/directory-skeleton";
import { SiteHeader } from "@/components/public/site-header";
import { Skeleton } from "@/components/ui/skeleton";
export default function Loading() {
return (
<div className="min-h-svh">
<SiteHeader />
<main className="mx-auto w-full max-w-7xl space-y-8 px-4 py-10 sm:px-6 sm:py-14">
<div className="space-y-3">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-10 w-full max-w-2xl" />
<Skeleton className="h-6 w-full max-w-xl" />
</div>
<DirectorySkeleton />
</main>
</div>
);
}
+35
View File
@@ -0,0 +1,35 @@
import Link from "next/link";
import { MapPinOffIcon } from "lucide-react";
import { SiteHeader } from "@/components/public/site-header";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
export default function NotFound() {
return (
<div className="min-h-svh">
<SiteHeader />
<main className="mx-auto grid min-h-[calc(100svh-4rem)] max-w-3xl place-items-center px-4 py-16">
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon"><MapPinOffIcon aria-hidden="true" /></EmptyMedia>
<EmptyTitle>ไม่พบคู่มือนี้</EmptyTitle>
<EmptyDescription>
หน้านี้อาจถูกซ่อน เปลี่ยนชื่อ หรือยังไม่มีเนื้อหาที่เผยแพร่
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button render={<Link href="/" />}>กลับไปหน้ารวมตัวละคร</Button>
</EmptyContent>
</Empty>
</main>
</div>
);
}
+20 -61
View File
@@ -1,68 +1,27 @@
import Image from "next/image";
import { connection } from "next/server";
import { CharacterDirectory } from "@/components/public/character-directory";
import { SiteHeader } from "@/components/public/site-header";
import { loadPublicDirectory } from "@/lib/content/loaders";
export default async function HomePage() {
await connection();
const characters = await loadPublicDirectory();
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert h-5 w-[100px]"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the{" "}
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
page.tsx
</code>{" "}
file.
<div className="min-h-svh bg-background">
<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="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
<p className="text-base leading-7 text-muted-foreground sm:text-lg">
ค้นหาตามชื่อ ธาตุ หรือบทบาท ทุกหน้าจัดรูปแบบมาให้อ่านง่ายทั้งมือถือและเดสก์ท็อป
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert h-[14px] w-4"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={14}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</header>
<CharacterDirectory characters={characters} />
</main>
</div>
);