Adds the Offerings module and retargets the CI/CD pipeline to Api.SlpSoftware
Continuous Integration / config (pull_request) Successful in 12s
Continuous Integration / changes (pull_request) Successful in 22s
Continuous Integration / backend-build (pull_request) Successful in 5m53s
Continuous Integration / vulnerability-scan (pull_request) Successful in 5m46s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m54s
Continuous Integration / backend-test (pull_request) Successful in 7m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m14s
Continuous Integration / frontend-test (pull_request) Successful in 4m59s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 7m34s
Continuous Integration / deploy-test (pull_request) Skipped
Continuous Integration / config (pull_request) Successful in 12s
Continuous Integration / changes (pull_request) Successful in 22s
Continuous Integration / backend-build (pull_request) Successful in 5m53s
Continuous Integration / vulnerability-scan (pull_request) Successful in 5m46s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m54s
Continuous Integration / backend-test (pull_request) Successful in 7m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m14s
Continuous Integration / frontend-test (pull_request) Successful in 4m59s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 7m34s
Continuous Integration / deploy-test (pull_request) Skipped
Implements Unit 2 "Offerings" (backend module, admin CRUD UI with drag-and-drop reordering, public GET /api/v1/offerings endpoint) and executes the feature's D-15 CI/CD cutover, switching the deploy pipeline's build/publish target from SlpModularCms.Api to SlpModularCms.Api.SlpSoftware. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWyStNL2ZsjrS7FLd7xvvN
This commit is contained in:
+124
@@ -0,0 +1,124 @@
|
||||
# Frontend Components — Unit: Offerings
|
||||
|
||||
New feature folder `frontend/src/features/offerings/`, following the existing `frontend/src/features/cms/` structural pattern (pages/components/services/schemas) — the closest existing precedent for an admin list+CRUD screen in this codebase (Functional Design Q2 investigation).
|
||||
|
||||
**New dependency** (Functional Design Q1 = A): `@dnd-kit/core` + `@dnd-kit/sortable`, added to `frontend/package.json`.
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
```mermaid
|
||||
%%{init: {'themeVariables': {'primaryTextColor':'#000000','textColor':'#000000','tertiaryTextColor':'#000000'}}}%%
|
||||
graph TD
|
||||
router["Admin Router"]
|
||||
list_page["OfferingsListPage"]
|
||||
form_page["OfferingFormPage<br/>(create and edit)"]
|
||||
list["OfferingsList<br/>(dnd-kit SortableContext)"]
|
||||
row["OfferingRow<br/>(dnd-kit useSortable)"]
|
||||
delete_dialog["DeleteOfferingDialog"]
|
||||
form["OfferingForm<br/>(shared by create/edit)"]
|
||||
dnd_hook["useOfferingsDnd<br/>(hooks/)"]
|
||||
|
||||
router -->|"/admin/offerings"| list_page
|
||||
router -->|"/admin/offerings/new"| form_page
|
||||
router -->|"/admin/offerings/:id/edit"| form_page
|
||||
list_page --> list
|
||||
list --> row
|
||||
list --> dnd_hook
|
||||
list_page --> delete_dialog
|
||||
form_page --> form
|
||||
|
||||
classDef root fill:#c6f6d5,stroke:#2e7d32,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
classDef page fill:#bee3f8,stroke:#0d47a1,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
classDef component fill:#e9d8fd,stroke:#4a148c,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
classDef hook fill:#fed7aa,stroke:#c05621,stroke-width:2px,color:#000000,font-weight:bold;
|
||||
|
||||
class router root;
|
||||
class list_page,form_page page;
|
||||
class list,row,delete_dialog,form component;
|
||||
class dnd_hook hook;
|
||||
```
|
||||
|
||||
Text alternative: the admin router has three Offerings routes — a list page, and a shared form page used for both create and edit (Functional Design Q2: separate pages, not modals). The list page composes a sortable list component (dnd-kit) made of individual rows, a delete-confirmation dialog, and the `useOfferingsDnd` hook that owns the drag-and-drop orchestration; the form page composes a single shared form component (green = router root, blue = pages, purple = components, orange = the feature-local hook).
|
||||
|
||||
## `OfferingsListPage` (`pages/OfferingsListPage.tsx`)
|
||||
|
||||
**Route**: `/admin/offerings`
|
||||
|
||||
**Responsibilities**: Fetches the admin offering list, renders `OfferingsList`, hosts `DeleteOfferingDialog`, links to `/admin/offerings/new` and per-row `/admin/offerings/:id/edit`.
|
||||
|
||||
**State**: `offeringPendingDelete: OfferingAdminDto | null` (controls whether `DeleteOfferingDialog` is open).
|
||||
|
||||
**API calls**: `useOfferings()` (`GET /api/v1/offerings/admin`).
|
||||
|
||||
## `OfferingsList` (`components/OfferingsList.tsx`)
|
||||
|
||||
**Props**: `offerings: OfferingAdminDto[]`, `onDeleteRequested: (offering: OfferingAdminDto) => void`.
|
||||
|
||||
**Responsibilities**: Wraps rows in a dnd-kit `DndContext`/`SortableContext` (Functional Design Q1), delegating the drag-end orchestration to `useOfferingsDnd` rather than handling it inline.
|
||||
|
||||
**Hooks used**: `useOfferingsDnd(offerings)` (see below).
|
||||
|
||||
## `useOfferingsDnd` (`hooks/useOfferingsDnd.ts`)
|
||||
|
||||
**Not an API-calling hook** — deliberately kept out of `services/`, since it owns dnd-kit sensor setup and local drag-state, and only calls into a `services/` hook at the end. A feature's own `hooks/` folder is a legitimate place for this kind of feature-local, non-API hook logic — it doesn't need to be either "in `services/`" or "promoted to the top-level `frontend/src/hooks/`"; those aren't the only two options.
|
||||
|
||||
**Responsibilities**: Configures dnd-kit sensors, computes the new order on drag end, optimistically updates local list state, and calls `useReorderOfferings()` (from `services/`) with the resulting ordered id list.
|
||||
|
||||
**Returns**: `{ items, sensors, handleDragEnd }` for `OfferingsList` to spread onto its `DndContext`/`SortableContext`.
|
||||
|
||||
**API calls (indirect, via the hook below)**: `useReorderOfferings()` (`PUT /api/v1/offerings/admin/reorder`).
|
||||
|
||||
## `OfferingRow` (`components/OfferingRow.tsx`)
|
||||
|
||||
**Props**: `offering: OfferingAdminDto`, `isFirst: boolean`, `isLast: boolean`, `onDeleteRequested: () => void`.
|
||||
|
||||
**Responsibilities**: Displays title/price/featured badge; a "featured" star icon (toggle, Functional Design Q5 — calls `useUpdateOffering()` with only `featured` flipped, reusing the row's already-loaded data); "move up"/"move down" buttons (disabled per `isFirst`/`isLast`, US-09); edit link to `/admin/offerings/:id/edit`; delete button (calls `onDeleteRequested`).
|
||||
|
||||
**Data-testid convention**: `offering-row-{id}-edit-link`, `offering-row-{id}-delete-button`, `offering-row-{id}-move-up-button`, `offering-row-{id}-move-down-button`, `offering-row-{id}-featured-toggle`.
|
||||
|
||||
**API calls**: `useUpdateOffering()` (featured toggle), `useMoveOffering(direction)` (`POST /api/v1/offerings/admin/{id}/move-up` or `/move-down`).
|
||||
|
||||
## `DeleteOfferingDialog` (`components/DeleteOfferingDialog.tsx`)
|
||||
|
||||
**Props**: `offering: OfferingAdminDto | null` (null = closed), `onConfirm: () => void`, `onCancel: () => void`.
|
||||
|
||||
**Responsibilities**: Confirmation dialog (Functional Design Q4) — "Weet je zeker dat je '[title]' wilt verwijderen?" / English equivalent per i18n.
|
||||
|
||||
**API calls**: none directly — the parent page calls `useDeleteOffering()` on confirm.
|
||||
|
||||
## `OfferingFormPage` (`pages/OfferingFormPage.tsx`)
|
||||
|
||||
**Routes**: `/admin/offerings/new` (create) and `/admin/offerings/:id/edit` (edit — loads the existing offering via `useOffering(id)` first).
|
||||
|
||||
**Responsibilities**: Hosts `OfferingForm`; on successful submit, navigates back to `/admin/offerings`.
|
||||
|
||||
## `OfferingForm` (`components/OfferingForm.tsx`)
|
||||
|
||||
**Props**: `initialValues?: OfferingAdminDto` (undefined for create), `onSubmit: (values: OfferingFormData) => void`, `isSubmitting: boolean`.
|
||||
|
||||
**Form fields**: `title`, `description`, `price`, `priceNote`, `features` (dynamic list, add/remove item, 1-10 items), `ctaLabel`, `featured` (checkbox/toggle, Functional Design Q5).
|
||||
|
||||
**Validation**: `react-hook-form` + `zod`, schema in `schemas/offering.ts`, mirroring BR-OFF-04's server-side bounds (Functional Design Q3): `title` ≤100, `description` ≤500, `price` ≤50, `priceNote` ≤100, `ctaLabel` ≤50, `features` 1-10 items each ≤200 characters, all required except `featured` (defaults `false`).
|
||||
|
||||
**API calls**: `useCreateOffering()` (`POST /api/v1/offerings/admin`) or `useUpdateOffering()` (`PUT /api/v1/offerings/admin/{id}`), selected by whether `initialValues` is present.
|
||||
|
||||
## Hooks
|
||||
|
||||
Two hook locations in this feature, split by what the hook actually does — not by a "shared vs. feature-specific" rule (a feature's own `hooks/` folder is a legitimate, separate option; it isn't limited to either "lives in `services/`" or "gets promoted to the top-level `frontend/src/hooks/`"):
|
||||
|
||||
- **`services/`** — React Query hooks that call the backend API, following the existing codebase convention (`frontend/src/features/cms/services/` already holds hooks like `useCmsInstances.ts`, not OOP-style service classes — the folder name is the established convention, the contents are hooks). Table below.
|
||||
- **`hooks/`** — feature-local hooks that are not themselves API calls. Currently just `useOfferingsDnd` (drag-and-drop orchestration, which calls a `services/` hook internally but isn't one itself).
|
||||
|
||||
### `services/` (API-calling hooks)
|
||||
|
||||
| Hook | Method/Route | Purpose |
|
||||
|---|---|---|
|
||||
| `useOfferings()` | `GET /api/v1/offerings/admin` | List for `OfferingsListPage` |
|
||||
| `useOffering(id)` | (derived from `useOfferings()` cache, or a dedicated fetch if not cached) | Prefill `OfferingFormPage` in edit mode |
|
||||
| `useCreateOffering()` | `POST /api/v1/offerings/admin` | Create |
|
||||
| `useUpdateOffering()` | `PUT /api/v1/offerings/admin/{id}` | Edit form submit, and the list-row featured toggle |
|
||||
| `useDeleteOffering()` | `DELETE /api/v1/offerings/admin/{id}` | Delete, after confirmation |
|
||||
| `useReorderOfferings()` | `PUT /api/v1/offerings/admin/reorder` | Drag-and-drop |
|
||||
| `useMoveOffering(direction)` | `POST /api/v1/offerings/admin/{id}/move-up` or `/move-down` | Button-based reorder |
|
||||
|
||||
All mutating hooks invalidate the `['offerings', 'admin']` query key on success, matching the existing `useAddCmsInstance`/`useUpdateCmsInstanceStatus` pattern in `features/cms/services/`.
|
||||
Reference in New Issue
Block a user