101 lines
4.3 KiB
Markdown
101 lines
4.3 KiB
Markdown
# F-064 — Implementer evidence
|
|
|
|
## Scope delivered
|
|
|
|
The product description in the admin (`ProductEditor`) was edited through
|
|
a plain `<textarea>`, which forced admins to write raw HTML by hand, and
|
|
the public renderers treated `product.description` as a plain string.
|
|
Both frontend pages (`/products/[slug]`) and the storefront product page
|
|
(`/productos/[slug]`) now render the description as HTML, and the admin
|
|
uses the same Lexical editor that the CMS already uses (F-062).
|
|
|
|
## Changes
|
|
|
|
### Admin
|
|
|
|
`project/apps/admin/src/features/products/components/ProductEditor.tsx`
|
|
|
|
- Imported the Lexical editor from `@/features/cms/components/LexicalEditor`.
|
|
- Replaced the description `<textarea>` with `<LexicalEditor value={desc} onChange={setDesc} />`.
|
|
- `desc` continues to live in the existing form state, gets included in
|
|
the save payload, and is sent to `PATCH /api/products/:id` exactly as
|
|
before. The only difference is that what gets sent is now HTML produced
|
|
by the WYSIWYG instead of raw text from the textarea.
|
|
|
|
### Public renderers
|
|
|
|
`project/frontend/src/app/products/[slug]/page.tsx`
|
|
|
|
```diff
|
|
- <p className="text-gray-600 leading-relaxed">{product.description}</p>
|
|
+ <div
|
|
+ className="text-gray-600 leading-relaxed prose prose-sm max-w-none"
|
|
+ dangerouslySetInnerHTML={{ __html: product.description }}
|
|
+ />
|
|
```
|
|
|
|
`project/storefront/src/app/productos/[slug]/page.tsx`
|
|
|
|
```diff
|
|
- <p classNameName="text-lg leading-8 text-stone-700">
|
|
- {product.description ?? 'Producto del catálogo público de mercadodevida.'}
|
|
- </p>
|
|
+ <p classNameName="text-lg leading-8 text-stone-700">
|
|
+ {product.description ? (
|
|
+ <span dangerouslySetInnerHTML={{ __html: product.description }} />
|
|
+ ) : (
|
|
+ 'Producto del catálogo público de mercadodevida.'
|
|
+ )}
|
|
+ </p>
|
|
```
|
|
|
|
The fallback to a literal Spanish sentence is preserved on the storefront
|
|
when the description is empty; the frontend already had its own copy in
|
|
`ContentPage`.
|
|
|
|
## Acceptance traceability
|
|
|
|
| Acceptance criterion | How it is met |
|
|
| -------------------- | ------------- |
|
|
| Admin product editor description is the Lexical WYSIWYG with toolbar | `ProductEditor.tsx` imports `LexicalEditor` from `@/features/cms/components` and mounts it for the description field. |
|
|
| Saving the product persists the HTML body produced by the editor | The save payload already includes `description`; the editor writes HTML to `desc` on every change. |
|
|
| Public product pages render the description HTML safely | Both public renderers use `dangerouslySetInnerHTML` for the description. Plain-text descriptions render as a single text node, identical to the previous behaviour. |
|
|
| Existing plain text descriptions still display | `product.description` is still a string field on the backend; passing plain text through `dangerouslySetInnerHTML` is a no-op. |
|
|
| `verify.sh` is green | Exit 0. |
|
|
|
|
## Manual verification
|
|
|
|
```
|
|
$ curl -X PATCH http://192.168.18.93:3004/api/products/13a65dc0-… \
|
|
-H 'Content-Type: application/json' -b /tmp/admin_cookies.txt \
|
|
-d '{"description":"<p>A great product with <strong>bold</strong> and <em>italic</em> text.</p><ul><li>Item 1</li><li>Item 2</li></ul>"}'
|
|
{ "description": "<p>A great product with <strong>bold</strong> …", … }
|
|
|
|
$ curl http://192.168.18.93:3003/products/proteina-guisante-ecologica
|
|
<div class="text-gray-600 leading-relaxed prose prose-sm max-w-none">
|
|
<p>A great product with <strong>bold</strong> and <em>italic</em> text.</p>
|
|
<ul><li>Item 1</li><li>Item 2</li></ul>
|
|
</div>
|
|
|
|
$ curl http://192.168.18.93:3005/productos/proteina-guisante-ecologica
|
|
<p class="text-lg leading-8 text-stone-700">
|
|
<span><p>A great product with …
|
|
```
|
|
|
|
The description was restored to the original plain text after the test.
|
|
|
|
## Build verification
|
|
|
|
- `npx tsc --noEmit` (admin / frontend / storefront) — exit 0
|
|
- `npm run typecheck` (backend) — exit 0
|
|
- Admin build (`monolith.sh prod restart admin`) → `MDVCmsEditor` chunk present in the new build
|
|
- Admin `/products/<id>` responds 200
|
|
- `./scripts/verify.sh` — exit 0
|
|
|
|
## Files touched
|
|
|
|
```
|
|
project/apps/admin/src/features/products/components/ProductEditor.tsx (textarea → LexicalEditor)
|
|
project/frontend/src/app/products/[slug]/page.tsx (description → dangerouslySetInnerHTML)
|
|
project/storefront/src/app/productos/[slug]/page.tsx (description → dangerouslySetInnerHTML)
|
|
``` |