# WP2 — Catalog (home · products · product detail · categories)

**Repo:** FE only — `/home/moonui4/public_html/moon-erp` (branch `hazemdev4`)
**Depends on:** WP0 ✅ `41e14035`+`9a04a6e8` · WP1 ✅ `05744239` · WPS ✅ (BE seeder `08804f477`)
**Review:** code-reviewer · **[FIN]:** no · **Migration:** no

## Goal

Fill the shell WP1 built with the four catalog pages, driven entirely by the real API. When this WP is done the owner can open the store, browse a live catalog, filter and search it, and open a product — the first genuinely usable slice of the storefront.

## Pages (theme markup is the design source of truth)

Theme, read-only:
`/tmp/claude-0/-home-moonui4-public-html/714ac5d7-2ce4-45d7-bc22-9a935e3e9336/scratchpad/theme/theme/`

| Route | Theme page | Content |
|---|---|---|
| `/` | `index.html` | hero slider · category tiles · product carousels · promo/offer sections |
| `/products` | `products.html` | filter sidebar · sort · pagination · search results |
| `/product/:id` | `product-detail.html` | gallery · price · quantity stepper · tabs · reviews |
| `/categories` | `categories.html` | full category grid |

Everything **between** the shell's header and footer is yours; the shell itself is done — reuse it, do not re-implement it.

**T1 (verified):** the shell region is byte-identical across all 29 pages, so anything you find above line 295 or below the footer in these pages is already built. Port only the page bodies.

## Shared components to extract (build once, reuse)

`ProductCard` · `ProductCarousel` · `CategoryTile` · `QuantityStepper` · `PriceDisplay` · `RatingStars` · `Pagination` · `ProductSkeleton` (loading state).

The theme repeats product-card markup many times with slight drift; pick the **home page** variant as canonical and note any deliberate difference. **Fix in the shared layer** — if a defect shows up on several pages, it gets one fix in the shared component, never a per-page patch.

## Contract facts you must honour (verified in the backend — these override the theme)

| # | Fact | What it means here |
|---|---|---|
| A8 | Money is **inconsistent**: decimals serialize as strings (`"25.500"`), computed fields as numbers (`51.0`) | WP0's HTTP layer already normalizes. **Never `parseFloat` in a component.** If a money field is missing from the normalize key lists, add it there — not locally |
| A9 | `CoercesNullDefaults` — the API **never returns null**. `category_id === 0` means "none"; `average_rating` is **omitted** when absent | falsy checks, never `=== null`; `average_rating?: number` |
| A14 | `category_id` filter is **NON-RECURSIVE** — verified empirically (per-category totals sum to exactly the catalog size) | A parent category can legitimately return zero products. **The current seed happens to attach products to every category including roots, so parents look fine — that is a property of the fixture, not a guarantee.** Do not build logic that assumes parents always have products; render a real empty state |
| A15 | `per_page` is **ignored** on `/categories` (hard `paginate(50)`); products cap `per_page` at 100 | don't send a page size to categories and expect it to apply |
| — | `categories/tree` is **unpaginated** `{data:[...]}`; products use the standard Laravel envelope `{data, links, meta:{current_page,last_page,per_page,total}}` | two different shapes — WP1's `CategoryState` already handles the tree |

**Do not call `/api/store/admin/*` — ever.** Public catalog is `/api/store/...`.

## Real data available (seeded, verified live)

24 bilingual products across 5 categories (3 roots + 2 children), 1 active offer over 2 products, 12 `store.*` settings. Deliberate edge cases **you must render correctly, not code around**:

- **6 products with an empty image** (`DEMO-P023, P019, P015, P009, P005, P004`) → needs a real placeholder (**OPEN-3 is still open; use a local neutral placeholder** — it is the recorded recommendation and is trivially swappable)
- **1 out of stock** (`DEMO-P012`) → disabled add-to-cart + a clear badge
- **2 discounted** via `/offers`, one with `custom_price 19.500` and one with `custom_price null` that falls back to a 20% rule → the discount UI must handle **both** shapes
- `average_rating` may be **absent entirely** on a product

If a page looks wrong against this data, the data is telling you something real — investigate before styling around it.

## Traps

⚠️ **T5 — `index.html` ships TWO conflicting hero-slider implementations.** Port the **inline one in the page tail** (RTL-aware and correct). `app.js`'s version is buggy legacy — ignore it entirely.

🔴 **T2 — do NOT port `style-ar.css` / `style-en.css`.** Every rule is keyed on `body[dir=...]` while `dir` lives on `<html>`, so ~380 lines never applied in the theme; adopting them would change the rendering for the first time. Use Tailwind logical utilities. WP6 audits those files.

⚠️ **RTL direction reads backwards.** WP1 hit exactly this: the theme is RTL-only, so its physical `right` is the inline **start**. Do not translate `right`→`end` mechanically — verify against the rendered theme.

⚠️ **T3 — Tailwind is compiled now, not the CDN.** Dynamically assembled class names (`` `bg-${x}-500` ``) **silently do not exist**. Literal complete class names only.

⚠️ **T9 — all theme imagery is external Unsplash** with a broken `onerror` that points at the same URL. Real products carry `image` from the API; wire the real pipeline and a working fallback.

## i18n

Every user-facing string is a key in **both** `ar.json` and `en.json` — no exceptions, and no Arabic literals in `.ts`/`.html`. **Bump `I18N_VERSION`** (currently `20260718b`) on any i18n edit or clients see raw keys after deploy.

Product/category **names come from the API** already localized via `Accept-Language` — do not translate those client-side.

## Acceptance criteria

- [ ] `npx ng build storefront` green **and** `npx ng build` (admin) still green. `angular.json`'s `moon-erp` block and everything under `src/` untouched.
- [ ] All four pages render against the **live moonui4 API** with real seeded data — state what you actually observed, per page.
- [ ] Filtering, sorting, search and pagination work against the API (not client-side over one page of results).
- [ ] The 6 image-less products show the placeholder; `DEMO-P012` shows out-of-stock with add-to-cart disabled; both discount shapes render correctly; a product without `average_rating` does not crash.
- [ ] An empty category (zero products) renders a proper empty state, not a blank region or a spinner forever.
- [ ] A failed API call degrades visibly (message or retry), never a blank page or an infinite skeleton.
- [ ] Visual fidelity checked by **screenshot diff against the served theme pages** at desktop + mobile. If you compare by eye instead, say so plainly — do not imply more rigor than you had.
- [ ] Zero hardcoded user-facing strings — grep your own diff and paste the result. `I18N_VERSION` bumped.

## Out of scope

No cart mutation logic (WP3), no auth screens or guards (WP4), no checkout (WP5). Wire "add to cart" to WP0's existing `cart.state` signal only — the merge-on-login replay is WP3's. No account/wishlist/prescription/loyalty/CMS pages at all. Do not touch the backend, `src/`, or `angular.json`'s `moon-erp` block.

⛔ **Do not push, deploy, run `/fullpush`, or merge to `main`.** The orchestrator handles the preview deploy.

## Commit

Conventional commit on `hazemdev4`. This WP **does** deliver a user-visible capability, so add **one bilingual bullet** under `## [Unreleased]` in `/home/moonui4/moon-erp-be/docs/moonstack/CHANGELOG.md`:
`- **Headline.** English body {{ar}} **العنوان.** النص العربي` — then verify the file is still well-formed (both halves around `{{ar}}`, heading intact, no truncated bullet).

## Report back

Report what you **verified with evidence** versus what you assumed. If anything in this brief contradicts what you find in the code, the theme, or the data — **say so**. The brief is a best current understanding, not gospel; two earlier WPs each found real errors in their own briefs.
