9.3 KiB
9.3 KiB
Product Rich Text Editor & Multi-Image Upload Feature
Overview
This feature enhancement adds:
- Rich Text Editor for product descriptions with HTML support
- Multiple Image Upload system with drag-and-drop reordering
- 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_idonproduct_ididx_product_images_display_orderon(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
filefield - Returns:
{url, filename} - Validates: Only image file types
- Saves to:
backend/uploads/products/
- Accepts: multipart/form-data with
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
- Body:
-
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
- Body:
Updated Product CRUD
-
POST
/api/admin/products- Create product with images- Accepts
images: List[str]in request body - Creates ProductImage records for each URL
- Accepts
-
PUT
/api/admin/products/{product_id}- Update product with images- Accepts
images: List[str](optional) - Replaces all existing images if provided
- Accepts
-
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:
/uploads→backend/uploads/
- Mounted:
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
- Open "Add Product" dialog in Admin Dashboard
- Fill in product name, price, category, etc.
- Use rich text editor to write formatted description
- Click "Add Images" button to upload multiple images
- Drag images to reorder (first = primary)
- Click "Create" - product saved with all images
Admin Editing Product
- Click Edit on existing product
- Dialog loads with current data including images
- Rich text editor shows formatted description
- Images displayed in grid with reorder/delete options
- Add more images or reorder existing ones
- Click "Update" - changes saved
Customer Viewing Product
- Browse products - sees primary image on card
- Click product for details
- Image carousel displays all product images
- Navigate with arrows or click thumbnails
- Description renders with HTML formatting
Backwards Compatibility
image_urlfield 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_urlfor 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
/uploadsdirectory - Admin authentication required for all mutations