test(admin): cover pages and navigation form components
This commit is contained in:
3
TODO.md
3
TODO.md
@@ -187,7 +187,7 @@ This file is the single source of truth for roadmap and delivery progress.
|
|||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
- [x] [P1] Unit tests for content schemas and service logic
|
- [x] [P1] Unit tests for content schemas and service logic
|
||||||
- [~] [P1] Component tests for admin forms (pages/media/navigation)
|
- [x] [P1] Component tests for admin forms (pages/media/navigation)
|
||||||
- [x] [P1] Integration tests for owner invariant and hidden support-user protection
|
- [x] [P1] Integration tests for owner invariant and hidden support-user protection
|
||||||
- [x] [P1] Integration tests for registration allow/deny behavior
|
- [x] [P1] Integration tests for registration allow/deny behavior
|
||||||
- [ ] [P1] Integration tests for translated content CRUD and locale-specific validation
|
- [ ] [P1] Integration tests for translated content CRUD and locale-specific validation
|
||||||
@@ -320,6 +320,7 @@ This file is the single source of truth for roadmap and delivery progress.
|
|||||||
- [2026-02-12] Added owner/support invariant integration tests for auth guards (`apps/admin/src/lib/auth/server.test.ts`), covering protected-user deletion blocking and one-owner repair/promotion rules.
|
- [2026-02-12] Added owner/support invariant integration tests for auth guards (`apps/admin/src/lib/auth/server.test.ts`), covering protected-user deletion blocking and one-owner repair/promotion rules.
|
||||||
- [2026-02-12] Started admin form component tests with media upload behavior coverage (`apps/admin/src/components/media/media-upload-form.test.tsx`).
|
- [2026-02-12] Started admin form component tests with media upload behavior coverage (`apps/admin/src/components/media/media-upload-form.test.tsx`).
|
||||||
- [2026-02-12] Added code handover documentation baseline: architecture map, critical invariants, request lifecycles, and onboarding playbook under `docs/product-engineering/`.
|
- [2026-02-12] Added code handover documentation baseline: architecture map, critical invariants, request lifecycles, and onboarding playbook under `docs/product-engineering/`.
|
||||||
|
- [2026-02-12] Completed admin form component coverage for pages/navigation/media using isolated form components and tests.
|
||||||
|
|
||||||
## How We Use This File
|
## How We Use This File
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { revalidatePath } from "next/cache"
|
|||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
import { AdminShell } from "@/components/admin-shell"
|
import { AdminShell } from "@/components/admin-shell"
|
||||||
|
import { CreateMenuForm } from "@/components/navigation/create-menu-form"
|
||||||
|
import { CreateNavigationItemForm } from "@/components/navigation/create-navigation-item-form"
|
||||||
import { requirePermissionForRoute } from "@/lib/route-guards"
|
import { requirePermissionForRoute } from "@/lib/route-guards"
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
@@ -206,123 +208,12 @@ export default async function NavigationManagementPage({
|
|||||||
<section className="grid gap-4 lg:grid-cols-2">
|
<section className="grid gap-4 lg:grid-cols-2">
|
||||||
<article className="rounded-xl border border-neutral-200 p-6">
|
<article className="rounded-xl border border-neutral-200 p-6">
|
||||||
<h2 className="text-xl font-medium">Create Menu</h2>
|
<h2 className="text-xl font-medium">Create Menu</h2>
|
||||||
<form action={createMenuAction} className="mt-4 space-y-3">
|
<CreateMenuForm action={createMenuAction} />
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Name</span>
|
|
||||||
<input
|
|
||||||
name="name"
|
|
||||||
required
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Slug</span>
|
|
||||||
<input
|
|
||||||
name="slug"
|
|
||||||
required
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Location</span>
|
|
||||||
<input
|
|
||||||
name="location"
|
|
||||||
defaultValue="primary"
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="inline-flex items-center gap-2 text-sm text-neutral-700">
|
|
||||||
<input
|
|
||||||
name="isVisible"
|
|
||||||
type="checkbox"
|
|
||||||
value="true"
|
|
||||||
defaultChecked
|
|
||||||
className="size-4"
|
|
||||||
/>
|
|
||||||
Visible
|
|
||||||
</label>
|
|
||||||
<Button type="submit">Create menu</Button>
|
|
||||||
</form>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="rounded-xl border border-neutral-200 p-6">
|
<article className="rounded-xl border border-neutral-200 p-6">
|
||||||
<h2 className="text-xl font-medium">Create Navigation Item</h2>
|
<h2 className="text-xl font-medium">Create Navigation Item</h2>
|
||||||
<form action={createItemAction} className="mt-4 space-y-3">
|
<CreateNavigationItemForm action={createItemAction} menus={menus} pages={pages} />
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Menu</span>
|
|
||||||
<select
|
|
||||||
name="menuId"
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
>
|
|
||||||
{menus.map((menu) => (
|
|
||||||
<option key={menu.id} value={menu.id}>
|
|
||||||
{menu.name} ({menu.location})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Label</span>
|
|
||||||
<input
|
|
||||||
name="label"
|
|
||||||
required
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Custom href</span>
|
|
||||||
<input
|
|
||||||
name="href"
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Linked page</span>
|
|
||||||
<select
|
|
||||||
name="pageId"
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
>
|
|
||||||
<option value="">(none)</option>
|
|
||||||
{pages.map((page) => (
|
|
||||||
<option key={page.id} value={page.id}>
|
|
||||||
{page.title} (/{page.slug})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Parent item id</span>
|
|
||||||
<input
|
|
||||||
name="parentId"
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Sort order</span>
|
|
||||||
<input
|
|
||||||
name="sortOrder"
|
|
||||||
defaultValue="0"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label className="inline-flex items-center gap-2 text-sm text-neutral-700">
|
|
||||||
<input
|
|
||||||
name="isVisible"
|
|
||||||
type="checkbox"
|
|
||||||
value="true"
|
|
||||||
defaultChecked
|
|
||||||
className="size-4"
|
|
||||||
/>
|
|
||||||
Visible
|
|
||||||
</label>
|
|
||||||
<Button type="submit">Create item</Button>
|
|
||||||
</form>
|
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createPage, listPages } from "@cms/db"
|
import { createPage, listPages } from "@cms/db"
|
||||||
import { Button } from "@cms/ui/button"
|
|
||||||
import { revalidatePath } from "next/cache"
|
import { revalidatePath } from "next/cache"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
import { AdminShell } from "@/components/admin-shell"
|
import { AdminShell } from "@/components/admin-shell"
|
||||||
|
import { CreatePageForm } from "@/components/pages/create-page-form"
|
||||||
import { requirePermissionForRoute } from "@/lib/route-guards"
|
import { requirePermissionForRoute } from "@/lib/route-guards"
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
@@ -110,75 +110,7 @@ export default async function PagesManagementPage({
|
|||||||
|
|
||||||
<section className="rounded-xl border border-neutral-200 p-6">
|
<section className="rounded-xl border border-neutral-200 p-6">
|
||||||
<h2 className="text-xl font-medium">Create Page</h2>
|
<h2 className="text-xl font-medium">Create Page</h2>
|
||||||
<form action={createPageAction} className="mt-4 space-y-3">
|
<CreatePageForm action={createPageAction} />
|
||||||
<div className="grid gap-3 md:grid-cols-3">
|
|
||||||
<label className="space-y-1 md:col-span-2">
|
|
||||||
<span className="text-xs text-neutral-600">Title</span>
|
|
||||||
<input
|
|
||||||
name="title"
|
|
||||||
required
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Status</span>
|
|
||||||
<select
|
|
||||||
name="status"
|
|
||||||
defaultValue="draft"
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
>
|
|
||||||
<option value="draft">draft</option>
|
|
||||||
<option value="published">published</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Slug</span>
|
|
||||||
<input
|
|
||||||
name="slug"
|
|
||||||
required
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Summary</span>
|
|
||||||
<input
|
|
||||||
name="summary"
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">Content</span>
|
|
||||||
<textarea
|
|
||||||
name="content"
|
|
||||||
rows={6}
|
|
||||||
required
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">SEO title</span>
|
|
||||||
<input
|
|
||||||
name="seoTitle"
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="space-y-1">
|
|
||||||
<span className="text-xs text-neutral-600">SEO description</span>
|
|
||||||
<input
|
|
||||||
name="seoDescription"
|
|
||||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit">Create page</Button>
|
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-xl border border-neutral-200 p-6">
|
<section className="rounded-xl border border-neutral-200 p-6">
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { render, screen } from "@testing-library/react"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
import { CreateMenuForm } from "./create-menu-form"
|
||||||
|
|
||||||
|
describe("CreateMenuForm", () => {
|
||||||
|
it("renders defaults for location and visibility", () => {
|
||||||
|
render(<CreateMenuForm action={vi.fn()} />)
|
||||||
|
|
||||||
|
const location = screen.getByLabelText("Location") as HTMLInputElement
|
||||||
|
expect(location.value).toBe("primary")
|
||||||
|
|
||||||
|
const visible = screen.getByLabelText("Visible") as HTMLInputElement
|
||||||
|
expect(visible.checked).toBe(true)
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: "Create menu" })).not.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
41
apps/admin/src/components/navigation/create-menu-form.tsx
Normal file
41
apps/admin/src/components/navigation/create-menu-form.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Button } from "@cms/ui/button"
|
||||||
|
|
||||||
|
type CreateMenuFormProps = {
|
||||||
|
action: (formData: FormData) => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateMenuForm({ action }: CreateMenuFormProps) {
|
||||||
|
return (
|
||||||
|
<form action={action} className="mt-4 space-y-3">
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Name</span>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
required
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Slug</span>
|
||||||
|
<input
|
||||||
|
name="slug"
|
||||||
|
required
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Location</span>
|
||||||
|
<input
|
||||||
|
name="location"
|
||||||
|
defaultValue="primary"
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="inline-flex items-center gap-2 text-sm text-neutral-700">
|
||||||
|
<input name="isVisible" type="checkbox" value="true" defaultChecked className="size-4" />
|
||||||
|
Visible
|
||||||
|
</label>
|
||||||
|
<Button type="submit">Create menu</Button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { render, screen } from "@testing-library/react"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
import { CreateNavigationItemForm } from "./create-navigation-item-form"
|
||||||
|
|
||||||
|
describe("CreateNavigationItemForm", () => {
|
||||||
|
it("renders menu/page options and defaults", () => {
|
||||||
|
render(
|
||||||
|
<CreateNavigationItemForm
|
||||||
|
action={vi.fn()}
|
||||||
|
menus={[{ id: "menu-1", name: "Primary", location: "header" }]}
|
||||||
|
pages={[{ id: "page-1", title: "Home", slug: "home" }]}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const menu = screen.getByLabelText("Menu") as HTMLSelectElement
|
||||||
|
expect(menu.options.length).toBe(1)
|
||||||
|
expect(menu.value).toBe("menu-1")
|
||||||
|
|
||||||
|
const page = screen.getByLabelText("Linked page") as HTMLSelectElement
|
||||||
|
expect(page.options.length).toBe(2)
|
||||||
|
expect(page.options[0]?.value).toBe("")
|
||||||
|
|
||||||
|
const sortOrder = screen.getByLabelText("Sort order") as HTMLInputElement
|
||||||
|
expect(sortOrder.value).toBe("0")
|
||||||
|
|
||||||
|
const visible = screen.getByLabelText("Visible") as HTMLInputElement
|
||||||
|
expect(visible.checked).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { Button } from "@cms/ui/button"
|
||||||
|
|
||||||
|
type MenuOption = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
location: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageOption = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateNavigationItemFormProps = {
|
||||||
|
action: (formData: FormData) => void | Promise<void>
|
||||||
|
menus: MenuOption[]
|
||||||
|
pages: PageOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateNavigationItemForm({ action, menus, pages }: CreateNavigationItemFormProps) {
|
||||||
|
return (
|
||||||
|
<form action={action} className="mt-4 space-y-3">
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Menu</span>
|
||||||
|
<select
|
||||||
|
name="menuId"
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
{menus.map((menu) => (
|
||||||
|
<option key={menu.id} value={menu.id}>
|
||||||
|
{menu.name} ({menu.location})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Label</span>
|
||||||
|
<input
|
||||||
|
name="label"
|
||||||
|
required
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Custom href</span>
|
||||||
|
<input
|
||||||
|
name="href"
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Linked page</span>
|
||||||
|
<select
|
||||||
|
name="pageId"
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="">(none)</option>
|
||||||
|
{pages.map((page) => (
|
||||||
|
<option key={page.id} value={page.id}>
|
||||||
|
{page.title} (/{page.slug})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Parent item id</span>
|
||||||
|
<input
|
||||||
|
name="parentId"
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Sort order</span>
|
||||||
|
<input
|
||||||
|
name="sortOrder"
|
||||||
|
defaultValue="0"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label className="inline-flex items-center gap-2 text-sm text-neutral-700">
|
||||||
|
<input name="isVisible" type="checkbox" value="true" defaultChecked className="size-4" />
|
||||||
|
Visible
|
||||||
|
</label>
|
||||||
|
<Button type="submit">Create item</Button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
apps/admin/src/components/pages/create-page-form.test.tsx
Normal file
21
apps/admin/src/components/pages/create-page-form.test.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { render, screen } from "@testing-library/react"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
import { CreatePageForm } from "./create-page-form"
|
||||||
|
|
||||||
|
describe("CreatePageForm", () => {
|
||||||
|
it("renders required fields and draft default status", () => {
|
||||||
|
render(<CreatePageForm action={vi.fn()} />)
|
||||||
|
|
||||||
|
expect((screen.getByLabelText("Title") as HTMLInputElement).name).toBe("title")
|
||||||
|
expect((screen.getByLabelText("Slug") as HTMLInputElement).name).toBe("slug")
|
||||||
|
expect((screen.getByLabelText("Content") as HTMLTextAreaElement).name).toBe("content")
|
||||||
|
|
||||||
|
const status = screen.getByLabelText("Status") as HTMLSelectElement
|
||||||
|
expect(status.value).toBe("draft")
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: "Create page" })).not.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
79
apps/admin/src/components/pages/create-page-form.tsx
Normal file
79
apps/admin/src/components/pages/create-page-form.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Button } from "@cms/ui/button"
|
||||||
|
|
||||||
|
type CreatePageFormProps = {
|
||||||
|
action: (formData: FormData) => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreatePageForm({ action }: CreatePageFormProps) {
|
||||||
|
return (
|
||||||
|
<form action={action} className="mt-4 space-y-3">
|
||||||
|
<div className="grid gap-3 md:grid-cols-3">
|
||||||
|
<label className="space-y-1 md:col-span-2">
|
||||||
|
<span className="text-xs text-neutral-600">Title</span>
|
||||||
|
<input
|
||||||
|
name="title"
|
||||||
|
required
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Status</span>
|
||||||
|
<select
|
||||||
|
name="status"
|
||||||
|
defaultValue="draft"
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="draft">draft</option>
|
||||||
|
<option value="published">published</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Slug</span>
|
||||||
|
<input
|
||||||
|
name="slug"
|
||||||
|
required
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Summary</span>
|
||||||
|
<input
|
||||||
|
name="summary"
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">Content</span>
|
||||||
|
<textarea
|
||||||
|
name="content"
|
||||||
|
rows={6}
|
||||||
|
required
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">SEO title</span>
|
||||||
|
<input
|
||||||
|
name="seoTitle"
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="space-y-1">
|
||||||
|
<span className="text-xs text-neutral-600">SEO description</span>
|
||||||
|
<input
|
||||||
|
name="seoDescription"
|
||||||
|
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit">Create page</Button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user