Files
PromptTech/docs/features/FEATURE_MULTI_IMAGE_RICHTEXT.md

333 lines
9.3 KiB
Markdown

# 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`**
```sql
- 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`)
```python
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: `/uploads``backend/uploads/`
### Pydantic Models
```python
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`)
```python
"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
```javascript
// 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
```javascript
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)
```javascript
// 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`)
```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