From f25956e9e245c65ece9499ccd4e1791b8a399bd4 Mon Sep 17 00:00:00 2001 From: senke Date: Sun, 22 Feb 2026 14:14:27 +0100 Subject: [PATCH] feat(marketplace): add rich text description with sanitization --- .../ProductDetailViewDescription.tsx | 10 +++- .../CreateProductViewDetailsCard.tsx | 36 +++++++++++++- apps/web/src/utils/sanitize.ts | 28 +++++++++++ .../internal/handlers/marketplace.go | 48 ++++++++++++++++++- 4 files changed, 117 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/marketplace/product-detail-view/ProductDetailViewDescription.tsx b/apps/web/src/components/marketplace/product-detail-view/ProductDetailViewDescription.tsx index 896748e6a..4ea09ce2a 100644 --- a/apps/web/src/components/marketplace/product-detail-view/ProductDetailViewDescription.tsx +++ b/apps/web/src/components/marketplace/product-detail-view/ProductDetailViewDescription.tsx @@ -1,7 +1,9 @@ /** * ProductDetailView — description card. + * v0.401 M1.11: Rich text description with sanitization */ import { Card } from '@/components/ui/card'; +import { sanitizeProductDescription } from '@/utils/sanitize'; import type { Product } from '@/types'; interface ProductDetailViewDescriptionProps { @@ -9,13 +11,19 @@ interface ProductDetailViewDescriptionProps { } export function ProductDetailViewDescription({ product }: ProductDetailViewDescriptionProps) { + const safeDescription = sanitizeProductDescription(product.description ?? ''); + return (

Description

-

{product.description}

+ {safeDescription ? ( +
+ ) : ( +

{product.description || 'No description.'}

+ )} {product.features?.length ? (
    {product.features.map((f: string, i: number) => ( diff --git a/apps/web/src/components/seller/create-product-view/CreateProductViewDetailsCard.tsx b/apps/web/src/components/seller/create-product-view/CreateProductViewDetailsCard.tsx index 47d454d0c..f2cc211ca 100644 --- a/apps/web/src/components/seller/create-product-view/CreateProductViewDetailsCard.tsx +++ b/apps/web/src/components/seller/create-product-view/CreateProductViewDetailsCard.tsx @@ -1,6 +1,8 @@ +import { useRef } from 'react'; import { Card } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; -import { Tag } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Tag, Bold, List } from 'lucide-react'; interface CreateProductViewDetailsCardProps { title: string; @@ -31,6 +33,26 @@ export function CreateProductViewDetailsCard({ keyVal, setKey, }: CreateProductViewDetailsCardProps) { + const descriptionRef = useRef(null); + + const insertAtCursor = (before: string, after: string) => { + const ta = descriptionRef.current; + if (!ta) return; + const start = ta.selectionStart; + const end = ta.selectionEnd; + const text = description; + const selected = text.slice(start, end) || 'text'; + const newText = text.slice(0, start) + before + selected + after + text.slice(end); + setDescription(newText); + setTimeout(() => { + ta.focus(); + ta.setSelectionRange(start + before.length, start + before.length + selected.length); + }, 0); + }; + + const handleBold = () => insertAtCursor('', ''); + const handleList = () => insertAtCursor('\n
      \n
    • ', '
    • \n
    \n'); + return (

    @@ -47,9 +69,19 @@ export function CreateProductViewDetailsCard({ +
    + + + Basic HTML: <b>, <ul><li> +