revamping documentation
This commit is contained in:
698
docs/architecture/theme-system/overview.md
Normal file
698
docs/architecture/theme-system/overview.md
Normal file
@@ -0,0 +1,698 @@
|
||||
# Multi-Theme Shop System - Complete Implementation Guide
|
||||
|
||||
## 🎨 Overview
|
||||
|
||||
This guide explains how to implement vendor-specific themes in your FastAPI multi-tenant e-commerce platform, allowing each vendor to have their own unique shop design, colors, branding, and layout.
|
||||
|
||||
## What You're Building
|
||||
|
||||
**Before:**
|
||||
- All vendor shops look the same
|
||||
- Same colors, fonts, layouts
|
||||
- Only vendor name changes
|
||||
|
||||
**After:**
|
||||
- Each vendor has unique theme
|
||||
- Custom colors, fonts, logos
|
||||
- Different layouts per vendor
|
||||
- Vendor-specific branding
|
||||
- CSS customization support
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Request → Vendor Middleware → Theme Middleware → Template Rendering
|
||||
↓ ↓ ↓
|
||||
Sets vendor Loads theme Applies styles
|
||||
in request config for and branding
|
||||
state vendor
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
1. Customer visits: customdomain1.com
|
||||
2. Vendor middleware: Identifies Vendor 1
|
||||
3. Theme middleware: Loads Vendor 1's theme
|
||||
4. Template receives:
|
||||
- vendor: Vendor 1 object
|
||||
- theme: Vendor 1 theme config
|
||||
5. Template renders with:
|
||||
- Vendor 1 colors
|
||||
- Vendor 1 logo
|
||||
- Vendor 1 layout preferences
|
||||
- Vendor 1 custom CSS
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Add Theme Database Table
|
||||
|
||||
Create the `vendor_themes` table:
|
||||
|
||||
```sql
|
||||
CREATE TABLE vendor_themes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vendor_id INTEGER UNIQUE NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
|
||||
theme_name VARCHAR(100) DEFAULT 'default',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
-- Colors (JSON)
|
||||
colors JSONB DEFAULT '{
|
||||
"primary": "#6366f1",
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#ec4899",
|
||||
"background": "#ffffff",
|
||||
"text": "#1f2937",
|
||||
"border": "#e5e7eb"
|
||||
}'::jsonb,
|
||||
|
||||
-- Typography
|
||||
font_family_heading VARCHAR(100) DEFAULT 'Inter, sans-serif',
|
||||
font_family_body VARCHAR(100) DEFAULT 'Inter, sans-serif',
|
||||
|
||||
-- Branding
|
||||
logo_url VARCHAR(500),
|
||||
logo_dark_url VARCHAR(500),
|
||||
favicon_url VARCHAR(500),
|
||||
banner_url VARCHAR(500),
|
||||
|
||||
-- Layout
|
||||
layout_style VARCHAR(50) DEFAULT 'grid',
|
||||
header_style VARCHAR(50) DEFAULT 'fixed',
|
||||
product_card_style VARCHAR(50) DEFAULT 'modern',
|
||||
|
||||
-- Customization
|
||||
custom_css TEXT,
|
||||
social_links JSONB DEFAULT '{}'::jsonb,
|
||||
|
||||
-- Meta
|
||||
meta_title_template VARCHAR(200),
|
||||
meta_description TEXT,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vendor_themes_vendor_id ON vendor_themes(vendor_id);
|
||||
CREATE INDEX idx_vendor_themes_active ON vendor_themes(vendor_id, is_active);
|
||||
```
|
||||
|
||||
### Step 2: Create VendorTheme Model
|
||||
|
||||
File: `models/database/vendor_theme.py`
|
||||
|
||||
See the complete model in `/home/claude/vendor_theme_model.py`
|
||||
|
||||
**Key features:**
|
||||
- JSON fields for flexible color schemes
|
||||
- Brand asset URLs (logo, favicon, banner)
|
||||
- Layout preferences
|
||||
- Custom CSS support
|
||||
- CSS variables generator
|
||||
- to_dict() for template rendering
|
||||
|
||||
### Step 3: Update Vendor Model
|
||||
|
||||
Add theme relationship to `models/database/vendor.py`:
|
||||
|
||||
```python
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class Vendor(Base):
|
||||
# ... existing fields ...
|
||||
|
||||
# Add theme relationship
|
||||
theme = relationship(
|
||||
"VendorTheme",
|
||||
back_populates="vendor",
|
||||
uselist=False, # One-to-one relationship
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
@property
|
||||
def active_theme(self):
|
||||
"""Get vendor's active theme or return None"""
|
||||
if self.theme and self.theme.is_active:
|
||||
return self.theme
|
||||
return None
|
||||
```
|
||||
|
||||
### Step 4: Create Theme Context Middleware
|
||||
|
||||
File: `middleware/theme_context.py`
|
||||
|
||||
See complete middleware in `/home/claude/theme_context_middleware.py`
|
||||
|
||||
**What it does:**
|
||||
1. Runs AFTER vendor_context_middleware
|
||||
2. Loads theme for detected vendor
|
||||
3. Injects theme into request.state
|
||||
4. Falls back to default theme if needed
|
||||
|
||||
**Add to main.py:**
|
||||
```python
|
||||
from middleware.theme_context import theme_context_middleware
|
||||
|
||||
# AFTER vendor_context_middleware
|
||||
app.middleware("http")(theme_context_middleware)
|
||||
```
|
||||
|
||||
### Step 5: Create Shop Base Template
|
||||
|
||||
File: `app/templates/shop/base.html`
|
||||
|
||||
See complete template in `/home/claude/shop_base_template.html`
|
||||
|
||||
**Key features:**
|
||||
- Injects CSS variables from theme
|
||||
- Vendor-specific logo (light/dark mode)
|
||||
- Theme-aware header/footer
|
||||
- Social links from theme config
|
||||
- Custom CSS injection
|
||||
- Dynamic favicon
|
||||
- SEO meta tags
|
||||
|
||||
**Template receives:**
|
||||
```python
|
||||
{
|
||||
"vendor": vendor_object, # From vendor middleware
|
||||
"theme": theme_dict, # From theme middleware
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Create Shop Layout JavaScript
|
||||
|
||||
File: `static/shop/js/shop-layout.js`
|
||||
|
||||
See complete code in `/home/claude/shop_layout.js`
|
||||
|
||||
**Provides:**
|
||||
- Theme toggling (light/dark)
|
||||
- Cart management
|
||||
- Mobile menu
|
||||
- Search overlay
|
||||
- Toast notifications
|
||||
- Price formatting
|
||||
- Date formatting
|
||||
|
||||
### Step 7: Update Route Handlers
|
||||
|
||||
Ensure theme is passed to templates:
|
||||
|
||||
```python
|
||||
from middleware.theme_context import get_current_theme
|
||||
|
||||
@router.get("/")
|
||||
async def shop_home(request: Request, db: Session = Depends(get_db)):
|
||||
vendor = request.state.vendor
|
||||
theme = get_current_theme(request) # or request.state.theme
|
||||
|
||||
# Get products for vendor
|
||||
products = db.query(Product).filter(
|
||||
Product.vendor_id == vendor.id,
|
||||
Product.is_active == True
|
||||
).all()
|
||||
|
||||
return templates.TemplateResponse("shop/home.html", {
|
||||
"request": request,
|
||||
"vendor": vendor,
|
||||
"theme": theme,
|
||||
"products": products
|
||||
})
|
||||
```
|
||||
|
||||
**Note:** If middleware is set up correctly, theme is already in `request.state.theme`, so you may not need to explicitly pass it!
|
||||
|
||||
## How Themes Work
|
||||
|
||||
### CSS Variables System
|
||||
|
||||
Each theme generates CSS custom properties:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-primary: #6366f1;
|
||||
--color-secondary: #8b5cf6;
|
||||
--color-accent: #ec4899;
|
||||
--color-background: #ffffff;
|
||||
--color-text: #1f2937;
|
||||
--color-border: #e5e7eb;
|
||||
--font-heading: Inter, sans-serif;
|
||||
--font-body: Inter, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in HTML/CSS:**
|
||||
```html
|
||||
<!-- In templates -->
|
||||
<button style="background-color: var(--color-primary)">
|
||||
Click Me
|
||||
</button>
|
||||
|
||||
<h1 style="font-family: var(--font-heading)">
|
||||
Welcome
|
||||
</h1>
|
||||
```
|
||||
|
||||
```css
|
||||
/* In stylesheets */
|
||||
.btn-primary {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-family: var(--font-heading);
|
||||
color: var(--color-text);
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Configuration Example
|
||||
|
||||
```python
|
||||
# Example theme for "Modern Electronics Store"
|
||||
theme = {
|
||||
"theme_name": "tech-modern",
|
||||
"colors": {
|
||||
"primary": "#2563eb", # Blue
|
||||
"secondary": "#0ea5e9", # Sky Blue
|
||||
"accent": "#f59e0b", # Amber
|
||||
"background": "#ffffff",
|
||||
"text": "#111827",
|
||||
"border": "#e5e7eb"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Roboto, sans-serif",
|
||||
"body": "Open Sans, sans-serif"
|
||||
},
|
||||
"branding": {
|
||||
"logo": "/media/vendors/tech-store/logo.png",
|
||||
"logo_dark": "/media/vendors/tech-store/logo-dark.png",
|
||||
"favicon": "/media/vendors/tech-store/favicon.ico",
|
||||
"banner": "/media/vendors/tech-store/banner.jpg"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed",
|
||||
"product_card": "modern"
|
||||
},
|
||||
"social_links": {
|
||||
"facebook": "https://facebook.com/techstore",
|
||||
"instagram": "https://instagram.com/techstore",
|
||||
"twitter": "https://twitter.com/techstore"
|
||||
},
|
||||
"custom_css": """
|
||||
.product-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
"""
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Theme Presets
|
||||
|
||||
You can create predefined theme templates:
|
||||
|
||||
```python
|
||||
# app/core/theme_presets.py
|
||||
|
||||
THEME_PRESETS = {
|
||||
"modern": {
|
||||
"colors": {
|
||||
"primary": "#6366f1",
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#ec4899",
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Inter, sans-serif",
|
||||
"body": "Inter, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed"
|
||||
}
|
||||
},
|
||||
|
||||
"classic": {
|
||||
"colors": {
|
||||
"primary": "#1e40af",
|
||||
"secondary": "#7c3aed",
|
||||
"accent": "#dc2626",
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Georgia, serif",
|
||||
"body": "Arial, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "list",
|
||||
"header": "static"
|
||||
}
|
||||
},
|
||||
|
||||
"minimal": {
|
||||
"colors": {
|
||||
"primary": "#000000",
|
||||
"secondary": "#404040",
|
||||
"accent": "#666666",
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Helvetica, sans-serif",
|
||||
"body": "Helvetica, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "transparent"
|
||||
}
|
||||
},
|
||||
|
||||
"vibrant": {
|
||||
"colors": {
|
||||
"primary": "#f59e0b",
|
||||
"secondary": "#ef4444",
|
||||
"accent": "#8b5cf6",
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Poppins, sans-serif",
|
||||
"body": "Open Sans, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "masonry",
|
||||
"header": "fixed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def apply_preset(theme: VendorTheme, preset_name: str):
|
||||
"""Apply a preset to a vendor theme"""
|
||||
if preset_name not in THEME_PRESETS:
|
||||
raise ValueError(f"Unknown preset: {preset_name}")
|
||||
|
||||
preset = THEME_PRESETS[preset_name]
|
||||
|
||||
theme.theme_name = preset_name
|
||||
theme.colors = preset["colors"]
|
||||
theme.font_family_heading = preset["fonts"]["heading"]
|
||||
theme.font_family_body = preset["fonts"]["body"]
|
||||
theme.layout_style = preset["layout"]["style"]
|
||||
theme.header_style = preset["layout"]["header"]
|
||||
|
||||
return theme
|
||||
```
|
||||
|
||||
## Admin Interface for Theme Management
|
||||
|
||||
Create admin endpoints for managing themes:
|
||||
|
||||
```python
|
||||
# app/api/v1/admin/vendor_themes.py
|
||||
|
||||
@router.get("/vendors/{vendor_id}/theme")
|
||||
def get_vendor_theme(vendor_id: int, db: Session = Depends(get_db)):
|
||||
"""Get theme configuration for vendor"""
|
||||
theme = db.query(VendorTheme).filter(
|
||||
VendorTheme.vendor_id == vendor_id
|
||||
).first()
|
||||
|
||||
if not theme:
|
||||
# Return default theme
|
||||
return get_default_theme()
|
||||
|
||||
return theme.to_dict()
|
||||
|
||||
|
||||
@router.put("/vendors/{vendor_id}/theme")
|
||||
def update_vendor_theme(
|
||||
vendor_id: int,
|
||||
theme_data: dict,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update or create theme for vendor"""
|
||||
|
||||
theme = db.query(VendorTheme).filter(
|
||||
VendorTheme.vendor_id == vendor_id
|
||||
).first()
|
||||
|
||||
if not theme:
|
||||
theme = VendorTheme(vendor_id=vendor_id)
|
||||
db.add(theme)
|
||||
|
||||
# Update fields
|
||||
if "colors" in theme_data:
|
||||
theme.colors = theme_data["colors"]
|
||||
|
||||
if "fonts" in theme_data:
|
||||
theme.font_family_heading = theme_data["fonts"].get("heading")
|
||||
theme.font_family_body = theme_data["fonts"].get("body")
|
||||
|
||||
if "branding" in theme_data:
|
||||
theme.logo_url = theme_data["branding"].get("logo")
|
||||
theme.logo_dark_url = theme_data["branding"].get("logo_dark")
|
||||
theme.favicon_url = theme_data["branding"].get("favicon")
|
||||
|
||||
if "layout" in theme_data:
|
||||
theme.layout_style = theme_data["layout"].get("style")
|
||||
theme.header_style = theme_data["layout"].get("header")
|
||||
|
||||
if "custom_css" in theme_data:
|
||||
theme.custom_css = theme_data["custom_css"]
|
||||
|
||||
db.commit()
|
||||
db.refresh(theme)
|
||||
|
||||
return theme.to_dict()
|
||||
|
||||
|
||||
@router.post("/vendors/{vendor_id}/theme/preset/{preset_name}")
|
||||
def apply_theme_preset(
|
||||
vendor_id: int,
|
||||
preset_name: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Apply a preset theme to vendor"""
|
||||
from app.core.theme_presets import apply_preset, THEME_PRESETS
|
||||
|
||||
if preset_name not in THEME_PRESETS:
|
||||
raise HTTPException(400, f"Unknown preset: {preset_name}")
|
||||
|
||||
theme = db.query(VendorTheme).filter(
|
||||
VendorTheme.vendor_id == vendor_id
|
||||
).first()
|
||||
|
||||
if not theme:
|
||||
theme = VendorTheme(vendor_id=vendor_id)
|
||||
db.add(theme)
|
||||
|
||||
apply_preset(theme, preset_name)
|
||||
db.commit()
|
||||
db.refresh(theme)
|
||||
|
||||
return {
|
||||
"message": f"Applied {preset_name} preset",
|
||||
"theme": theme.to_dict()
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Different Themes for Different Vendors
|
||||
|
||||
### Vendor 1: Tech Electronics Store
|
||||
```python
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#2563eb", # Blue
|
||||
"secondary": "#0ea5e9",
|
||||
"accent": "#f59e0b"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Roboto, sans-serif",
|
||||
"body": "Open Sans, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vendor 2: Fashion Boutique
|
||||
```python
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#ec4899", # Pink
|
||||
"secondary": "#f472b6",
|
||||
"accent": "#fbbf24"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Playfair Display, serif",
|
||||
"body": "Lato, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "masonry",
|
||||
"header": "transparent"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vendor 3: Organic Food Store
|
||||
```python
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#10b981", # Green
|
||||
"secondary": "#059669",
|
||||
"accent": "#f59e0b"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Merriweather, serif",
|
||||
"body": "Source Sans Pro, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "list",
|
||||
"header": "static"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Themes
|
||||
|
||||
### Test 1: View Different Vendor Themes
|
||||
|
||||
```bash
|
||||
# Visit Vendor 1 (Tech store with blue theme)
|
||||
curl http://vendor1.localhost:8000/
|
||||
|
||||
# Visit Vendor 2 (Fashion with pink theme)
|
||||
curl http://vendor2.localhost:8000/
|
||||
|
||||
# Each should have different:
|
||||
# - Colors in CSS variables
|
||||
# - Logo
|
||||
# - Fonts
|
||||
# - Layout
|
||||
```
|
||||
|
||||
### Test 2: Theme API
|
||||
|
||||
```bash
|
||||
# Get vendor theme
|
||||
curl http://localhost:8000/api/v1/admin/vendors/1/theme
|
||||
|
||||
# Update colors
|
||||
curl -X PUT http://localhost:8000/api/v1/admin/vendors/1/theme \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"colors": {
|
||||
"primary": "#ff0000",
|
||||
"secondary": "#00ff00"
|
||||
}
|
||||
}'
|
||||
|
||||
# Apply preset
|
||||
curl -X POST http://localhost:8000/api/v1/admin/vendors/1/theme/preset/modern
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### For Platform Owner
|
||||
- ✅ Premium feature for enterprise vendors
|
||||
- ✅ Differentiate vendor packages (basic vs premium themes)
|
||||
- ✅ Additional revenue stream
|
||||
- ✅ Competitive advantage
|
||||
|
||||
### For Vendors
|
||||
- ✅ Unique brand identity
|
||||
- ✅ Professional appearance
|
||||
- ✅ Better customer recognition
|
||||
- ✅ Customizable to match brand
|
||||
|
||||
### For Customers
|
||||
- ✅ Distinct shopping experiences
|
||||
- ✅ Better brand recognition
|
||||
- ✅ More engaging designs
|
||||
- ✅ Professional appearance
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### 1. Theme Preview
|
||||
Allow vendors to preview themes before applying:
|
||||
|
||||
```python
|
||||
@router.get("/vendors/{vendor_id}/theme/preview/{preset_name}")
|
||||
def preview_theme(vendor_id: int, preset_name: str):
|
||||
"""Generate preview URL for theme"""
|
||||
# Return preview HTML with preset applied
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. Theme Marketplace
|
||||
Create a marketplace of premium themes:
|
||||
|
||||
```python
|
||||
class PremiumTheme(Base):
|
||||
__tablename__ = "premium_themes"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(100))
|
||||
description = Column(Text)
|
||||
price = Column(Numeric(10, 2))
|
||||
preview_image = Column(String(500))
|
||||
config = Column(JSON)
|
||||
```
|
||||
|
||||
### 3. Dark Mode Auto-Detection
|
||||
Respect user's system preferences:
|
||||
|
||||
```javascript
|
||||
// Detect system dark mode preference
|
||||
if (window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
this.dark = true;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Theme Analytics
|
||||
Track which themes perform best:
|
||||
|
||||
```python
|
||||
class ThemeAnalytics(Base):
|
||||
__tablename__ = "theme_analytics"
|
||||
|
||||
theme_id = Column(Integer, ForeignKey("vendor_themes.id"))
|
||||
conversion_rate = Column(Numeric(5, 2))
|
||||
avg_session_duration = Column(Integer)
|
||||
bounce_rate = Column(Numeric(5, 2))
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**What you've built:**
|
||||
- ✅ Vendor-specific theme system
|
||||
- ✅ CSS variables for dynamic styling
|
||||
- ✅ Custom branding (logos, colors, fonts)
|
||||
- ✅ Layout customization
|
||||
- ✅ Custom CSS support
|
||||
- ✅ Theme presets
|
||||
- ✅ Admin theme management
|
||||
|
||||
**Each vendor now has:**
|
||||
- Unique colors and fonts
|
||||
- Custom logo and branding
|
||||
- Layout preferences
|
||||
- Social media links
|
||||
- Custom CSS overrides
|
||||
|
||||
**All controlled by:**
|
||||
- Database configuration
|
||||
- No code changes needed per vendor
|
||||
- Admin panel management
|
||||
- Preview and testing
|
||||
|
||||
**Your architecture supports this perfectly!** The vendor context + theme middleware pattern works seamlessly with your existing Alpine.js frontend.
|
||||
|
||||
Start with the default theme, then let vendors customize their shops! 🎨
|
||||
360
docs/architecture/theme-system/presets.md
Normal file
360
docs/architecture/theme-system/presets.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# THEME PRESETS USAGE GUIDE
|
||||
|
||||
## What Changed in Your Presets
|
||||
|
||||
### ✅ What You Had Right
|
||||
- Good preset structure with colors, fonts, layout
|
||||
- Clean `apply_preset()` function
|
||||
- Good preset names (modern, classic, minimal, vibrant)
|
||||
|
||||
### 🔧 What We Added
|
||||
1. **Missing color fields:** `background`, `text`, `border`
|
||||
2. **Missing layout field:** `product_card` style
|
||||
3. **"default" preset:** Your platform's default theme
|
||||
4. **Extra presets:** "elegant" and "nature" themes
|
||||
5. **Helper functions:** `get_preset()`, `get_available_presets()`, `get_preset_preview()`
|
||||
6. **Custom preset builder:** `create_custom_preset()`
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 1. Apply Preset to New Vendor
|
||||
|
||||
```python
|
||||
from models.database.vendor_theme import VendorTheme
|
||||
from app.core.theme_presets import apply_preset
|
||||
from app.core.database import SessionLocal
|
||||
|
||||
# Create theme for vendor
|
||||
db = SessionLocal()
|
||||
vendor_id = 1
|
||||
|
||||
# Create and apply preset
|
||||
theme = VendorTheme(vendor_id=vendor_id)
|
||||
apply_preset(theme, "modern")
|
||||
|
||||
db.add(theme)
|
||||
db.commit()
|
||||
```
|
||||
|
||||
### 2. Change Vendor's Theme
|
||||
|
||||
```python
|
||||
from models.database.vendor_theme import VendorTheme
|
||||
from app.core.theme_presets import apply_preset
|
||||
|
||||
# Get existing theme
|
||||
theme = db.query(VendorTheme).filter(
|
||||
VendorTheme.vendor_id == vendor_id
|
||||
).first()
|
||||
|
||||
if theme:
|
||||
# Update to new preset
|
||||
apply_preset(theme, "classic")
|
||||
else:
|
||||
# Create new theme
|
||||
theme = VendorTheme(vendor_id=vendor_id)
|
||||
apply_preset(theme, "classic")
|
||||
db.add(theme)
|
||||
|
||||
db.commit()
|
||||
```
|
||||
|
||||
### 3. Get Available Presets (For UI Dropdown)
|
||||
|
||||
```python
|
||||
from app.core.theme_presets import get_available_presets, get_preset_preview
|
||||
|
||||
# Get list of preset names
|
||||
presets = get_available_presets()
|
||||
# Returns: ['default', 'modern', 'classic', 'minimal', 'vibrant', 'elegant', 'nature']
|
||||
|
||||
# Get preview info for UI
|
||||
previews = []
|
||||
for preset_name in presets:
|
||||
preview = get_preset_preview(preset_name)
|
||||
previews.append(preview)
|
||||
|
||||
# Returns list of dicts with:
|
||||
# {
|
||||
# "name": "modern",
|
||||
# "description": "Contemporary tech-inspired design...",
|
||||
# "primary_color": "#6366f1",
|
||||
# "secondary_color": "#8b5cf6",
|
||||
# ...
|
||||
# }
|
||||
```
|
||||
|
||||
### 4. API Endpoint to Apply Preset
|
||||
|
||||
```python
|
||||
# In your API route
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.core.theme_presets import apply_preset, get_available_presets
|
||||
|
||||
@router.put("/theme/preset")
|
||||
def apply_theme_preset(
|
||||
preset_name: str,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Apply a theme preset to vendor."""
|
||||
|
||||
# Validate preset name
|
||||
if preset_name not in get_available_presets():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid preset. Available: {get_available_presets()}"
|
||||
)
|
||||
|
||||
# Get or create vendor theme
|
||||
theme = db.query(VendorTheme).filter(
|
||||
VendorTheme.vendor_id == vendor.id
|
||||
).first()
|
||||
|
||||
if not theme:
|
||||
theme = VendorTheme(vendor_id=vendor.id)
|
||||
db.add(theme)
|
||||
|
||||
# Apply preset
|
||||
apply_preset(theme, preset_name)
|
||||
db.commit()
|
||||
db.refresh(theme)
|
||||
|
||||
return {
|
||||
"message": f"Theme preset '{preset_name}' applied successfully",
|
||||
"theme": theme.to_dict()
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Get All Presets for Theme Selector
|
||||
|
||||
```python
|
||||
@router.get("/theme/presets")
|
||||
def get_theme_presets():
|
||||
"""Get all available theme presets with previews."""
|
||||
from app.core.theme_presets import get_available_presets, get_preset_preview
|
||||
|
||||
presets = []
|
||||
for preset_name in get_available_presets():
|
||||
preview = get_preset_preview(preset_name)
|
||||
presets.append(preview)
|
||||
|
||||
return {"presets": presets}
|
||||
|
||||
# Returns:
|
||||
# {
|
||||
# "presets": [
|
||||
# {
|
||||
# "name": "default",
|
||||
# "description": "Clean and professional...",
|
||||
# "primary_color": "#6366f1",
|
||||
# "secondary_color": "#8b5cf6",
|
||||
# "accent_color": "#ec4899",
|
||||
# "heading_font": "Inter, sans-serif",
|
||||
# "body_font": "Inter, sans-serif",
|
||||
# "layout_style": "grid"
|
||||
# },
|
||||
# ...
|
||||
# ]
|
||||
# }
|
||||
```
|
||||
|
||||
### 6. Create Custom Theme (Not from Preset)
|
||||
|
||||
```python
|
||||
from app.core.theme_presets import create_custom_preset
|
||||
|
||||
# User provides custom colors
|
||||
custom_preset = create_custom_preset(
|
||||
colors={
|
||||
"primary": "#ff0000",
|
||||
"secondary": "#00ff00",
|
||||
"accent": "#0000ff",
|
||||
"background": "#ffffff",
|
||||
"text": "#000000",
|
||||
"border": "#cccccc"
|
||||
},
|
||||
fonts={
|
||||
"heading": "Arial, sans-serif",
|
||||
"body": "Verdana, sans-serif"
|
||||
},
|
||||
layout={
|
||||
"style": "grid",
|
||||
"header": "fixed",
|
||||
"product_card": "modern"
|
||||
},
|
||||
name="my_custom"
|
||||
)
|
||||
|
||||
# Apply to vendor theme
|
||||
theme = VendorTheme(vendor_id=vendor_id)
|
||||
theme.theme_name = "custom"
|
||||
theme.colors = custom_preset["colors"]
|
||||
theme.font_family_heading = custom_preset["fonts"]["heading"]
|
||||
theme.font_family_body = custom_preset["fonts"]["body"]
|
||||
theme.layout_style = custom_preset["layout"]["style"]
|
||||
theme.header_style = custom_preset["layout"]["header"]
|
||||
theme.product_card_style = custom_preset["layout"]["product_card"]
|
||||
theme.is_active = True
|
||||
|
||||
db.add(theme)
|
||||
db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available Presets
|
||||
|
||||
| Preset | Description | Primary Color | Use Case |
|
||||
|--------|-------------|---------------|----------|
|
||||
| `default` | Clean & professional | Indigo (#6366f1) | General purpose |
|
||||
| `modern` | Tech-inspired | Indigo (#6366f1) | Tech products |
|
||||
| `classic` | Traditional | Dark Blue (#1e40af) | Established brands |
|
||||
| `minimal` | Ultra-clean B&W | Black (#000000) | Minimalist brands |
|
||||
| `vibrant` | Bold & energetic | Orange (#f59e0b) | Creative brands |
|
||||
| `elegant` | Sophisticated | Gray (#6b7280) | Luxury products |
|
||||
| `nature` | Eco-friendly | Green (#059669) | Organic/eco brands |
|
||||
|
||||
---
|
||||
|
||||
## Complete Preset Structure
|
||||
|
||||
Each preset includes:
|
||||
|
||||
```python
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#6366f1", # Main brand color
|
||||
"secondary": "#8b5cf6", # Supporting color
|
||||
"accent": "#ec4899", # Call-to-action color
|
||||
"background": "#ffffff", # Page background
|
||||
"text": "#1f2937", # Text color
|
||||
"border": "#e5e7eb" # Border/divider color
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Inter, sans-serif", # Headings (h1-h6)
|
||||
"body": "Inter, sans-serif" # Body text
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid", # grid | list | masonry
|
||||
"header": "fixed", # fixed | static | transparent
|
||||
"product_card": "modern" # modern | classic | minimal
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Admin Panel
|
||||
|
||||
### Theme Editor UI Flow
|
||||
|
||||
1. **Preset Selector**
|
||||
```javascript
|
||||
// Fetch available presets
|
||||
fetch('/api/v1/vendor/theme/presets')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
// Display preset cards with previews
|
||||
data.presets.forEach(preset => {
|
||||
showPresetCard(preset.name, preset.primary_color, preset.description)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
2. **Apply Preset Button**
|
||||
```javascript
|
||||
function applyPreset(presetName) {
|
||||
fetch('/api/v1/vendor/theme/preset', {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({preset_name: presetName})
|
||||
})
|
||||
.then(() => {
|
||||
alert('Theme updated!')
|
||||
location.reload() // Refresh to show new theme
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
3. **Custom Color Picker** (After applying preset)
|
||||
```javascript
|
||||
// User can then customize colors
|
||||
function updateColors(colors) {
|
||||
fetch('/api/v1/vendor/theme/colors', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({colors})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Presets
|
||||
|
||||
```python
|
||||
# Test script
|
||||
from app.core.theme_presets import apply_preset, get_available_presets
|
||||
from models.database.vendor_theme import VendorTheme
|
||||
|
||||
def test_all_presets():
|
||||
"""Test applying all presets"""
|
||||
presets = get_available_presets()
|
||||
|
||||
for preset_name in presets:
|
||||
theme = VendorTheme(vendor_id=999) # Test vendor
|
||||
apply_preset(theme, preset_name)
|
||||
|
||||
assert theme.theme_name == preset_name
|
||||
assert theme.colors is not None
|
||||
assert theme.font_family_heading is not None
|
||||
assert theme.is_active == True
|
||||
|
||||
print(f"✅ {preset_name} preset OK")
|
||||
|
||||
test_all_presets()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSS Variables Generation
|
||||
|
||||
Your middleware already handles this via `VendorTheme.to_dict()`, which includes:
|
||||
|
||||
```python
|
||||
"css_variables": {
|
||||
"--color-primary": "#6366f1",
|
||||
"--color-secondary": "#8b5cf6",
|
||||
"--color-accent": "#ec4899",
|
||||
"--color-background": "#ffffff",
|
||||
"--color-text": "#1f2937",
|
||||
"--color-border": "#e5e7eb",
|
||||
"--font-heading": "Inter, sans-serif",
|
||||
"--font-body": "Inter, sans-serif",
|
||||
}
|
||||
```
|
||||
|
||||
Use in templates:
|
||||
```html
|
||||
<style>
|
||||
:root {
|
||||
{% for key, value in theme.css_variables.items() %}
|
||||
{{ key }}: {{ value }};
|
||||
{% endfor %}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Copy `theme_presets.py` to `app/core/theme_presets.py`
|
||||
2. ✅ Create API endpoints for applying presets
|
||||
3. ✅ Build theme selector UI in admin panel
|
||||
4. ✅ Test all presets work correctly
|
||||
5. ✅ Add custom color picker for fine-tuning
|
||||
|
||||
Perfect! Your presets are now complete and production-ready! 🎨
|
||||
Reference in New Issue
Block a user