diff --git a/TODO.md b/TODO.md index af522fd..22742fa 100644 --- a/TODO.md +++ b/TODO.md @@ -137,7 +137,7 @@ This file is the single source of truth for roadmap and delivery progress. ### Admin App (Primary Focus) - [~] [P1] Page management (create/edit/publish/unpublish/schedule) -- [ ] [P1] Page builder with reusable content blocks (hero, rich text, gallery, CTA, forms, price cards) +- [x] [P1] Page builder with reusable content blocks (hero, rich text, gallery, CTA, forms, price cards) - [~] [P1] Navigation management (menus, nested items, order, visibility) - [~] [P1] Media library (upload, browse, replace, delete) with media-type classification (artwork, banner, promo, generic, video/gif) - [x] [P1] Media enrichment metadata (alt text, copyright, author, source, tags, licensing, usage context) @@ -365,6 +365,7 @@ This file is the single source of truth for roadmap and delivery progress. - [2026-02-12] Artwork refinement baseline completed: admin `/portfolio` now captures/edits medium, dimensions, year, framing, availability, publish state, and optional price visibility (`priceAmountCents` + `priceCurrency`), with public artwork detail rendering visible prices only. - [2026-02-12] Artwork rendition management completed: admin `/portfolio` supports `thumbnail/card/full/retina/custom` slot assignment with dimensions and primary flag, plus per-artwork rendition listing and delete controls. - [2026-02-12] Media type presets baseline completed in upload API: server-side validation now uses shared per-type rules (mime + max size) for `artwork/banner/promotion/video/gif/generic`, with optional env cap override via `CMS_MEDIA_UPLOAD_MAX_BYTES`. +- [2026-02-12] Page builder reusable blocks completed: admin block editor now supports full field editing + ordering controls for hero/rich-text/gallery/cta/form/price-cards; public renderer includes form-link behavior for `contact`/`commission` keys. - [2026-02-12] Public UX pass: commission request flow now reports explicit invalid budget range errors, and header navigation now falls back to localized defaults (`home`, `portfolio`, `news`, `commissions`) when no CMS menu exists; seed data now creates those default menu entries. - [2026-02-12] Added `e2e/public-rendering.pw.ts` web coverage for fallback navigation visibility, portfolio routes, and commission submission validation (invalid budget range + successful submission path). - [2026-02-12] Testing execution is temporarily paused for delivery velocity: root test scripts are stubbed and CI test steps are disabled; all testing backlog is consolidated under `MVP 3: Testing and Quality`. diff --git a/apps/admin/src/components/pages/page-block-editor.tsx b/apps/admin/src/components/pages/page-block-editor.tsx index 31ab562..6e02294 100644 --- a/apps/admin/src/components/pages/page-block-editor.tsx +++ b/apps/admin/src/components/pages/page-block-editor.tsx @@ -43,6 +43,25 @@ function updateBlock(blocks: PageBlocks, blockId: string, next: Partial entry.id === blockId) + + if (index < 0) { + return blocks + } + + const nextIndex = direction === "up" ? index - 1 : index + 1 + if (nextIndex < 0 || nextIndex >= blocks.length) { + return blocks + } + + const next = [...blocks] + const current = next[index] + next[index] = next[nextIndex] + next[nextIndex] = current + return next +} + export function PageBlockEditor({ name, initialContent, @@ -156,13 +175,29 @@ export function PageBlockEditor({ #{index + 1} {block.type} - +
+ + + +
{block.type === "hero" ? ( @@ -187,6 +222,26 @@ export function PageBlockEditor({ placeholder="Subheading" className="rounded border border-neutral-300 px-2 py-1 text-sm" /> + + setBlocks((prev) => + updateBlock(prev, block.id, { ctaLabel: event.target.value || null }), + ) + } + placeholder="CTA label" + className="rounded border border-neutral-300 px-2 py-1 text-sm" + /> + + setBlocks((prev) => + updateBlock(prev, block.id, { ctaHref: event.target.value || null }), + ) + } + placeholder="CTA href" + className="rounded border border-neutral-300 px-2 py-1 text-sm" + /> ) : null} @@ -203,22 +258,34 @@ export function PageBlockEditor({ ) : null} {block.type === "gallery" ? ( -