Files
PromptTech/docs/features/FEATURE_MULTI_IMAGE_RICHTEXT.md

9.3 KiB

Product Rich Text Editor & Multi-Image Upload Feature

Overview

This feature enhancement adds:

  1. Rich Text Editor for product descriptions with HTML support
  2. Multiple Image Upload system with drag-and-drop reordering
  3. Image Carousel for frontend product display

Backend Changes

Database Schema

New Table: product_images

- id (UUID, Primary Key)
- product_id (UUID, Foreign Key  products.id, CASCADE DELETE)
- image_url (VARCHAR, NOT NULL)
- display_order (INTEGER, DEFAULT 0)
- is_primary (BOOLEAN, DEFAULT FALSE)
- created_at (TIMESTAMP)

Indexes:

  • idx_product_images_product_id on product_id
  • idx_product_images_display_order on (product_id, display_order)

Models (backend/models.py)

class ProductImage(Base):
    __tablename__ = "product_images"
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    product_id = Column(UUID(as_uuid=True), ForeignKey("products.id", ondelete="CASCADE"))
    image_url = Column(String, nullable=False)
    display_order = Column(Integer, default=0)
    is_primary = Column(Boolean, default=False)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    
    product = relationship("Product", back_populates="images")

# Updated Product model:
images = relationship("ProductImage", back_populates="product", 
                     cascade="all, delete-orphan", 
                     order_by="ProductImage.display_order")

API Endpoints (backend/server.py)

Image Upload

  • POST /api/upload/image - Upload single image
    • Accepts: multipart/form-data with file field
    • Returns: {url, filename}
    • Validates: Only image file types
    • Saves to: backend/uploads/products/

Product Image Management

  • POST /api/admin/products/{product_id}/images - Add multiple images

    • Body: {image_urls: List[str]}
    • Auto-sets first image as primary if none exist
  • DELETE /api/admin/products/{product_id}/images/{image_id} - Delete image

    • Removes from database and filesystem
  • PUT /api/admin/products/{product_id}/images/reorder - Reorder images

    • Body: {image_id: display_order} mapping

Updated Product CRUD

  • POST /api/admin/products - Create product with images

    • Accepts images: List[str] in request body
    • Creates ProductImage records for each URL
  • PUT /api/admin/products/{product_id} - Update product with images

    • Accepts images: List[str] (optional)
    • Replaces all existing images if provided
  • GET /api/products - List products (includes images array)

  • GET /api/products/{id} - Get product detail (includes images array)

  • GET /api/admin/products - Admin product list (includes images array)

Static File Serving

  • GET /uploads/** - Serve uploaded images
    • Mounted: /uploadsbackend/uploads/

Pydantic Models

class ProductCreate(BaseModel):
    images: List[str] = []  # Added
    description: str  # Now supports HTML

class ProductUpdate(BaseModel):
    images: Optional[List[str]] = None  # Added
    description: Optional[str] = None  # Supports HTML

Serializer (product_to_dict)

"images": [
    {
        "id": str(img.id),
        "image_url": img.image_url,
        "display_order": img.display_order,
        "is_primary": img.is_primary
    }
    for img in sorted(product.images, key=lambda x: x.display_order)
]

Frontend Changes

New Components

1. RichTextEditor (frontend/src/components/RichTextEditor.js)

  • Uses TipTap editor with StarterKit
  • Features:
    • Bold, Italic formatting
    • Heading 2
    • Bullet/Numbered lists
    • Blockquotes
    • Undo/Redo
  • Props: {content, onChange, placeholder}
  • Returns HTML string

2. ImageUploadManager (frontend/src/components/ImageUploadManager.js)

  • Multi-image upload with preview
  • Features:
    • File upload button
    • Drag-and-drop reordering
    • Delete individual images
    • Primary image indicator (first = primary)
    • Image preview grid
  • Props: {images, onChange, token}

3. ProductImageCarousel (frontend/src/components/ProductImageCarousel.js)

  • Image slider with thumbnails
  • Features:
    • Navigation arrows (prev/next)
    • Thumbnail strip navigation
    • Image counter
    • Keyboard navigation support
    • Responsive design
  • Props: {images, alt}

Updated Pages

AdminDashboard.js

// Added imports
import RichTextEditor from '../components/RichTextEditor';
import ImageUploadManager from '../components/ImageUploadManager';

// Updated product form state
productForm: {
  images: [],  // New field
  description: ""  // Now holds HTML
}

// Updated dialog
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
  <RichTextEditor 
    content={productForm.description}
    onChange={(html) => setProductForm({...productForm, description: html})}
  />
  
  <ImageUploadManager
    images={productForm.images}
    onChange={(images) => setProductForm({...productForm, images})}
    token={token}
  />
</DialogContent>

// Edit product - populate images
setProductForm({
  images: product.images?.map(img => img.image_url) || []
})

ProductDetail.js

import ProductImageCarousel from '../components/ProductImageCarousel';

// Replace single image with carousel
<ProductImageCarousel 
  images={product.images?.length > 0 ? product.images : [product.image_url]} 
  alt={product.name}
/>

// Display HTML description
<div 
  className="prose prose-sm"
  dangerouslySetInnerHTML={{ __html: product.description }}
/>

ProductCard.js (Products listing)

// Get primary image or first image
const getImageUrl = () => {
  if (product.images?.length > 0) {
    const primaryImage = product.images.find(img => img.is_primary) || product.images[0];
    const url = primaryImage.image_url || primaryImage;
    return url.startsWith('/uploads') 
      ? `${process.env.REACT_APP_BACKEND_URL}${url}` 
      : url;
  }
  return product.image_url;
};

<img src={getImageUrl()} alt={product.name} />

CSS Styles (frontend/src/index.css)

/* TipTap Editor Styles */
.ProseMirror { outline: none; }
.ProseMirror h2 { font-size: 1.5em; font-weight: 600; }
.ProseMirror ul, .ProseMirror ol { padding-left: 1.5em; }
.ProseMirror blockquote { border-left: 3px solid; padding-left: 1em; }

/* Product Description Display */
.prose h2 { font-size: 1.5em; font-weight: 600; }
.prose ul, .prose ol { padding-left: 1.5em; }
.prose blockquote { border-left: 3px solid; font-style: italic; }

Usage Flow

Admin Creating Product with Multiple Images

  1. Open "Add Product" dialog in Admin Dashboard
  2. Fill in product name, price, category, etc.
  3. Use rich text editor to write formatted description
  4. Click "Add Images" button to upload multiple images
  5. Drag images to reorder (first = primary)
  6. Click "Create" - product saved with all images

Admin Editing Product

  1. Click Edit on existing product
  2. Dialog loads with current data including images
  3. Rich text editor shows formatted description
  4. Images displayed in grid with reorder/delete options
  5. Add more images or reorder existing ones
  6. Click "Update" - changes saved

Customer Viewing Product

  1. Browse products - sees primary image on card
  2. Click product for details
  3. Image carousel displays all product images
  4. Navigate with arrows or click thumbnails
  5. Description renders with HTML formatting

Backwards Compatibility

  • image_url field retained for legacy support
  • Products without images fallback to image_url
  • Frontend handles both old (single URL) and new (images array)
  • Database migrations non-breaking

File Structure

backend/
  ├── models.py           # Added ProductImage model
  ├── server.py           # Added endpoints, updated CRUD
  └── uploads/
      └── products/       # Image storage directory

frontend/src/
  ├── components/
  │   ├── RichTextEditor.js       # NEW
  │   ├── ImageUploadManager.js   # NEW
  │   └── ProductImageCarousel.js # NEW
  ├── pages/
  │   ├── AdminDashboard.js       # Updated
  │   ├── ProductDetail.js        # Updated
  │   └── Products.js             # Updated (via ProductCard)
  └── index.css                   # Added prose/editor styles

Dependencies

Already Installed:

  • @tiptap/react - Rich text editor
  • @tiptap/starter-kit - Editor extensions
  • @tiptap/extension-placeholder - Placeholder text

Backend:

  • shutil - File operations (built-in)
  • uuid - Unique filenames (built-in)

Testing Checklist

  • Create product with multiple images
  • Upload images via drag-and-drop
  • Reorder images by dragging
  • Delete individual images
  • Edit product preserves existing images
  • Rich text formatting saves correctly
  • HTML renders properly on product detail page
  • Image carousel navigation works
  • Primary image displays on product cards
  • Fallback to image_url for legacy products
  • Image files serve correctly via /uploads
  • Deleting product cascades to delete images

Security Notes

  • File upload validates image MIME types only
  • Unique UUIDs prevent filename conflicts
  • CASCADE DELETE ensures orphaned images cleaned up
  • Static file serving restricted to /uploads directory
  • Admin authentication required for all mutations