feat : update
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
@@ -6,6 +7,7 @@ import { connection } from "next/server";
|
||||
import { GuideSectionEditor } from "@/components/admin/guide-section-editor";
|
||||
import { GuideSectionNav } from "@/components/admin/guide-section-nav";
|
||||
import { AdminHeader } from "@/components/admin/admin-header";
|
||||
import { EditorPresence } from "@/components/admin/editor-presence";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
import {
|
||||
@@ -55,12 +57,16 @@ export default async function StructuredEditorPage({
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<div className="relative size-16 shrink-0 overflow-hidden rounded-lg border">
|
||||
<Image src={guide.coverUrl} alt={guide.name} fill className="object-cover" />
|
||||
</div>
|
||||
<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}
|
||||
</h1>
|
||||
<EditorPresence guideId={guide.id} />
|
||||
</div>
|
||||
</header>
|
||||
<GuideSectionNav
|
||||
|
||||
@@ -10,6 +10,7 @@ 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";
|
||||
@@ -27,5 +28,5 @@ 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></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>;
|
||||
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>;
|
||||
}
|
||||
|
||||
+37
-1
@@ -4,7 +4,11 @@ import { updateTag } from "next/cache";
|
||||
import * as z from "zod";
|
||||
|
||||
import type { AutosaveResponse } from "@/lib/editor/autosave";
|
||||
import { pageCacheTag } from "@/lib/cache/tags";
|
||||
import { GLOSSARY_CACHE_TAG, pageCacheTag } from "@/lib/cache/tags";
|
||||
import {
|
||||
createGlossaryAlias,
|
||||
deleteGlossaryAlias,
|
||||
} from "@/lib/glossary-repository";
|
||||
import {
|
||||
createNamedRevision,
|
||||
saveDataSourceVersion,
|
||||
@@ -301,3 +305,35 @@ export async function startCatalogSync() {
|
||||
return { status: "error" as const, message: cause instanceof Error ? cause.message : "เริ่ม Sync ไม่สำเร็จ" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function addGlossaryAlias(input: unknown) {
|
||||
await requireAdmin();
|
||||
try {
|
||||
const alias = await createGlossaryAlias(input);
|
||||
updateTag(GLOSSARY_CACHE_TAG);
|
||||
return { status: "saved" as const, alias };
|
||||
} catch (cause) {
|
||||
const message = cause instanceof Error && cause.message.includes("unique")
|
||||
? "Shortcut นี้ถูกใช้งานแล้ว"
|
||||
: cause instanceof z.ZodError
|
||||
? cause.issues.map((issue) => issue.message).join(" • ")
|
||||
: cause instanceof Error
|
||||
? cause.message
|
||||
: "เพิ่ม Shortcut ไม่สำเร็จ";
|
||||
return { status: "error" as const, message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeGlossaryAlias(id: string) {
|
||||
await requireAdmin();
|
||||
try {
|
||||
await deleteGlossaryAlias(z.string().uuid().parse(id));
|
||||
updateTag(GLOSSARY_CACHE_TAG);
|
||||
return { status: "saved" as const };
|
||||
} catch (cause) {
|
||||
return {
|
||||
status: "error" as const,
|
||||
message: cause instanceof Error ? cause.message : "ลบ Shortcut ไม่สำเร็จ",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { AdminHeader } from "@/components/admin/admin-header";
|
||||
import { GlossaryManager } from "@/components/admin/glossary-manager";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
import { listGlossaryAliases } from "@/lib/glossary-repository";
|
||||
import { getActiveCatalog } from "@/lib/guides/queries";
|
||||
|
||||
export default async function GlossaryPage() {
|
||||
await connection();
|
||||
if (!(await getAdminSession())) redirect("/admin/login");
|
||||
const [catalog, aliases] = await Promise.all([
|
||||
getActiveCatalog(),
|
||||
listGlossaryAliases(),
|
||||
]);
|
||||
if (!catalog) redirect("/admin");
|
||||
return (
|
||||
<div className="min-h-svh">
|
||||
<AdminHeader />
|
||||
<main className="mx-auto flex w-full max-w-4xl flex-col gap-8 p-4 sm:p-6 lg:p-8">
|
||||
<header>
|
||||
<p className="text-sm font-medium text-primary">Search shortcuts</p>
|
||||
<h1 className="font-heading text-3xl font-semibold">Glossary Manager</h1>
|
||||
</header>
|
||||
<GlossaryManager aliases={aliases} catalogs={{
|
||||
character: catalog.characters,
|
||||
weapon: catalog.weapons,
|
||||
artifact: catalog.artifacts,
|
||||
}} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { connection } from "next/server";
|
||||
import * as z from "zod";
|
||||
|
||||
import { AdminAuthorizationError, requireAdmin } from "@/lib/auth/server";
|
||||
import { updateAdminPresence } from "@/lib/presence";
|
||||
|
||||
const inputSchema = z.strictObject({
|
||||
tabId: z.string().uuid(),
|
||||
guideId: z.string().uuid().nullable(),
|
||||
active: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
await connection();
|
||||
try {
|
||||
const admin = await requireAdmin();
|
||||
const parsed = inputSchema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return Response.json({ error: "invalid-presence" }, { status: 400 });
|
||||
return Response.json(await updateAdminPresence({
|
||||
...parsed.data,
|
||||
memberId: `${admin.session.id}:${parsed.data.tabId}`,
|
||||
user: admin.user,
|
||||
}), { headers: { "Cache-Control": "no-store" } });
|
||||
} catch (cause) {
|
||||
if (cause instanceof AdminAuthorizationError) {
|
||||
return Response.json({ error: "unauthorized" }, { status: 401 });
|
||||
}
|
||||
return Response.json({ error: "presence-unavailable" }, {
|
||||
status: 503,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { connection } from "next/server";
|
||||
import * as z from "zod";
|
||||
|
||||
import { updateVisitorPresence } from "@/lib/presence";
|
||||
|
||||
const inputSchema = z.strictObject({
|
||||
tabId: z.string().uuid(),
|
||||
guideId: z.string().uuid().nullable(),
|
||||
active: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
await connection();
|
||||
const parsed = inputSchema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return Response.json({ error: "invalid-presence" }, { status: 400 });
|
||||
try {
|
||||
return Response.json(await updateVisitorPresence(parsed.data), {
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
} catch {
|
||||
return Response.json({ error: "presence-unavailable" }, {
|
||||
status: 503,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user