feat(mall): serve home marketing content from the API

Wave 4 of replacing the fixed-data mock adapter, and the first capability the
mall never had a backend for: the home page's banners, promo tiles, quick links
and floor advert art move out of local arrays.

- four explicit tables (`banners`, `promos`, `quick_links`, `floor_adverts`)
  rather than one JSONB payload table, so Postgres enforces each shape
- a migration seeds them from the assets the page already rendered, so the flip
  is visually a no-op. Destinations are real routes now: the mock's promo links
  pointed at dangling `?category=c1` ids and its first banner used `sort=sales`,
  which the catalog API rejects
- `GET /api/content/home` is public and returns the four active, ordered lists,
  always including a key so a page can render a missing block
- `GET /api/admin/content` and `PUT /api/admin/content/{kind}` let a platform
  admin read everything and replace one kind transactionally, with positions
  reindexed from the submitted order and a rejected list changing nothing
- the mall's fixed-data adapter learns `getHomeContent`, and a `content` domain
  joins the per-domain switch so the rollback path still renders the page

Verified: 23 backend tests green including six new content tests; all three
frontends build; the home page renders the same four blocks as before, an admin
reorder and deactivation change the rendered carousel, and the fixed-data
rollback renders every block with the backend stopped.

OpenSpec change: openspec/changes/replace-mock-api-wave-4
This commit is contained in:
2026-09-17 16:48:10 +00:00
parent 2136a48fbe
commit 104737e4e1
17 changed files with 866 additions and 15 deletions
+40
View File
@@ -9,6 +9,7 @@ import type {
AuthTokens,
Cart,
CartItem,
HomeContent,
Invoice,
InvoiceKind,
Order,
@@ -18,8 +19,11 @@ import type {
} from "@vmall/shared";
import {
BASE_CURRENCY,
MOCK_BANNERS,
MOCK_CATEGORIES,
MOCK_CURRENCIES,
MOCK_PROMOS,
MOCK_QUICK_LINKS,
MOCK_STORES,
MOCK_USER,
defaultAddress,
@@ -314,6 +318,40 @@ export function createMockApi(): ApiClient {
listMyInvoices: () => Promise.resolve(state.invoices.map((i) => ({ ...i }))),
// Mirror of the seeded storefront-content rows, so the home page renders
// identically when every domain is configured to fixed data.
getHomeContent: (): Promise<HomeContent> =>
Promise.resolve({
banners: MOCK_BANNERS.map((b, i) => ({
id: `bn${i + 1}`,
image: b.image,
url: b.url,
position: i,
active: true,
})),
promos: MOCK_PROMOS.map((p, i) => ({
id: `pr${i + 1}`,
image: p.image,
url: p.url,
position: i,
active: true,
})),
quick_links: MOCK_QUICK_LINKS.map((q, i) => ({
id: `ql${i + 1}`,
label: q.label,
url: q.url,
glyph: q.glyph,
position: i,
active: true,
})),
floor_adverts: Array.from({ length: 6 }, (_, i) => ({
id: `fa${i + 1}`,
image: `/mock/floor-adv-${i + 1}.svg`,
position: i,
active: true,
})),
}),
shop: {
getMyShop: () => unsupported(),
listMyProducts: () => unsupported(),
@@ -341,6 +379,8 @@ export function createMockApi(): ApiClient {
listCurrencies: () => unsupported(),
upsertCurrency: () => unsupported(),
setRate: () => unsupported(),
getContent: () => unsupported(),
replaceContent: () => unsupported(),
},
};
}