Initial commit - PromptTech
This commit is contained in:
332
docs/features/FEATURE_MULTI_IMAGE_RICHTEXT.md
Normal file
332
docs/features/FEATURE_MULTI_IMAGE_RICHTEXT.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user