feat : update
This commit is contained in:
@@ -21,7 +21,6 @@ const sections = [
|
||||
"artifact",
|
||||
"constellations",
|
||||
"team",
|
||||
"extra",
|
||||
] as const;
|
||||
|
||||
export const instant = false;
|
||||
@@ -34,9 +33,22 @@ export default async function StructuredEditorPage({
|
||||
await connection();
|
||||
if (!(await getAdminSession())) redirect("/admin/login");
|
||||
const { character, page } = await params;
|
||||
if (!sections.includes(page as (typeof sections)[number])) notFound();
|
||||
const guide = await getGuideByCharacterKey(character);
|
||||
if (!guide) notFound();
|
||||
const extraSections = guide.sections.filter((section) => section.kind === "extra");
|
||||
if (page === "extra") {
|
||||
redirect(
|
||||
extraSections[0]
|
||||
? `/admin/${character}/${extraSections[0].slug}`
|
||||
: `/admin/${character}`,
|
||||
);
|
||||
}
|
||||
const fixedSection = sections.includes(page as (typeof sections)[number]);
|
||||
const extraSection = extraSections.find((section) => section.slug === page);
|
||||
if (!fixedSection && !extraSection) notFound();
|
||||
const editorSection = fixedSection
|
||||
? page as (typeof sections)[number]
|
||||
: "extra" as const;
|
||||
const [catalog, content] = await Promise.all([
|
||||
getActiveCatalog(),
|
||||
getAdminGuideContent(guide.slug),
|
||||
@@ -65,18 +77,20 @@ export default async function StructuredEditorPage({
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground">{guide.name}</p>
|
||||
<h1 className="whitespace-nowrap font-heading text-3xl font-semibold">
|
||||
{guide.sections.find((section) => section.slug === page)?.title ??
|
||||
page}
|
||||
{extraSection?.title ?? guide.sections.find((section) => section.slug === page)?.title ?? page}
|
||||
</h1>
|
||||
<EditorPresence guideId={guide.id} />
|
||||
</div>
|
||||
</header>
|
||||
<GuideSectionNav
|
||||
guideId={guide.id}
|
||||
characterKey={guide.characterKey}
|
||||
current={page as (typeof sections)[number]}
|
||||
current={page}
|
||||
extras={extraSections.map(({ id, slug, title }) => ({ id, slug, title }))}
|
||||
/>
|
||||
<GuideSectionEditor
|
||||
section={page as (typeof sections)[number]}
|
||||
section={editorSection}
|
||||
extraSectionId={extraSection?.id}
|
||||
guide={guide}
|
||||
weapons={catalog.weapons}
|
||||
artifacts={catalog.artifacts}
|
||||
@@ -119,7 +133,7 @@ export default async function StructuredEditorPage({
|
||||
damage: member.damage,
|
||||
})),
|
||||
})),
|
||||
extras: content.extras.map((section) => ({
|
||||
extras: content.extras.filter((section) => !extraSection || section.id === extraSection.id).map((section) => ({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
rows: section.rows.map((row) => ({
|
||||
|
||||
@@ -4,13 +4,14 @@ import { ArrowLeftIcon, ArrowUpRightIcon } from "lucide-react";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { AdminHeader } from "@/components/admin/admin-header";
|
||||
import { EditorPresence } from "@/components/admin/editor-presence";
|
||||
import { ExtraOverview } from "@/components/admin/guide-extra-controls";
|
||||
import { GuideOverviewForm } from "@/components/admin/guide-overview-form";
|
||||
import { GuideSectionNav } from "@/components/admin/guide-section-nav";
|
||||
import { GuideVisibilityControl } from "@/components/admin/guide-visibility-control";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardAction, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminHeader } from "@/components/admin/admin-header";
|
||||
import { GuideOverviewForm } from "@/components/admin/guide-overview-form";
|
||||
import { GuideVisibilityControl } from "@/components/admin/guide-visibility-control";
|
||||
import { GuideSectionNav } from "@/components/admin/guide-section-nav";
|
||||
import { EditorPresence } from "@/components/admin/editor-presence";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
import { getGuideByCharacterKey } from "@/lib/guides/queries";
|
||||
import { navigationIconPath } from "@/lib/navigation";
|
||||
@@ -28,5 +29,46 @@ export default async function GuideOverviewPage({ params }: { params: Promise<{
|
||||
const { character } = await params;
|
||||
const guide = await getGuideByCharacterKey(character);
|
||||
if (!guide) notFound();
|
||||
return <div className="min-h-svh"><AdminHeader /><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 flex-wrap 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 className="min-w-0 flex-1"><p className="text-sm text-muted-foreground">เลือกส่วนที่ต้องการแก้ไข</p><h1 className="font-heading text-3xl font-semibold">{guide.name}</h1><EditorPresence guideId={guide.id} /></div><GuideVisibilityControl guideId={guide.id} guideName={guide.name} isPublic={guide.isPublic} /></header><GuideSectionNav characterKey={guide.characterKey} current="overview" /><GuideOverviewForm guide={guide} /><div className="grid gap-4 md:grid-cols-2">{sections.map(({ slug, title, description }) => <Link key={slug} href={`/admin/${guide.characterKey}/${slug}`} prefetch className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"><Card className="h-full transition-colors hover:bg-muted/50"><CardHeader>{navigationIconPath(slug) ? <Image src={navigationIconPath(slug)!} alt="" width={40} height={40} className="size-10 object-contain" /> : null}<CardTitle>{title}</CardTitle><CardDescription>{description}</CardDescription><CardAction><ArrowUpRightIcon aria-hidden="true" className="size-5 text-primary" /></CardAction></CardHeader></Card></Link>)}<Link href={`/admin/${guide.characterKey}/extra`} prefetch className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"><Card className="h-full transition-colors hover:bg-muted/50"><CardHeader><Image src={navigationIconPath("extra")!} alt="" width={40} height={40} className="size-10 object-contain" /><CardTitle>Extra sections</CardTitle><CardDescription>ส่วนเสริมแบบภาพ หัวข้อ ข้อความสั้น และเปอร์เซ็นต์</CardDescription><CardAction><ArrowUpRightIcon aria-hidden="true" className="size-5 text-primary" /></CardAction></CardHeader></Card></Link></div></main></div>;
|
||||
const extras = guide.sections
|
||||
.filter((section) => section.kind === "extra")
|
||||
.map(({ id, slug, title }) => ({ id, slug, title }));
|
||||
|
||||
return (
|
||||
<div className="min-h-svh">
|
||||
<AdminHeader />
|
||||
<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 flex-wrap 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 className="min-w-0 flex-1">
|
||||
<p className="text-sm text-muted-foreground">เลือกส่วนที่ต้องการแก้ไข</p>
|
||||
<h1 className="font-heading text-3xl font-semibold">{guide.name}</h1>
|
||||
<EditorPresence guideId={guide.id} />
|
||||
</div>
|
||||
<GuideVisibilityControl guideId={guide.id} guideName={guide.name} isPublic={guide.isPublic} />
|
||||
</header>
|
||||
<GuideSectionNav guideId={guide.id} characterKey={guide.characterKey} current="overview" extras={extras} />
|
||||
<GuideOverviewForm guide={guide} />
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{sections.map(({ slug, title, description }) => (
|
||||
<Link key={slug} href={`/admin/${guide.characterKey}/${slug}`} prefetch className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<Card className="h-full transition-colors hover:bg-muted/50">
|
||||
<CardHeader>
|
||||
{navigationIconPath(slug) ? <Image src={navigationIconPath(slug)!} alt="" width={40} height={40} className="size-10 object-contain" /> : null}
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
<CardAction><ArrowUpRightIcon aria-hidden="true" className="size-5 text-primary" /></CardAction>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<ExtraOverview guideId={guide.id} characterKey={guide.characterKey} initialVersion={guide.version} initialExtras={extras} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,17 @@ import { requireAdmin } from "@/lib/auth/server";
|
||||
import { builtInTemplates } from "@/lib/templates";
|
||||
import { persistTemplateApplication } from "@/lib/templates/persist";
|
||||
import {
|
||||
createExtraSection,
|
||||
createGuide,
|
||||
deleteExtraSection,
|
||||
GuideVersionConflictError,
|
||||
permanentlyDeleteGuide,
|
||||
reorderExtraSections,
|
||||
restoreGuide,
|
||||
runCatalogSync,
|
||||
saveArtifacts,
|
||||
saveConstellations,
|
||||
saveExtraSection,
|
||||
saveExtraSections,
|
||||
saveTeams,
|
||||
setGuideVisibility,
|
||||
@@ -274,6 +278,64 @@ export async function saveExtraSectionList(guideId: string, input: unknown) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function createGuideExtra(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
try {
|
||||
const section = await createExtraSection(z.string().uuid().parse(guideId), input);
|
||||
updateTag(pageCacheTag(guideId));
|
||||
return {
|
||||
status: "saved" as const,
|
||||
version: section.version,
|
||||
section: { id: section.id, slug: section.slug, title: section.title },
|
||||
};
|
||||
} catch (cause) {
|
||||
return {
|
||||
status: "error" as const,
|
||||
message: cause instanceof z.ZodError
|
||||
? cause.issues.map((issue) => issue.message).join(" • ")
|
||||
: cause instanceof Error
|
||||
? cause.message
|
||||
: "สร้าง Extra section ไม่สำเร็จ",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveGuideExtra(
|
||||
guideId: string,
|
||||
sectionId: string,
|
||||
input: unknown,
|
||||
) {
|
||||
await requireAdmin();
|
||||
const parsedGuideId = z.string().uuid().parse(guideId);
|
||||
const parsedSectionId = z.string().uuid().parse(sectionId);
|
||||
const result = await runGuideAction(() =>
|
||||
saveExtraSection(parsedGuideId, parsedSectionId, input),
|
||||
);
|
||||
if (result.status === "saved") updateTag(pageCacheTag(guideId));
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteGuideExtra(guideId: string, sectionId: string) {
|
||||
await requireAdmin();
|
||||
const result = await runGuideAction(() =>
|
||||
deleteExtraSection(
|
||||
z.string().uuid().parse(guideId),
|
||||
z.string().uuid().parse(sectionId),
|
||||
),
|
||||
);
|
||||
if (result.status === "saved") updateTag(pageCacheTag(guideId));
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function reorderGuideExtras(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
const result = await runGuideAction(() =>
|
||||
reorderExtraSections(z.string().uuid().parse(guideId), input),
|
||||
);
|
||||
if (result.status === "saved") updateTag(pageCacheTag(guideId));
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function moveGuideToTrash(guideId: string) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => trashGuide(guideId));
|
||||
|
||||
Reference in New Issue
Block a user