140 lines
5.9 KiB
TypeScript
140 lines
5.9 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import Link from "next/link";
|
|
import { ArrowLeftIcon } from "lucide-react";
|
|
|
|
import Image from "@/components/ui/resilient-image";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { SiteHeader } from "@/components/public/site-header";
|
|
import { SiteFooter } from "@/components/public/site-footer";
|
|
import { ScrollableNav } from "@/components/public/scrollable-nav";
|
|
import { LiveRefresh } from "@/components/live-refresh";
|
|
import type { PublicGuideSummary } from "@/lib/guides/queries";
|
|
import { commentNavigationSection } from "@/lib/comments/navigation";
|
|
import { navigationIconPath } from "@/lib/navigation";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export function publicSectionTitle(slug: string, fallback: string): string {
|
|
if (slug === "weapon") return "Weapons";
|
|
if (slug === "artifact") return "Artifacts";
|
|
if (slug === "constellations") return "Constellations";
|
|
if (slug === "team") return "Team calc";
|
|
if (slug === "comment") return "Comment";
|
|
return fallback;
|
|
}
|
|
|
|
export function GuidePageShell({
|
|
guide,
|
|
sections: guideSections,
|
|
section,
|
|
preview = false,
|
|
children,
|
|
}: {
|
|
guide: Pick<PublicGuideSummary, "id" | "slug" | "name" | "characterKey" | "coverUrl">;
|
|
sections: Pick<PublicGuideSummary["sections"][number], "id" | "slug" | "title">[];
|
|
section?: string;
|
|
preview?: boolean;
|
|
children: ReactNode;
|
|
}) {
|
|
const sections = preview ? guideSections : [...guideSections, commentNavigationSection(guide.id)];
|
|
const baseHref = preview ? `/admin/${guide.characterKey}/preview` : `/${guide.slug}`;
|
|
const currentPageTitle = section
|
|
? publicSectionTitle(section, sections.find((item) => item.slug === section)?.title ?? section)
|
|
: "Overview";
|
|
return (
|
|
<div className="flex min-h-svh flex-col bg-background">
|
|
<LiveRefresh endpoint={`/api/events/page/${guide.id}`} />
|
|
<SiteHeader editHref={`/admin/${guide.characterKey}`} guideId={guide.id} />
|
|
{preview ? (
|
|
<div className="mx-auto w-full max-w-7xl px-4 pt-4 sm:px-6 lg:px-8">
|
|
<Alert>
|
|
<AlertTitle>Preview</AlertTitle>
|
|
<AlertDescription>หน้านี้แสดงข้อมูลที่บันทึกล่าสุดและเปิดดูได้เฉพาะผู้ดูแล</AlertDescription>
|
|
</Alert>
|
|
</div>
|
|
) : null}
|
|
<div className="sticky top-0 z-40 bg-background/95 supports-backdrop-filter:backdrop-blur">
|
|
<div className="mx-auto flex min-h-12 w-full max-w-7xl items-center gap-3 px-4 sm:min-h-16 sm:px-6 lg:px-8">
|
|
<Button variant="ghost" size="icon" className="max-sm:size-11" render={<Link href={preview ? `/admin/${guide.characterKey}` : "/"} />} nativeButton={false} aria-label={preview ? "กลับหน้าแก้ไข" : "กลับหน้ารวม"}>
|
|
<ArrowLeftIcon />
|
|
</Button>
|
|
<div className="relative size-10 shrink-0 overflow-hidden rounded-lg border sm:size-16">
|
|
<Image
|
|
src={guide.coverUrl}
|
|
alt={guide.name}
|
|
fill
|
|
sizes="(min-width: 640px) 64px, 40px"
|
|
className="object-cover"
|
|
/>
|
|
</div>
|
|
<div className="flex min-w-0 flex-1 flex-col items-start gap-1">
|
|
<p className="truncate text-sm text-muted-foreground">
|
|
{guide.name}
|
|
</p>
|
|
<p className="max-w-full truncate font-heading text-lg font-semibold sm:text-2xl">
|
|
{currentPageTitle}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
<ScrollableNav
|
|
ariaLabel={`เมนูคู่มือ ${guide.name}`}
|
|
activeKey={section ?? "overview"}
|
|
className="min-h-11 w-full rounded-xl border bg-card p-1 sm:p-2"
|
|
>
|
|
<Link
|
|
href={baseHref}
|
|
aria-current={!section ? "page" : undefined}
|
|
className={cn(
|
|
"flex min-h-11 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
!section
|
|
? "bg-primary/15 font-semibold text-primary"
|
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
)}
|
|
prefetch={false}
|
|
>
|
|
<Image
|
|
src={navigationIconPath("overview")!}
|
|
alt=""
|
|
width={20}
|
|
height={20}
|
|
className="size-5 object-contain"
|
|
/>
|
|
Overview
|
|
</Link>
|
|
{sections.map((item) => (
|
|
<Link
|
|
key={item.id}
|
|
href={`${baseHref}/${item.slug}`}
|
|
aria-current={section === item.slug ? "page" : undefined}
|
|
className={cn(
|
|
"flex min-h-11 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
section === item.slug
|
|
? "bg-primary/15 font-semibold text-primary"
|
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
)}
|
|
prefetch={false}
|
|
>
|
|
{navigationIconPath(item.slug) ? (
|
|
<Image
|
|
src={navigationIconPath(item.slug)!}
|
|
alt=""
|
|
width={20}
|
|
height={20}
|
|
className="size-5 object-contain"
|
|
/>
|
|
) : null}
|
|
{publicSectionTitle(item.slug, item.title)}
|
|
</Link>
|
|
))}
|
|
</ScrollableNav>
|
|
</div>
|
|
</div>
|
|
<main className="mx-auto w-full max-w-7xl p-4 sm:p-6 lg:p-10">
|
|
{children}
|
|
</main>
|
|
<SiteFooter />
|
|
</div>
|
|
);
|
|
}
|