Multitenant implementation with custom Domain, theme per vendor
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
# VENDOR THEME EDITOR - COMPLETE IMPLEMENTATION GUIDE
|
||||
Following Your Frontend Architecture
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
This implementation follows your **exact Alpine.js architecture pattern** with:
|
||||
- ✅ Proper logging setup
|
||||
- ✅ `...data()` inheritance
|
||||
- ✅ Initialization guard pattern
|
||||
- ✅ Lowercase `apiClient` usage
|
||||
- ✅ Page-specific logger (`themeLog`)
|
||||
- ✅ `currentPage` identifier
|
||||
- ✅ Performance tracking
|
||||
- ✅ Proper error handling
|
||||
|
||||
---
|
||||
|
||||
## 📦 Files to Install
|
||||
|
||||
### 1. JavaScript Component (Alpine.js)
|
||||
**File:** `vendor-theme-alpine.js`
|
||||
**Install to:** `static/admin/js/vendor-theme.js`
|
||||
|
||||
```bash
|
||||
cp vendor-theme-alpine.js static/admin/js/vendor-theme.js
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- ✅ Follows `dashboard.js` pattern exactly
|
||||
- ✅ Uses `adminVendorTheme()` function name
|
||||
- ✅ Inherits base with `...data()`
|
||||
- ✅ Sets `currentPage: 'vendor-theme'`
|
||||
- ✅ Has initialization guard
|
||||
- ✅ Uses lowercase `apiClient`
|
||||
- ✅ Has `themeLog` logger
|
||||
- ✅ Performance tracking with `Date.now()`
|
||||
|
||||
---
|
||||
|
||||
### 2. API Endpoints (Backend)
|
||||
**File:** `vendor_themes_api.py`
|
||||
**Install to:** `app/api/v1/admin/vendor_themes.py`
|
||||
|
||||
```bash
|
||||
cp vendor_themes_api.py app/api/v1/admin/vendor_themes.py
|
||||
```
|
||||
|
||||
**Endpoints:**
|
||||
```
|
||||
GET /api/v1/admin/vendor-themes/presets
|
||||
GET /api/v1/admin/vendor-themes/{vendor_code}
|
||||
PUT /api/v1/admin/vendor-themes/{vendor_code}
|
||||
POST /api/v1/admin/vendor-themes/{vendor_code}/preset/{preset_name}
|
||||
DELETE /api/v1/admin/vendor-themes/{vendor_code}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Pydantic Schemas
|
||||
**File:** `vendor_theme_schemas.py`
|
||||
**Install to:** `models/schema/vendor_theme.py`
|
||||
|
||||
```bash
|
||||
cp vendor_theme_schemas.py models/schema/vendor_theme.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. HTML Template
|
||||
**File:** `vendor-theme.html`
|
||||
**Install to:** `app/templates/admin/vendor-theme.html`
|
||||
|
||||
```bash
|
||||
cp vendor-theme.html app/templates/admin/vendor-theme.html
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- ✅ Extends `admin/base.html`
|
||||
- ✅ Uses `{% block alpine_data %}adminVendorTheme(){% endblock %}`
|
||||
- ✅ Loads script in `{% block extra_scripts %}`
|
||||
- ✅ 7 preset buttons
|
||||
- ✅ 6 color pickers
|
||||
- ✅ Font and layout selectors
|
||||
- ✅ Live preview panel
|
||||
|
||||
---
|
||||
|
||||
### 5. Frontend Router Update
|
||||
**File:** `pages-updated.py`
|
||||
**Install to:** `app/api/v1/admin/pages.py`
|
||||
|
||||
```bash
|
||||
cp pages-updated.py app/api/v1/admin/pages.py
|
||||
```
|
||||
|
||||
**Change:**
|
||||
- Added route: `GET /vendors/{vendor_code}/theme`
|
||||
- Returns: `admin/vendor-theme.html`
|
||||
|
||||
---
|
||||
|
||||
### 6. API Router Registration
|
||||
**File:** `__init__-updated.py`
|
||||
**Install to:** `app/api/v1/admin/__init__.py`
|
||||
|
||||
```bash
|
||||
cp __init__-updated.py app/api/v1/admin/__init__.py
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
```python
|
||||
# Added import
|
||||
from . import vendor_themes
|
||||
|
||||
# Added router registration
|
||||
router.include_router(vendor_themes.router, tags=["admin-vendor-themes"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Theme Presets (if not already installed)
|
||||
**File:** `theme_presets.py`
|
||||
**Install to:** `app/core/theme_presets.py`
|
||||
|
||||
```bash
|
||||
cp theme_presets.py app/core/theme_presets.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Installation Steps
|
||||
|
||||
### Step 1: Copy All Files
|
||||
|
||||
```bash
|
||||
# 1. JavaScript (Frontend)
|
||||
cp vendor-theme-alpine.js static/admin/js/vendor-theme.js
|
||||
|
||||
# 2. API Endpoints (Backend)
|
||||
cp vendor_themes_api.py app/api/v1/admin/vendor_themes.py
|
||||
|
||||
# 3. Pydantic Schemas
|
||||
cp vendor_theme_schemas.py models/schema/vendor_theme.py
|
||||
|
||||
# 4. HTML Template
|
||||
cp vendor-theme.html app/templates/admin/vendor-theme.html
|
||||
|
||||
# 5. Theme Presets (if not done)
|
||||
cp theme_presets.py app/core/theme_presets.py
|
||||
|
||||
# 6. Update Frontend Router
|
||||
cp pages-updated.py app/api/v1/admin/pages.py
|
||||
|
||||
# 7. Update API Router
|
||||
cp __init__-updated.py app/api/v1/admin/__init__.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Verify Database
|
||||
|
||||
Make sure the `vendor_themes` table exists:
|
||||
|
||||
```bash
|
||||
# Check migrations
|
||||
alembic history
|
||||
|
||||
# Run migration if needed
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Restart Server
|
||||
|
||||
```bash
|
||||
# Stop server
|
||||
pkill -f uvicorn
|
||||
|
||||
# Start server
|
||||
python -m uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### 1. Check JavaScript Loading
|
||||
|
||||
Open browser console and look for:
|
||||
```
|
||||
ℹ️ [THEME INFO] Vendor theme editor module loaded
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Test Page Load
|
||||
|
||||
Navigate to:
|
||||
```
|
||||
http://localhost:8000/admin/vendors/VENDOR001/theme
|
||||
```
|
||||
|
||||
Expected console output:
|
||||
```
|
||||
ℹ️ [THEME INFO] === VENDOR THEME EDITOR INITIALIZING ===
|
||||
ℹ️ [THEME INFO] Vendor code: VENDOR001
|
||||
ℹ️ [THEME INFO] Loading vendor data...
|
||||
ℹ️ [THEME INFO] Vendor loaded in 45ms: Vendor Name
|
||||
ℹ️ [THEME INFO] Loading theme...
|
||||
ℹ️ [THEME INFO] Theme loaded in 23ms: default
|
||||
ℹ️ [THEME INFO] Loading presets...
|
||||
ℹ️ [THEME INFO] 7 presets loaded in 12ms
|
||||
ℹ️ [THEME INFO] === THEME EDITOR INITIALIZATION COMPLETE (80ms) ===
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Test Preset Application
|
||||
|
||||
Click "Modern" preset button.
|
||||
|
||||
Expected console output:
|
||||
```
|
||||
ℹ️ [THEME INFO] Applying preset: modern
|
||||
ℹ️ [THEME INFO] Preset applied in 56ms
|
||||
```
|
||||
|
||||
Expected UI: Colors and fonts update instantly
|
||||
|
||||
---
|
||||
|
||||
### 4. Test Color Changes
|
||||
|
||||
1. Click primary color picker
|
||||
2. Choose a new color
|
||||
3. Preview should update immediately
|
||||
|
||||
---
|
||||
|
||||
### 5. Test Save
|
||||
|
||||
1. Click "Save Theme" button
|
||||
2. Expected toast: "Theme saved successfully"
|
||||
3. Expected console:
|
||||
```
|
||||
ℹ️ [THEME INFO] Saving theme: {theme_name: 'modern', ...}
|
||||
ℹ️ [THEME INFO] Theme saved in 34ms
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Test API Endpoints
|
||||
|
||||
```bash
|
||||
# Get presets
|
||||
curl http://localhost:8000/api/v1/admin/vendor-themes/presets \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# Get vendor theme
|
||||
curl http://localhost:8000/api/v1/admin/vendor-themes/VENDOR001 \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# Apply preset
|
||||
curl -X POST \
|
||||
http://localhost:8000/api/v1/admin/vendor-themes/VENDOR001/preset/modern \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# Save theme
|
||||
curl -X PUT \
|
||||
http://localhost:8000/api/v1/admin/vendor-themes/VENDOR001 \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-d '{
|
||||
"colors": {
|
||||
"primary": "#ff0000"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Architecture Compliance Checklist
|
||||
|
||||
### JavaScript File ✅
|
||||
- [x] Logging setup (`themeLog`)
|
||||
- [x] Function name: `adminVendorTheme()`
|
||||
- [x] `...data()` at start
|
||||
- [x] `currentPage: 'vendor-theme'`
|
||||
- [x] Initialization guard (`window._vendorThemeInitialized`)
|
||||
- [x] Uses lowercase `apiClient`
|
||||
- [x] Uses page-specific logger (`themeLog`)
|
||||
- [x] Performance tracking (`Date.now()`)
|
||||
- [x] Module loaded log at end
|
||||
|
||||
---
|
||||
|
||||
### HTML Template ✅
|
||||
- [x] Extends `admin/base.html`
|
||||
- [x] `alpine_data` block uses `adminVendorTheme()`
|
||||
- [x] `x-show` for loading states
|
||||
- [x] `x-text` for reactive data
|
||||
- [x] Loads JS in `extra_scripts` block
|
||||
|
||||
---
|
||||
|
||||
### API Routes ✅
|
||||
- [x] RESTful endpoint structure
|
||||
- [x] Proper error handling
|
||||
- [x] Admin authentication required
|
||||
- [x] Pydantic validation
|
||||
- [x] Logging
|
||||
|
||||
---
|
||||
|
||||
## 📊 Data Flow
|
||||
|
||||
```
|
||||
1. User visits /admin/vendors/VENDOR001/theme
|
||||
↓
|
||||
2. pages.py returns vendor-theme.html
|
||||
↓
|
||||
3. Template loads vendor-theme.js
|
||||
↓
|
||||
4. Alpine.js calls adminVendorTheme()
|
||||
↓
|
||||
5. Component spreads ...data() (gets base UI state)
|
||||
↓
|
||||
6. Component adds page-specific state
|
||||
↓
|
||||
7. init() runs (with guard)
|
||||
↓
|
||||
8. Loads vendor data via apiClient.get()
|
||||
↓
|
||||
9. Loads theme data via apiClient.get()
|
||||
↓
|
||||
10. Loads presets via apiClient.get()
|
||||
↓
|
||||
11. UI updates reactively
|
||||
↓
|
||||
12. User clicks preset button
|
||||
↓
|
||||
13. applyPreset() sends apiClient.post()
|
||||
↓
|
||||
14. API applies preset and saves to DB
|
||||
↓
|
||||
15. Response updates themeData
|
||||
↓
|
||||
16. UI updates (colors, fonts, preview)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### "adminVendorTheme is not defined"
|
||||
|
||||
**Problem:** JavaScript file not loaded or function name mismatch
|
||||
|
||||
**Fix:**
|
||||
1. Check template: `{% block alpine_data %}adminVendorTheme(){% endblock %}`
|
||||
2. Check JS file has: `function adminVendorTheme() {`
|
||||
3. Check JS file is loaded in `extra_scripts` block
|
||||
|
||||
---
|
||||
|
||||
### "apiClient is not defined"
|
||||
|
||||
**Problem:** `api-client.js` not loaded before your script
|
||||
|
||||
**Fix:** Check `base.html` loads scripts in this order:
|
||||
1. `log-config.js`
|
||||
2. `icons.js`
|
||||
3. `init-alpine.js`
|
||||
4. `utils.js`
|
||||
5. `api-client.js`
|
||||
6. Alpine.js CDN
|
||||
7. Your page script (vendor-theme.js)
|
||||
|
||||
---
|
||||
|
||||
### Init runs multiple times
|
||||
|
||||
**Problem:** Guard not working
|
||||
|
||||
**Expected:** You should see this warning in console:
|
||||
```
|
||||
⚠️ [THEME WARN] Theme editor already initialized, skipping...
|
||||
```
|
||||
|
||||
**If not appearing:** Check guard is in `init()`:
|
||||
```javascript
|
||||
if (window._vendorThemeInitialized) {
|
||||
themeLog.warn('Theme editor already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._vendorThemeInitialized = true;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### No console logs
|
||||
|
||||
**Problem:** Log level too low
|
||||
|
||||
**Fix:** Set `THEME_LOG_LEVEL = 4` at top of vendor-theme.js for all logs
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Checklist
|
||||
|
||||
- [ ] All 7 files copied
|
||||
- [ ] API router registered in `__init__.py`
|
||||
- [ ] Frontend route added in `pages.py`
|
||||
- [ ] Database has `vendor_themes` table
|
||||
- [ ] Server restarted
|
||||
- [ ] Can access `/admin/vendors/VENDOR001/theme`
|
||||
- [ ] Console shows module loaded log
|
||||
- [ ] Console shows initialization logs
|
||||
- [ ] Presets load and work
|
||||
- [ ] Color pickers work
|
||||
- [ ] Save button works
|
||||
- [ ] No console errors
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Success Indicators
|
||||
|
||||
When everything is working:
|
||||
|
||||
1. **Console logs:**
|
||||
```
|
||||
ℹ️ [THEME INFO] Vendor theme editor module loaded
|
||||
ℹ️ [THEME INFO] === VENDOR THEME EDITOR INITIALIZING ===
|
||||
ℹ️ [THEME INFO] === THEME EDITOR INITIALIZATION COMPLETE (XXms) ===
|
||||
```
|
||||
|
||||
2. **No errors** in browser console
|
||||
|
||||
3. **Live preview** updates when you change colors
|
||||
|
||||
4. **Preset buttons** change theme instantly
|
||||
|
||||
5. **Save button** shows success toast
|
||||
|
||||
6. **Changes persist** after page refresh
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- Frontend Architecture: `FRONTEND_ARCHITECTURE_OVERVIEW.txt`
|
||||
- Alpine.js Template: `FRONTEND_ALPINE_PAGE_TEMPLATE.md`
|
||||
- Your working example: `static/admin/js/dashboard.js`
|
||||
|
||||
---
|
||||
|
||||
**Your theme editor now follows your exact frontend architecture! 🎨**
|
||||
@@ -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! 🎨
|
||||
@@ -0,0 +1,536 @@
|
||||
# VENDOR THEME BACKEND - PROPER ARCHITECTURE IMPLEMENTATION
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation follows your **complete backend architecture** with:
|
||||
- ✅ **Separation of Concerns** - Service layer handles business logic
|
||||
- ✅ **Exception Management** - Custom exceptions for all error cases
|
||||
- ✅ **Proper Layering** - Database → Service → API → Frontend
|
||||
- ✅ **Naming Conventions** - Follows your established patterns
|
||||
- ✅ **No Business Logic in Endpoints** - All logic in service layer
|
||||
|
||||
---
|
||||
|
||||
## 📦 File Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── exceptions/
|
||||
│ ├── __init__.py # ← UPDATE (add vendor_theme imports)
|
||||
│ └── vendor_theme.py # ← NEW (custom exceptions)
|
||||
├── services/
|
||||
│ └── vendor_theme_service.py # ← NEW (business logic)
|
||||
└── api/v1/admin/
|
||||
├── __init__.py # ← UPDATE (register router)
|
||||
├── pages.py # ← UPDATE (add theme route)
|
||||
└── vendor_themes.py # ← NEW (endpoints only)
|
||||
|
||||
models/
|
||||
└── schema/
|
||||
└── vendor_theme.py # ← Already created (Pydantic)
|
||||
|
||||
static/admin/js/
|
||||
└── vendor-theme.js # ← Already created (Alpine.js)
|
||||
|
||||
app/templates/admin/
|
||||
└── vendor-theme.html # ← Already created (HTML)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Layers
|
||||
|
||||
### Layer 1: Exceptions (Error Handling)
|
||||
**File:** `app/exceptions/vendor_theme.py`
|
||||
|
||||
```python
|
||||
# Custom exceptions for domain-specific errors
|
||||
- VendorThemeNotFoundException
|
||||
- ThemePresetNotFoundException
|
||||
- InvalidThemeDataException
|
||||
- ThemeValidationException
|
||||
- InvalidColorFormatException
|
||||
- InvalidFontFamilyException
|
||||
- ThemeOperationException
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- ✅ Extends base exceptions (`ResourceNotFoundException`, etc.)
|
||||
- ✅ Provides specific error codes
|
||||
- ✅ Includes detailed error information
|
||||
- ✅ No HTTP status codes (that's endpoint layer)
|
||||
|
||||
---
|
||||
|
||||
### Layer 2: Service (Business Logic)
|
||||
**File:** `app/services/vendor_theme_service.py`
|
||||
|
||||
**Responsibilities:**
|
||||
- ✅ Get/Create/Update/Delete themes
|
||||
- ✅ Apply presets
|
||||
- ✅ Validate theme data
|
||||
- ✅ Database operations
|
||||
- ✅ Raise custom exceptions
|
||||
|
||||
**Does NOT:**
|
||||
- ❌ Handle HTTP requests/responses
|
||||
- ❌ Raise HTTPException
|
||||
- ❌ Know about FastAPI
|
||||
|
||||
**Example Service Method:**
|
||||
```python
|
||||
def update_theme(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_code: str,
|
||||
theme_data: VendorThemeUpdate
|
||||
) -> VendorTheme:
|
||||
"""Update theme - returns VendorTheme or raises exception."""
|
||||
|
||||
try:
|
||||
vendor = self._get_vendor_by_code(db, vendor_code)
|
||||
theme = # ... get or create theme
|
||||
|
||||
# Validate
|
||||
self._validate_theme_data(theme_data)
|
||||
|
||||
# Update
|
||||
self._apply_theme_updates(theme, theme_data)
|
||||
|
||||
db.commit()
|
||||
return theme
|
||||
|
||||
except CustomException:
|
||||
raise # Re-raise custom exceptions
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise ThemeOperationException(...) # Wrap generic errors
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Layer 3: API Endpoints (HTTP Interface)
|
||||
**File:** `app/api/v1/admin/vendor_themes.py`
|
||||
|
||||
**Responsibilities:**
|
||||
- ✅ Handle HTTP requests
|
||||
- ✅ Call service methods
|
||||
- ✅ Convert exceptions to HTTP responses
|
||||
- ✅ Return JSON responses
|
||||
|
||||
**Does NOT:**
|
||||
- ❌ Contain business logic
|
||||
- ❌ Validate data (service does this)
|
||||
- ❌ Access database directly
|
||||
|
||||
**Example Endpoint:**
|
||||
```python
|
||||
@router.put("/{vendor_code}")
|
||||
async def update_vendor_theme(
|
||||
vendor_code: str,
|
||||
theme_data: VendorThemeUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""Endpoint - just calls service and handles errors."""
|
||||
|
||||
try:
|
||||
# Call service (all logic there)
|
||||
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
|
||||
|
||||
# Return response
|
||||
return theme.to_dict()
|
||||
|
||||
except VendorNotFoundException:
|
||||
raise HTTPException(status_code=404, detail="...")
|
||||
|
||||
except ThemeValidationException as e:
|
||||
raise HTTPException(status_code=422, detail=e.message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(...)
|
||||
raise HTTPException(status_code=500, detail="...")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📥 Installation
|
||||
|
||||
### Step 1: Install Exception Layer
|
||||
|
||||
```bash
|
||||
# 1. Copy exception file
|
||||
cp vendor_theme_exceptions.py app/exceptions/vendor_theme.py
|
||||
|
||||
# 2. Update exceptions __init__.py
|
||||
cp exceptions__init__-updated.py app/exceptions/__init__.py
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
```python
|
||||
from app.exceptions import VendorThemeNotFoundException # Should work
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Install Service Layer
|
||||
|
||||
```bash
|
||||
# Copy service file
|
||||
cp vendor_theme_service.py app/services/vendor_theme_service.py
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
```python
|
||||
from app.services.vendor_theme_service import vendor_theme_service # Should work
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Install API Layer
|
||||
|
||||
```bash
|
||||
# Copy API endpoints
|
||||
cp vendor_themes_endpoints.py app/api/v1/admin/vendor_themes.py
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
```python
|
||||
from app.api.v1.admin import vendor_themes # Should work
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Register Router
|
||||
|
||||
```bash
|
||||
# Update API router
|
||||
cp __init__-updated.py app/api/v1/admin/__init__.py
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
```python
|
||||
# Added:
|
||||
from . import vendor_themes
|
||||
router.include_router(vendor_themes.router, tags=["admin-vendor-themes"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Update Frontend Router
|
||||
|
||||
```bash
|
||||
# Update pages router
|
||||
cp pages-updated.py app/api/v1/admin/pages.py
|
||||
```
|
||||
|
||||
**Added Route:**
|
||||
```python
|
||||
@router.get("/vendors/{vendor_code}/theme")
|
||||
async def admin_vendor_theme_page(...):
|
||||
return templates.TemplateResponse("admin/vendor-theme.html", ...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: Copy Frontend Files (Already Done)
|
||||
|
||||
```bash
|
||||
# JavaScript
|
||||
cp vendor-theme-alpine.js static/admin/js/vendor-theme.js
|
||||
|
||||
# HTML Template
|
||||
cp vendor-theme.html app/templates/admin/vendor-theme.html
|
||||
|
||||
# Pydantic Schemas
|
||||
cp vendor_theme_schemas.py models/schema/vendor_theme.py
|
||||
|
||||
# Theme Presets
|
||||
cp theme_presets.py app/core/theme_presets.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Data Flow
|
||||
|
||||
### Complete Request Flow
|
||||
|
||||
```
|
||||
1. User clicks "Save Theme" button
|
||||
↓
|
||||
2. JavaScript (vendor-theme.js)
|
||||
- apiClient.put('/api/v1/admin/vendor-themes/VENDOR001', data)
|
||||
↓
|
||||
3. API Endpoint (vendor_themes.py)
|
||||
- Receives HTTP PUT request
|
||||
- Validates admin authentication
|
||||
- Calls service layer
|
||||
↓
|
||||
4. Service Layer (vendor_theme_service.py)
|
||||
- Validates theme data
|
||||
- Gets vendor from database
|
||||
- Creates/updates VendorTheme
|
||||
- Commits transaction
|
||||
- Returns VendorTheme object
|
||||
↓
|
||||
5. API Endpoint
|
||||
- Converts VendorTheme to dict
|
||||
- Returns JSON response
|
||||
↓
|
||||
6. JavaScript
|
||||
- Receives response
|
||||
- Shows success toast
|
||||
- Updates UI
|
||||
```
|
||||
|
||||
### Error Flow
|
||||
|
||||
```
|
||||
1. Service detects invalid color
|
||||
↓
|
||||
2. Service raises InvalidColorFormatException
|
||||
↓
|
||||
3. API endpoint catches exception
|
||||
↓
|
||||
4. API converts to HTTPException(422)
|
||||
↓
|
||||
5. FastAPI returns JSON error
|
||||
↓
|
||||
6. JavaScript catches error
|
||||
↓
|
||||
7. Shows error toast to user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Architecture Compliance
|
||||
|
||||
### Separation of Concerns ✅
|
||||
|
||||
| Layer | Responsibilities | ✅ |
|
||||
|-------|-----------------|-----|
|
||||
| **Exceptions** | Define error types | ✅ |
|
||||
| **Service** | Business logic, validation, DB operations | ✅ |
|
||||
| **API** | HTTP handling, auth, error conversion | ✅ |
|
||||
| **Frontend** | User interaction, API calls | ✅ |
|
||||
|
||||
### What Goes Where
|
||||
|
||||
```python
|
||||
# ❌ WRONG - Business logic in endpoint
|
||||
@router.put("/{vendor_code}")
|
||||
async def update_theme(...):
|
||||
vendor = db.query(Vendor).filter(...).first() # ❌ Direct DB
|
||||
if not vendor:
|
||||
raise HTTPException(404) # ❌ Should be custom exception
|
||||
|
||||
theme.colors = theme_data.colors # ❌ Business logic
|
||||
if not self._is_valid_color(theme.colors['primary']): # ❌ Validation
|
||||
raise HTTPException(422)
|
||||
|
||||
db.commit() # ❌ Transaction management
|
||||
return theme.to_dict()
|
||||
|
||||
|
||||
# ✅ CORRECT - Clean separation
|
||||
@router.put("/{vendor_code}")
|
||||
async def update_theme(...):
|
||||
try:
|
||||
# Just call service
|
||||
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
|
||||
return theme.to_dict()
|
||||
except VendorNotFoundException:
|
||||
raise HTTPException(404)
|
||||
except ThemeValidationException as e:
|
||||
raise HTTPException(422, detail=e.message)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Unit Tests (Service Layer)
|
||||
|
||||
```python
|
||||
# tests/unit/services/test_vendor_theme_service.py
|
||||
|
||||
def test_update_theme_validates_colors(service, db, vendor):
|
||||
"""Test color validation."""
|
||||
theme_data = VendorThemeUpdate(
|
||||
colors={"primary": "not-a-color"} # Invalid
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidColorFormatException):
|
||||
service.update_theme(db, vendor.vendor_code, theme_data)
|
||||
|
||||
|
||||
def test_apply_preset_invalid_name(service, db, vendor):
|
||||
"""Test invalid preset name."""
|
||||
with pytest.raises(ThemePresetNotFoundException) as exc_info:
|
||||
service.apply_theme_preset(db, vendor.vendor_code, "invalid")
|
||||
|
||||
assert "invalid" in str(exc_info.value.message)
|
||||
```
|
||||
|
||||
### Integration Tests (API Layer)
|
||||
|
||||
```python
|
||||
# tests/integration/api/v1/admin/test_vendor_themes.py
|
||||
|
||||
def test_update_theme_endpoint(client, admin_headers, vendor):
|
||||
"""Test theme update endpoint."""
|
||||
response = client.put(
|
||||
f"/api/v1/admin/vendor-themes/{vendor.vendor_code}",
|
||||
json={"colors": {"primary": "#ff0000"}},
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["colors"]["primary"] == "#ff0000"
|
||||
|
||||
|
||||
def test_update_theme_invalid_color(client, admin_headers, vendor):
|
||||
"""Test validation error response."""
|
||||
response = client.put(
|
||||
f"/api/v1/admin/vendor-themes/{vendor.vendor_code}",
|
||||
json={"colors": {"primary": "invalid"}},
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert "invalid color" in response.json()["detail"].lower()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Comparison: Before vs After
|
||||
|
||||
### Before (Original) ❌
|
||||
|
||||
```python
|
||||
# vendor_themes_api.py (OLD)
|
||||
@router.put("/{vendor_code}")
|
||||
async def update_vendor_theme(vendor_code: str, theme_data: dict, ...):
|
||||
# ❌ Direct database access
|
||||
vendor = db.query(Vendor).filter(...).first()
|
||||
|
||||
# ❌ Business logic in endpoint
|
||||
if not theme:
|
||||
theme = VendorTheme(vendor_id=vendor.id)
|
||||
db.add(theme)
|
||||
|
||||
# ❌ Data manipulation in endpoint
|
||||
if "colors" in theme_data:
|
||||
theme.colors = theme_data["colors"]
|
||||
|
||||
# ❌ Transaction management in endpoint
|
||||
db.commit()
|
||||
db.refresh(theme)
|
||||
|
||||
# ❌ Generic exception
|
||||
except Exception as e:
|
||||
raise HTTPException(500, detail="Failed")
|
||||
```
|
||||
|
||||
### After (Refactored) ✅
|
||||
|
||||
```python
|
||||
# vendor_themes.py (NEW - API Layer)
|
||||
@router.put("/{vendor_code}")
|
||||
async def update_vendor_theme(vendor_code: str, theme_data: VendorThemeUpdate, ...):
|
||||
try:
|
||||
# ✅ Just call service
|
||||
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
|
||||
return theme.to_dict()
|
||||
|
||||
# ✅ Specific exception handling
|
||||
except VendorNotFoundException:
|
||||
raise HTTPException(404, detail="Vendor not found")
|
||||
except ThemeValidationException as e:
|
||||
raise HTTPException(422, detail=e.message)
|
||||
|
||||
|
||||
# vendor_theme_service.py (NEW - Service Layer)
|
||||
class VendorThemeService:
|
||||
def update_theme(self, db, vendor_code, theme_data):
|
||||
try:
|
||||
# ✅ Business logic here
|
||||
vendor = self._get_vendor_by_code(db, vendor_code)
|
||||
theme = self._get_or_create_theme(db, vendor)
|
||||
|
||||
# ✅ Validation
|
||||
self._validate_theme_data(theme_data)
|
||||
|
||||
# ✅ Data updates
|
||||
self._apply_theme_updates(theme, theme_data)
|
||||
|
||||
# ✅ Transaction management
|
||||
db.commit()
|
||||
return theme
|
||||
|
||||
# ✅ Custom exceptions
|
||||
except ValidationError:
|
||||
raise ThemeValidationException(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Files Reference
|
||||
|
||||
### Backend Files (7 files)
|
||||
|
||||
1. **vendor_theme_exceptions.py** → `app/exceptions/vendor_theme.py`
|
||||
- Custom exception classes
|
||||
|
||||
2. **exceptions__init__-updated.py** → `app/exceptions/__init__.py`
|
||||
- Updated with vendor_theme imports
|
||||
|
||||
3. **vendor_theme_service.py** → `app/services/vendor_theme_service.py`
|
||||
- Business logic service
|
||||
|
||||
4. **vendor_themes_endpoints.py** → `app/api/v1/admin/vendor_themes.py`
|
||||
- API endpoints (thin layer)
|
||||
|
||||
5. **__init__-updated.py** → `app/api/v1/admin/__init__.py`
|
||||
- Router registration
|
||||
|
||||
6. **pages-updated.py** → `app/api/v1/admin/pages.py`
|
||||
- Frontend route
|
||||
|
||||
7. **vendor_theme_schemas.py** → `models/schema/vendor_theme.py`
|
||||
- Pydantic models
|
||||
|
||||
### Frontend Files (3 files)
|
||||
|
||||
8. **vendor-theme-alpine.js** → `static/admin/js/vendor-theme.js`
|
||||
9. **vendor-theme.html** → `app/templates/admin/vendor-theme.html`
|
||||
10. **theme_presets.py** → `app/core/theme_presets.py`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Checklist
|
||||
|
||||
### Backend Architecture
|
||||
- [ ] Exceptions in `app/exceptions/vendor_theme.py`
|
||||
- [ ] Service in `app/services/vendor_theme_service.py`
|
||||
- [ ] Endpoints in `app/api/v1/admin/vendor_themes.py`
|
||||
- [ ] No business logic in endpoints
|
||||
- [ ] No HTTPException in service
|
||||
- [ ] Custom exceptions used throughout
|
||||
|
||||
### Frontend Architecture
|
||||
- [ ] Alpine.js component with `...data()`
|
||||
- [ ] Uses lowercase `apiClient`
|
||||
- [ ] Has initialization guard
|
||||
- [ ] Follows dashboard.js pattern
|
||||
|
||||
### Integration
|
||||
- [ ] Router registered in `__init__.py`
|
||||
- [ ] Frontend route in `pages.py`
|
||||
- [ ] All exceptions imported
|
||||
- [ ] Service imported in endpoints
|
||||
|
||||
---
|
||||
|
||||
**Your theme editor now follows proper backend architecture with complete separation of concerns!** 🎯
|
||||
@@ -224,11 +224,11 @@ Simplifying to minimal working version:
|
||||
- Extends base.html
|
||||
- Alpine.js adminDashboard() component
|
||||
|
||||
4. **`app/templates/partials/header.html`** ✅
|
||||
4. **`app/templates/admin/partials/header.html`** ✅
|
||||
- Top navigation bar
|
||||
- Updated logout link to /admin/login
|
||||
|
||||
5. **`app/templates/partials/sidebar.html`** ✅
|
||||
5. **`app/templates/admin/partials/sidebar.html`** ✅
|
||||
- Side navigation menu
|
||||
- Updated all links to /admin/* paths
|
||||
|
||||
|
||||
Reference in New Issue
Block a user