diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9c996..7f83751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,56 @@ +## Unreleased (2026-02-12) + +* test(admin): cover support fallback route and mark todo complete ([4ac74101487e0bb220215328510fd4344c9110e3](https://git.fellies.net/Citali/cms.fellies.org/commits/4ac74101487e0bb220215328510fd4344c9110e3)) +* test(auth): add registration policy route-flow integration tests ([39178c2d8d9c203ceca63a36012b58173d21d4d4](https://git.fellies.net/Citali/cms.fellies.org/commits/39178c2d8d9c203ceca63a36012b58173d21d4d4)) +* test(ci): add quality gates, e2e data prep, and i18n integration coverage ([4d4b583cf4dcb0f3b99f74c666af15d3b1a0fc59](https://git.fellies.net/Citali/cms.fellies.org/commits/4d4b583cf4dcb0f3b99f74c666af15d3b1a0fc59)) +* test(crud): finalize MVP1 gate CRUD contract coverage ([36b09cd9d76049219569bb4828aa292aec78a935](https://git.fellies.net/Citali/cms.fellies.org/commits/36b09cd9d76049219569bb4828aa292aec78a935)) +* test(e2e): add mvp1 happy path scenarios ([7c4b667bc79a022518d03a52a98060a9c86c673c](https://git.fellies.net/Citali/cms.fellies.org/commits/7c4b667bc79a022518d03a52a98060a9c86c673c)) +* test(mvp0): complete remaining i18n, RBAC, and CRUD coverage ([3b130568e9bfff9e428c3650c37dd8b1abfbed57](https://git.fellies.net/Citali/cms.fellies.org/commits/3b130568e9bfff9e428c3650c37dd8b1abfbed57)) +* test(mvp1): add owner invariants and media form coverage ([37f62a8007d6b50e7b53e5785365965c00c74a5b](https://git.fellies.net/Citali/cms.fellies.org/commits/37f62a8007d6b50e7b53e5785365965c00c74a5b)) +* test(mvp1): expand domain schema and service unit coverage ([24676bd384c39d954b791ff2322a4469ab86a83a](https://git.fellies.net/Citali/cms.fellies.org/commits/24676bd384c39d954b791ff2322a4469ab86a83a)) +* feat(admin-auth): add first-start onboarding flow and dev db reset command ([7b665ae633e869560654cf1c115d12dc05af1c45](https://git.fellies.net/Citali/cms.fellies.org/commits/7b665ae633e869560654cf1c115d12dc05af1c45)) +* feat(admin-auth): support username login and add dashboard logout ([b96cd6d8005aea50c49a70540e0fd2afff94c682](https://git.fellies.net/Citali/cms.fellies.org/commits/b96cd6d8005aea50c49a70540e0fd2afff94c682)) +* feat(admin-i18n): add cookie-based locale runtime and switcher baseline ([b618c8cb5161eef46e80d622156a1e53a732ea35](https://git.fellies.net/Citali/cms.fellies.org/commits/b618c8cb5161eef46e80d622156a1e53a732ea35)) +* feat(admin): add IA shell and protected section skeleton routes ([bf1a92d129b89811b58d6920a4e09482816988a0](https://git.fellies.net/Citali/cms.fellies.org/commits/bf1a92d129b89811b58d6920a4e09482816988a0)) +* feat(admin): add posts CRUD sandbox and shared CRUD foundation ([07e5f53793da5aa0b17e62a3a9b99c23a56dbcf0](https://git.fellies.net/Citali/cms.fellies.org/commits/07e5f53793da5aa0b17e62a3a9b99c23a56dbcf0)) +* feat(admin): add registration policy settings and disabled register state ([d0f731743c789ef836e4573eadece2f4c67c973d](https://git.fellies.net/Citali/cms.fellies.org/commits/d0f731743c789ef836e4573eadece2f4c67c973d)) +* feat(auth): block protected account deletion in auth endpoints ([0e2248b5c7f72684e4db6d4ab8f306b10f50ac66](https://git.fellies.net/Citali/cms.fellies.org/commits/0e2248b5c7f72684e4db6d4ab8f306b10f50ac66)) +* feat(auth): bootstrap protected support and first owner users ([411861419f160e3573a71ea67d57af7e0e91de7d](https://git.fellies.net/Citali/cms.fellies.org/commits/411861419f160e3573a71ea67d57af7e0e91de7d)) +* feat(auth): enforce single-owner invariant in bootstrap flow ([29a6e38ff3b725e4232736c4b5b007a2989acb82](https://git.fellies.net/Citali/cms.fellies.org/commits/29a6e38ff3b725e4232736c4b5b007a2989acb82)) +* feat(ci): stamp build metadata and validate footer version hash ([af52b8581f7dbe320c92752fcc56b73e37f0c2ba](https://git.fellies.net/Citali/cms.fellies.org/commits/af52b8581f7dbe320c92752fcc56b73e37f0c2ba)) +* feat(commissions): add customer records and kanban workflow baseline ([994b33e081c3507cbf88820028b819a4fc4b07a0](https://git.fellies.net/Citali/cms.fellies.org/commits/994b33e081c3507cbf88820028b819a4fc4b07a0)) +* feat(content): add announcements and public news flows ([dbf817c25511b3038b7abe81e4577d3518fd3f19](https://git.fellies.net/Citali/cms.fellies.org/commits/dbf817c25511b3038b7abe81e4577d3518fd3f19)) +* feat(media): add admin media CRUD preview and storage cleanup ([7d9bc9dca9197e87cc590ad6b49837c5774fcd4f](https://git.fellies.net/Citali/cms.fellies.org/commits/7d9bc9dca9197e87cc590ad6b49837c5774fcd4f)) +* feat(media): add mvp1 upload pipeline baseline ([5becba602c3aaefbe24ec71414f62b29a155d158](https://git.fellies.net/Citali/cms.fellies.org/commits/5becba602c3aaefbe24ec71414f62b29a155d158)) +* feat(media): complete mvp1 media foundation workflows ([ad351ed73ab7c76a9e751348ebf943aec5f0d084](https://git.fellies.net/Citali/cms.fellies.org/commits/ad351ed73ab7c76a9e751348ebf943aec5f0d084)) +* feat(media): default to s3 with local upload fallback ([86a8af25d8c28c2ab19039b56b1c69263c7450c5](https://git.fellies.net/Citali/cms.fellies.org/commits/86a8af25d8c28c2ab19039b56b1c69263c7450c5)) +* feat(media): scaffold mvp1 media and portfolio foundation ([d727ab8b5b896e5471829a6a1880dc33da28d070](https://git.fellies.net/Citali/cms.fellies.org/commits/d727ab8b5b896e5471829a6a1880dc33da28d070)) +* feat(media): support local and s3 upload providers ([19738b77d8842f3263e7f049aa47063dcfbd4ae6](https://git.fellies.net/Citali/cms.fellies.org/commits/19738b77d8842f3263e7f049aa47063dcfbd4ae6)) +* feat(pages): add pages and navigation builder baseline ([281b1d7a1be72af4cff790ca7e97d51cafcd8139](https://git.fellies.net/Citali/cms.fellies.org/commits/281b1d7a1be72af4cff790ca7e97d51cafcd8139)) +* feat(release): publish gitea release notes and enable production rollback ([ccac669454b46a3918b34df1d3c5f0e5f00aa1d9](https://git.fellies.net/Citali/cms.fellies.org/commits/ccac669454b46a3918b34df1d3c5f0e5f00aa1d9)) +* feat(settings): manage public header banner in admin ([d1face36c540673486b494d276f5af1621b6e6cb](https://git.fellies.net/Citali/cms.fellies.org/commits/d1face36c540673486b494d276f5af1621b6e6cb)) +* feat(versioning): show runtime version and git hash in app footers ([3de4d5732e26e06e825986e58ec271d0f0ff4007](https://git.fellies.net/Citali/cms.fellies.org/commits/3de4d5732e26e06e825986e58ec271d0f0ff4007)) +* feat(web-i18n): add es/fr locales and expand switcher locale set ([de26cb7647cf537a783cc9c77ae447a0f8a09ef6](https://git.fellies.net/Citali/cms.fellies.org/commits/de26cb7647cf537a783cc9c77ae447a0f8a09ef6)) +* feat(web): complete MVP0 public layout, banner, and SEO baseline ([8390689c8dd81dca5662b842c827a4759a9025e1](https://git.fellies.net/Citali/cms.fellies.org/commits/8390689c8dd81dca5662b842c827a4759a9025e1)) +* feat(web): render cms pages and navigation from db ([f65a9ea03f39c21ee9b31e7f9100e3a1f522525f](https://git.fellies.net/Citali/cms.fellies.org/commits/f65a9ea03f39c21ee9b31e7f9100e3a1f522525f)) +* refactor(db): simplify to single prisma schema workflow ([df1280af4a1d24bd9374fc9a005cea9142745d46](https://git.fellies.net/Citali/cms.fellies.org/commits/df1280af4a1d24bd9374fc9a005cea9142745d46)) +* refactor(media): use asset-centric storage key layout ([3e4f0b6c75c59422675637bf658b6682c22f4a89](https://git.fellies.net/Citali/cms.fellies.org/commits/3e4f0b6c75c59422675637bf658b6682c22f4a89)) +* docs(adr): add glossary pages and ADR baseline structure ([cec87679ca5efcf70883b6c78245f8197a8a4432](https://git.fellies.net/Citali/cms.fellies.org/commits/cec87679ca5efcf70883b6c78245f8197a8a4432)) +* docs(crud): add implementation examples and complete docs task ([7b4b23fc4ffdd7e6be8af9da6b2026067acbd35e](https://git.fellies.net/Citali/cms.fellies.org/commits/7b4b23fc4ffdd7e6be8af9da6b2026067acbd35e)) +* docs(gitflow): add branch protection verification checklist ([f9f2b4eb15bd42690891bdc8e4d34c5e55c343dc](https://git.fellies.net/Citali/cms.fellies.org/commits/f9f2b4eb15bd42690891bdc8e4d34c5e55c343dc)) +* docs(i18n): add conventions guide and wire docs navigation ([5872593b014e527ef884ea89053f9cf191edf5dc](https://git.fellies.net/Citali/cms.fellies.org/commits/5872593b014e527ef884ea89053f9cf191edf5dc)) +* docs(ops): add environment and deployment runbook ([4d6e17a13b3ee4a00d93713a223871e96ee94550](https://git.fellies.net/Citali/cms.fellies.org/commits/4d6e17a13b3ee4a00d93713a223871e96ee94550)) +* docs(ops): add staging deployment checklist and evidence template ([637dfd2651a8ad7b0900c0a87c714da7750aaae2](https://git.fellies.net/Citali/cms.fellies.org/commits/637dfd2651a8ad7b0900c0a87c714da7750aaae2)) +* docs(product): add cms feature topics, package catalog, and inspiration notes ([5b47fafe89e7d1e4fb42646f8bac0e2423828c07](https://git.fellies.net/Citali/cms.fellies.org/commits/5b47fafe89e7d1e4fb42646f8bac0e2423828c07)) +* docs(versioning): define release policy and close MVP0 pipeline tasks ([516b7730128951a9f0527b89291b21e14e35aca2](https://git.fellies.net/Citali/cms.fellies.org/commits/516b7730128951a9f0527b89291b21e14e35aca2)) +* chore(ci): add gitea actions runner compose setup ([334a5e35264bf57f1f3586bd78364a5b1d704876](https://git.fellies.net/Citali/cms.fellies.org/commits/334a5e35264bf57f1f3586bd78364a5b1d704876)) +* chore(repo): remove theoretical workflow and fix prisma ci generation ([a57464d818c10caa2732c1aac113d9a251342de1](https://git.fellies.net/Citali/cms.fellies.org/commits/a57464d818c10caa2732c1aac113d9a251342de1)) +* chore(repo): update turbo dependency ([37fabad1f8ceb6224c892facb60b5aa2bca02cc5](https://git.fellies.net/Citali/cms.fellies.org/commits/37fabad1f8ceb6224c892facb60b5aa2bca02cc5)) +* fix(ci): gitea workflows ([c174f840bcfa297937fa40bc3ce4593ddc8ca599](https://git.fellies.net/Citali/cms.fellies.org/commits/c174f840bcfa297937fa40bc3ce4593ddc8ca599)) +* fix(db): organize imports for biome check ([14c3df623a84a3307d4e825bbf36cccfd882eb49](https://git.fellies.net/Citali/cms.fellies.org/commits/14c3df623a84a3307d4e825bbf36cccfd882eb49)) +* ci(delivery): add deploy and release workflow scaffolds ([969e88670f5cb3dd0156e4a53bd84d729be4fe82](https://git.fellies.net/Citali/cms.fellies.org/commits/969e88670f5cb3dd0156e4a53bd84d729be4fe82)) +* ci(gitflow): enforce branch and PR governance checks ([21cc55a1b93f9c7d5ec0db7643f6fe895312a325](https://git.fellies.net/Citali/cms.fellies.org/commits/21cc55a1b93f9c7d5ec0db7643f6fe895312a325)) + ## 0.1.0 (2026-02-10) ### Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2712a06..32b7cff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,7 +46,7 @@ Rules: ## Changelog Process - Keep commit messages conventional. -- Generate/update `CHANGELOG.md` with: +- Generate/update `CHANGELOG.md` with release-focused sections (includes `Unreleased`): ```bash bun run changelog:release @@ -57,3 +57,10 @@ bun run changelog:release ```bash bun run changelog:preview ``` + +- For exhaustive output across all allowed commit types (`feat`, `fix`, `docs`, `test`, `ci`, `chore`, `refactor`, etc.): + +```bash +bun run changelog:full:preview +bun run changelog:full:release +``` diff --git a/README.md b/README.md index ebb1233..599e31c 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ docker compose --env-file .env.gitea-runner -f docker-compose.gitea-runner.yml u - Changelog file: `CHANGELOG.md` - Commit schema: Conventional Commits (see `CONTRIBUTING.md`) -- Generate/update changelog from git commits: +- Generate/update changelog from git commits (release-focused sections + `Unreleased`): ```bash bun run changelog:release @@ -150,6 +150,13 @@ bun run changelog:release bun run changelog:preview ``` +- Generate exhaustive changelog output across all supported commit types: + +```bash +bun run changelog:full:preview +bun run changelog:full:release +``` + ## Docs Tool - Docs tool: VitePress diff --git a/TODO.md b/TODO.md index bc13ab8..c05d960 100644 --- a/TODO.md +++ b/TODO.md @@ -124,13 +124,13 @@ This file is the single source of truth for roadmap and delivery progress. S3/local upload adapter, media processing presets, metadata input flows, admin media CRUD UI - [~] [P1] `todo/mvp1-pages-navigation-builder`: page CRUD, navigation tree, reusable page blocks (forms/price cards/gallery embeds) -- [ ] [P1] `todo/mvp1-commissions-customers`: +- [~] [P1] `todo/mvp1-commissions-customers`: commission request intake + admin CRUD + kanban + customer entity/linking -- [ ] [P1] `todo/mvp1-announcements-news`: +- [~] [P1] `todo/mvp1-announcements-news`: announcement management/rendering + news/blog CRUD and public rendering - [~] [P1] `todo/mvp1-public-rendering-integration`: public rendering for pages/navigation/media/portfolio/announcements and commissioning entrypoints -- [ ] [P1] `todo/mvp1-e2e-happy-paths`: +- [~] [P1] `todo/mvp1-e2e-happy-paths`: end-to-end scenarios for page publish, media flow, announcement display, commission flow ### Separate Product Ideas Backlog (Non-Blocking) @@ -156,13 +156,13 @@ This file is the single source of truth for roadmap and delivery progress. - [ ] [P1] Users management (invite, roles, status) - [ ] [P1] Disable/ban user function and enforcement in auth/session checks - [~] [P1] Owner/support protection rules in user management actions (cannot delete/demote) -- [ ] [P1] Commissions management (request intake, owner, due date, notes, linked customer, linked artworks) -- [ ] [P1] Customer records (contact profile, notes, consent flags, recurrence marker) -- [ ] [P1] Customer-to-commission linkage and reuse workflow (no re-entry for recurring customers) -- [ ] [P1] Kanban workflow for commissions (new, scoped, in-progress, review, done) -- [ ] [P1] Header banner management (message, CTA, active window) -- [ ] [P1] Announcements management (prominent site notices with schedule, priority, and audience targeting) -- [ ] [P2] News/blog editorial workflow (draft/review/publish, authoring metadata) +- [~] [P1] Commissions management (request intake, owner, due date, notes, linked customer, linked artworks) +- [~] [P1] Customer records (contact profile, notes, consent flags, recurrence marker) +- [~] [P1] Customer-to-commission linkage and reuse workflow (no re-entry for recurring customers) +- [~] [P1] Kanban workflow for commissions (new, scoped, in-progress, review, done) +- [x] [P1] Header banner management (message, CTA, active window) +- [~] [P1] Announcements management (prominent site notices with schedule, priority, and audience targeting) +- [~] [P2] News/blog editorial workflow (draft/review/publish, authoring metadata) ### Public App @@ -171,29 +171,63 @@ This file is the single source of truth for roadmap and delivery progress. - [ ] [P1] Media entity rendering with enrichment data - [ ] [P1] Portfolio views (gallery/album/category/tag) for artworks with filter and sort controls - [ ] [P1] Rendition-aware media delivery (thumbnail/card/full) per template slot -- [ ] [P1] Translation-ready content model for public entities (pages/news/navigation labels) +- [~] [P1] Translation-ready content model for public entities (pages/news/navigation labels) - [ ] [P2] Artwork views and listing filters - [ ] [P1] Commission request submission flow -- [ ] [P1] Header banner render logic and fallbacks +- [x] [P1] Header banner render logic and fallbacks - [ ] [P1] Announcement render slots (homepage + optional global/top banner position) ### News / Blog (Secondary Track) -- [ ] [P1] News/blog content type (editorial content for artist updates and process posts) -- [ ] [P1] Admin list/editor for news posts -- [ ] [P1] Public news index + detail pages +- [~] [P1] News/blog content type (editorial content for artist updates and process posts) +- [~] [P1] Admin list/editor for news posts +- [~] [P1] Public news index + detail pages - [ ] [P2] Tag/category and basic archive support ### Testing -- [ ] [P1] Unit tests for content schemas and service logic -- [ ] [P1] Component tests for admin forms (pages/media/navigation) -- [ ] [P1] Integration tests for owner invariant and hidden support-user protection -- [ ] [P1] Integration tests for registration allow/deny behavior -- [ ] [P1] Integration tests for translated content CRUD and locale-specific validation -- [ ] [P1] E2E happy paths: create page, publish, see on public app -- [ ] [P1] E2E happy paths: media upload + artwork refinement display -- [ ] [P1] E2E happy paths: commissions kanban transitions +- [x] [P1] Unit tests for content schemas and service logic +- [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 registration allow/deny behavior +- [x] [P1] Integration tests for translated content CRUD and locale-specific validation +- [~] [P1] E2E happy paths: create page, publish, see on public app +- [~] [P1] E2E happy paths: media upload + artwork refinement display +- [~] [P1] E2E happy paths: commissions kanban transitions + +### Code Documentation And Handover + +- [x] [P1] Create architecture map per package/app (`what exists`, `why`, `how to extend`) for `@cms/db`, `@cms/content`, `@cms/crud`, `@cms/ui`, `apps/admin`, `apps/web` +- [x] [P1] Add module-level ownership docs for auth, media, pages/navigation, commissions, announcements/news flows +- [x] [P1] Document critical invariants (single owner rule, protected support user, registration policy gates, media storage key contract) +- [x] [P1] Add “request lifecycle” docs for key flows (auth sign-in/up, media upload, page publish, commission status change) +- [x] [P1] Add coding handover playbook: local setup, migration workflow, test strategy, branch/release process, common failure recovery +- [ ] [P2] Add code-level diagrams (Mermaid) for service boundaries and data relationships +- [ ] [P2] Add route/action inventory for admin and public apps with linked source files + +## MVP 1.5: UX/UI And Theming + +### MVP1.5 Suggested Branch Order + +- [ ] [P1] `todo/mvp15-design-tokens-foundation`: + establish shared design tokens (color, spacing, radius, typography scale, motion) in `@cms/ui` and app-level theme contracts +- [ ] [P1] `todo/mvp15-admin-layout-polish`: + refine admin shell, navigation hierarchy, spacing rhythm, table/form visual consistency, empty/loading/error states +- [ ] [P1] `todo/mvp15-public-layout-and-templates`: + define public visual direction (hero/header/footer/content widths), page templates for home/content/news/portfolio +- [ ] [P2] `todo/mvp15-component-library-pass`: + align shadcn-based primitives with CMS brand system (buttons, inputs, cards, badges, tabs, dialogs, toasts) +- [ ] [P2] `todo/mvp15-responsive-and-a11y-pass`: + mobile/tablet breakpoints, keyboard flow, focus states, contrast checks, reduced-motion support +- [ ] [P2] `todo/mvp15-visual-regression-baseline`: + add screenshot baselines for critical admin/public routes to guard layout regressions + +### Deliverables + +- [ ] [P1] Admin UI baseline feels production-ready for daily editorial use +- [ ] [P1] Public UI baseline is template-ready for artist branding and portfolio storytelling +- [ ] [P2] Shared UI primitives are consistent across admin and public apps +- [ ] [P2] Core routes have visual-regression coverage for the new layout baseline ## MVP 2: Production Readiness @@ -276,6 +310,19 @@ This file is the single source of truth for roadmap and delivery progress. - [2026-02-12] Admin media CRUD now includes list-to-detail flow (`/media/:id`) with metadata edit and delete actions. - [2026-02-12] MVP1 pages/navigation baseline started: `Page`, `NavigationMenu`, and `NavigationItem` models plus admin CRUD routes (`/pages`, `/pages/:id`, `/navigation`). - [2026-02-12] Public app now renders CMS-managed navigation (header) and CMS-managed pages by slug (including homepage when `home` page exists). +- [2026-02-12] Commissions/customer baseline added: admin `/commissions` now supports customer creation, commission intake, status transitions, and a basic kanban board. +- [2026-02-12] Announcements/news baseline added: admin `/announcements` + `/news` management screens and public announcement rendering slots (`global_top`, `homepage`). +- [2026-02-12] Public news routes now exist at `/news` and `/news/:slug` (detail restricted to published posts). +- [2026-02-12] Added `e2e/happy-paths.pw.ts` covering admin login, page publish/public rendering, announcement rendering, media upload, and commission status transition. +- [2026-02-12] Expanded unit coverage for content/domain schemas and post service behavior (`packages/content/src/domain-schemas.test.ts`, `packages/db/src/posts.test.ts`). +- [2026-02-12] Added auth flow integration tests for `/login`, `/register`, `/welcome` to validate registration allow/deny and owner bootstrap redirects. +- [2026-02-12] Admin settings now manage public header banner (enabled/message/CTA), backed by `system_setting` and consumed by public layout rendering. +- [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] 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. +- [2026-02-12] Added page translation CRUD baseline (`PageTranslation`) with locale validation (`de/en/es/fr`) and integration coverage for localized read + fallback behavior. +- [2026-02-12] Page editor now supports locale translations in `/pages/:id`; public page rendering uses locale-aware page lookup with base-content fallback. ## How We Use This File diff --git a/apps/admin/src/app/announcements/page.tsx b/apps/admin/src/app/announcements/page.tsx new file mode 100644 index 0000000..95129fd --- /dev/null +++ b/apps/admin/src/app/announcements/page.tsx @@ -0,0 +1,423 @@ +import { + createAnnouncement, + deleteAnnouncement, + listAnnouncements, + updateAnnouncement, +} from "@cms/db" +import { Button } from "@cms/ui/button" +import { revalidatePath } from "next/cache" +import { redirect } from "next/navigation" + +import { AdminShell } from "@/components/admin-shell" +import { requirePermissionForRoute } from "@/lib/route-guards" + +export const dynamic = "force-dynamic" + +type SearchParamsInput = Record + +function readFirstValue(value: string | string[] | undefined): string | null { + if (Array.isArray(value)) { + return value[0] ?? null + } + + return value ?? null +} + +function readInputString(formData: FormData, field: string): string { + const value = formData.get(field) + return typeof value === "string" ? value.trim() : "" +} + +function readNullableString(formData: FormData, field: string): string | null { + const value = readInputString(formData, field) + return value.length > 0 ? value : null +} + +function readNullableDate(formData: FormData, field: string): Date | null { + const value = readInputString(formData, field) + + if (!value) { + return null + } + + const parsed = new Date(value) + + if (Number.isNaN(parsed.getTime())) { + return null + } + + return parsed +} + +function readInt(formData: FormData, field: string, fallback = 100): number { + const value = readInputString(formData, field) + + if (!value) { + return fallback + } + + const parsed = Number.parseInt(value, 10) + + if (!Number.isFinite(parsed)) { + return fallback + } + + return parsed +} + +function redirectWithState(params: { notice?: string; error?: string }) { + const query = new URLSearchParams() + + if (params.notice) { + query.set("notice", params.notice) + } + + if (params.error) { + query.set("error", params.error) + } + + const value = query.toString() + redirect(value ? `/announcements?${value}` : "/announcements") +} + +async function createAnnouncementAction(formData: FormData) { + "use server" + + await requirePermissionForRoute({ + nextPath: "/announcements", + permission: "banner:write", + scope: "global", + }) + + try { + await createAnnouncement({ + title: readInputString(formData, "title"), + message: readInputString(formData, "message"), + placement: readInputString(formData, "placement"), + priority: readInt(formData, "priority", 100), + ctaLabel: readNullableString(formData, "ctaLabel"), + ctaHref: readNullableString(formData, "ctaHref"), + startsAt: readNullableDate(formData, "startsAt"), + endsAt: readNullableDate(formData, "endsAt"), + isVisible: readInputString(formData, "isVisible") === "true", + }) + } catch { + redirectWithState({ error: "Failed to create announcement." }) + } + + revalidatePath("/announcements") + revalidatePath("/") + redirectWithState({ notice: "Announcement created." }) +} + +async function updateAnnouncementAction(formData: FormData) { + "use server" + + await requirePermissionForRoute({ + nextPath: "/announcements", + permission: "banner:write", + scope: "global", + }) + + try { + await updateAnnouncement({ + id: readInputString(formData, "id"), + title: readInputString(formData, "title"), + message: readInputString(formData, "message"), + placement: readInputString(formData, "placement"), + priority: readInt(formData, "priority", 100), + ctaLabel: readNullableString(formData, "ctaLabel"), + ctaHref: readNullableString(formData, "ctaHref"), + startsAt: readNullableDate(formData, "startsAt"), + endsAt: readNullableDate(formData, "endsAt"), + isVisible: readInputString(formData, "isVisible") === "true", + }) + } catch { + redirectWithState({ error: "Failed to update announcement." }) + } + + revalidatePath("/announcements") + revalidatePath("/") + redirectWithState({ notice: "Announcement updated." }) +} + +async function deleteAnnouncementAction(formData: FormData) { + "use server" + + await requirePermissionForRoute({ + nextPath: "/announcements", + permission: "banner:write", + scope: "global", + }) + + try { + await deleteAnnouncement(readInputString(formData, "id")) + } catch { + redirectWithState({ error: "Failed to delete announcement." }) + } + + revalidatePath("/announcements") + revalidatePath("/") + redirectWithState({ notice: "Announcement deleted." }) +} + +function dateInputValue(value: Date | null): string { + if (!value) { + return "" + } + + return value.toISOString().slice(0, 10) +} + +export default async function AnnouncementsPage({ + searchParams, +}: { + searchParams: Promise +}) { + const role = await requirePermissionForRoute({ + nextPath: "/announcements", + permission: "banner:read", + scope: "global", + }) + + const [resolvedSearchParams, announcements] = await Promise.all([ + searchParams, + listAnnouncements(200), + ]) + + const notice = readFirstValue(resolvedSearchParams.notice) + const error = readFirstValue(resolvedSearchParams.error) + + return ( + + {notice ? ( +
+ {notice} +
+ ) : null} + + {error ? ( +
+ {error} +
+ ) : null} + +
+

Create Announcement

+
+ +