refactor: rename shop to storefront for consistency
Rename all "shop" directories and references to "storefront" to match the API and route naming convention already in use. Renamed directories: - app/templates/shop/ → app/templates/storefront/ - static/shop/ → static/storefront/ - app/templates/shared/macros/shop/ → .../macros/storefront/ - docs/frontend/shop/ → docs/frontend/storefront/ Renamed files: - shop.css → storefront.css - shop-layout.js → storefront-layout.js Updated references in: - app/routes/storefront_pages.py (21 template references) - app/modules/cms/routes/pages/vendor.py - app/templates/storefront/base.html (static paths) - All storefront templates (extends/includes) - docs/architecture/frontend-structure.md This aligns the template/static naming with: - Route file: storefront_pages.py - API directory: app/api/v1/storefront/ - Module routes: */routes/api/storefront.py - URL paths: /storefront/* Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1345
docs/frontend/storefront/architecture.md
Normal file
1345
docs/frontend/storefront/architecture.md
Normal file
File diff suppressed because it is too large
Load Diff
476
docs/frontend/storefront/authentication-pages.md
Normal file
476
docs/frontend/storefront/authentication-pages.md
Normal file
@@ -0,0 +1,476 @@
|
||||
# Shop Authentication Pages
|
||||
|
||||
## Overview
|
||||
|
||||
This document details the implementation of customer authentication pages in the shop frontend. All pages use Tailwind CSS, Alpine.js, and integrate with the multi-theme system for a branded, consistent experience across all vendors.
|
||||
|
||||
## Implementation Date
|
||||
2025-11-24
|
||||
|
||||
---
|
||||
|
||||
## 📄 Available Pages
|
||||
|
||||
### 1. Login Page
|
||||
**Location:** `app/templates/shop/account/login.html`
|
||||
**Route:** `/shop/account/login`
|
||||
|
||||
#### Features
|
||||
- Two-column layout with vendor branding on the left
|
||||
- Email and password fields with validation
|
||||
- Password visibility toggle
|
||||
- "Remember me" checkbox
|
||||
- Links to register and forgot password pages
|
||||
- Error and success message alerts
|
||||
- Loading states with animated spinner
|
||||
- Theme-aware colors using CSS variables
|
||||
- Full dark mode support
|
||||
- Mobile responsive design
|
||||
|
||||
#### Alpine.js Component
|
||||
```javascript
|
||||
function customerLogin() {
|
||||
return {
|
||||
credentials: { email: '', password: '' },
|
||||
rememberMe: false,
|
||||
showPassword: false,
|
||||
loading: false,
|
||||
errors: {},
|
||||
alert: { show: false, type: 'error', message: '' },
|
||||
dark: false,
|
||||
|
||||
async handleLogin() {
|
||||
// Validates input
|
||||
// Calls POST /api/v1/shop/auth/login
|
||||
// Stores token in localStorage
|
||||
// Redirects to account page or return URL
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### API Integration
|
||||
- **Endpoint:** `POST /api/v1/shop/auth/login`
|
||||
- **Request:** `{ email_or_username: string, password: string }`
|
||||
- **Response:** `{ access_token: string, user: object }`
|
||||
|
||||
---
|
||||
|
||||
### 2. Register Page
|
||||
**Location:** `app/templates/shop/account/register.html`
|
||||
**Route:** `/shop/account/register`
|
||||
|
||||
#### Features
|
||||
- Two-column layout with vendor branding
|
||||
- Form fields:
|
||||
- First name (required)
|
||||
- Last name (required)
|
||||
- Email (required, validated)
|
||||
- Phone (optional)
|
||||
- Password (required, min 8 chars, 1 letter, 1 number)
|
||||
- Confirm password (required, must match)
|
||||
- Marketing consent checkbox
|
||||
- Real-time client-side validation
|
||||
- Password visibility toggle
|
||||
- Password strength requirements displayed
|
||||
- Theme-aware styling
|
||||
- Loading states with spinner
|
||||
- Success/error message handling
|
||||
- Redirects to login page after successful registration
|
||||
|
||||
#### Validation Rules
|
||||
- **First Name:** Required, non-empty
|
||||
- **Last Name:** Required, non-empty
|
||||
- **Email:** Required, valid email format
|
||||
- **Password:** Minimum 8 characters, at least one letter, at least one number
|
||||
- **Confirm Password:** Must match password field
|
||||
|
||||
#### Alpine.js Component
|
||||
```javascript
|
||||
function customerRegistration() {
|
||||
return {
|
||||
formData: {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
marketing_consent: false
|
||||
},
|
||||
confirmPassword: '',
|
||||
showPassword: false,
|
||||
loading: false,
|
||||
errors: {},
|
||||
|
||||
validateForm() {
|
||||
// Validates all fields
|
||||
// Sets this.errors for invalid fields
|
||||
// Returns true if all valid
|
||||
},
|
||||
|
||||
async handleRegister() {
|
||||
// Validates form
|
||||
// Calls POST /api/v1/shop/auth/register
|
||||
// Shows success message
|
||||
// Redirects to login with ?registered=true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### API Integration
|
||||
- **Endpoint:** `POST /api/v1/shop/auth/register`
|
||||
- **Request:** `{ first_name: string, last_name: string, email: string, phone?: string, password: string, marketing_consent: boolean }`
|
||||
- **Response:** `{ message: string }`
|
||||
|
||||
---
|
||||
|
||||
### 3. Forgot Password Page
|
||||
**Location:** `app/templates/shop/account/forgot-password.html`
|
||||
**Route:** `/shop/account/forgot-password`
|
||||
|
||||
#### Features
|
||||
- Two-column layout with vendor branding
|
||||
- Email input field
|
||||
- Two-state interface:
|
||||
1. **Form State:** Email input with submit button
|
||||
2. **Success State:** Confirmation message with checkmark icon
|
||||
- Success state displays:
|
||||
- Checkmark icon
|
||||
- "Check Your Email" heading
|
||||
- Email sent confirmation
|
||||
- Instructions to check inbox
|
||||
- Option to retry if email not received
|
||||
- Theme-aware styling
|
||||
- Links back to login and shop homepage
|
||||
- Dark mode support
|
||||
- Mobile responsive
|
||||
|
||||
#### Alpine.js Component
|
||||
```javascript
|
||||
function forgotPassword() {
|
||||
return {
|
||||
email: '',
|
||||
emailSent: false,
|
||||
loading: false,
|
||||
errors: {},
|
||||
alert: { show: false, type: 'error', message: '' },
|
||||
dark: false,
|
||||
|
||||
async handleSubmit() {
|
||||
// Validates email
|
||||
// Calls POST /api/v1/shop/auth/forgot-password
|
||||
// Sets emailSent = true on success
|
||||
// Shows confirmation message
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### API Integration
|
||||
- **Endpoint:** `POST /api/v1/shop/auth/forgot-password`
|
||||
- **Request:** `{ email: string }`
|
||||
- **Response:** `{ message: string }`
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Theme Integration
|
||||
|
||||
All authentication pages inject the vendor's theme CSS variables for consistent branding:
|
||||
|
||||
```html
|
||||
<style id="vendor-theme-variables">
|
||||
:root {
|
||||
{% for key, value in theme.css_variables.items() %}
|
||||
{{ key }}: {{ value }};
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
/* Theme-aware button and focus colors */
|
||||
.btn-primary-theme {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
.btn-primary-theme:hover:not(:disabled) {
|
||||
background-color: var(--color-primary-dark, var(--color-primary));
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
.focus-primary:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 124, 58, 237), 0.1);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Themed Elements
|
||||
|
||||
| Element | CSS Variable | Usage |
|
||||
|---------|-------------|--------|
|
||||
| Left panel background | `var(--color-primary)` | Brand color fills entire left column |
|
||||
| Submit buttons | `var(--color-primary)` | Primary action buttons |
|
||||
| Links | `var(--color-primary)` | Forgot password, register, login links |
|
||||
| Checkboxes | `var(--color-primary)` | Remember me, marketing consent |
|
||||
| Focus states | `var(--color-primary)` | Input field focus rings |
|
||||
| Vendor logo | `theme.branding.logo` | Displayed in left column |
|
||||
|
||||
### Benefits
|
||||
- ✅ Each vendor's auth pages automatically match their brand
|
||||
- ✅ Consistent with main shop design
|
||||
- ✅ Dark mode adapts to vendor colors
|
||||
- ✅ Professional, polished appearance
|
||||
- ✅ No custom CSS needed per vendor
|
||||
|
||||
---
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
### Mobile (<640px)
|
||||
- Vertical layout (branding on top, form below)
|
||||
- Smaller h-32 branding section
|
||||
- Full-width buttons
|
||||
- Reduced padding (p-6 instead of p-12)
|
||||
- Touch-friendly input fields
|
||||
- Stacked form elements
|
||||
|
||||
### Tablet (640px-1024px)
|
||||
- Side-by-side layout begins (md:flex-row)
|
||||
- Branding section grows (md:h-auto md:w-1/2)
|
||||
- Form section gets more space (md:w-1/2)
|
||||
- Comfortable padding (sm:p-12)
|
||||
|
||||
### Desktop (>1024px)
|
||||
- Full two-column layout
|
||||
- Max width container (max-w-4xl)
|
||||
- Centered on page with margins
|
||||
- Larger brand imagery
|
||||
- Optimal form spacing
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
### Client-Side Security
|
||||
- Input validation before API submission
|
||||
- Password visibility toggle (not plain text by default)
|
||||
- HTTPS required (enforced by deployment)
|
||||
- No sensitive data in URLs or query params
|
||||
- Tokens stored in localStorage (not cookies)
|
||||
- Form autocomplete attributes for password managers
|
||||
|
||||
### Server-Side Security (API)
|
||||
- Password hashing with bcrypt
|
||||
- Email validation and sanitization
|
||||
- Rate limiting on auth endpoints
|
||||
- CSRF protection
|
||||
- SQL injection prevention via ORM
|
||||
- JWT token-based authentication
|
||||
- Secure password reset tokens
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Design Principles
|
||||
|
||||
### Consistency
|
||||
- All three pages share the same layout structure
|
||||
- Identical styling patterns and spacing
|
||||
- Consistent error/success message handling
|
||||
- Same loading state indicators
|
||||
|
||||
### User Experience
|
||||
- Clear visual hierarchy
|
||||
- Immediate feedback on actions
|
||||
- Helpful error messages
|
||||
- Loading states prevent duplicate submissions
|
||||
- Success states confirm actions
|
||||
- Links to related pages (login ↔ register ↔ forgot password)
|
||||
|
||||
### Accessibility
|
||||
- Semantic HTML
|
||||
- Proper form labels
|
||||
- Focus states visible
|
||||
- Keyboard navigation support
|
||||
- Screen reader friendly
|
||||
- Sufficient color contrast
|
||||
|
||||
### Performance
|
||||
- Minimal JavaScript (Alpine.js only)
|
||||
- Tailwind CSS from CDN with local fallback
|
||||
- No external dependencies beyond Alpine.js
|
||||
- Fast page loads
|
||||
- Inline SVG icons (no image requests)
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Navigation Flow
|
||||
|
||||
```
|
||||
Shop Homepage
|
||||
↓
|
||||
Login Page ←→ Register Page
|
||||
↓ ↓
|
||||
Forgot Password |
|
||||
↓ ↓
|
||||
Check Email Account/Cart
|
||||
```
|
||||
|
||||
### Link Structure
|
||||
- **Login Page:**
|
||||
- "Forgot password?" → `/shop/account/forgot-password`
|
||||
- "Create an account" → `/shop/account/register`
|
||||
- "← Continue shopping" → `/shop/`
|
||||
|
||||
- **Register Page:**
|
||||
- "Already have an account? Sign in instead" → `/shop/account/login`
|
||||
|
||||
- **Forgot Password Page:**
|
||||
- "Remember your password? Sign in" → `/shop/account/login`
|
||||
- "← Continue shopping" → `/shop/`
|
||||
|
||||
All links use `{{ base_url }}` for multi-access routing support.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Customization Guide
|
||||
|
||||
### For Developers
|
||||
|
||||
#### Adding New Fields
|
||||
1. Add field to HTML form
|
||||
2. Add field to Alpine.js data
|
||||
3. Update validation logic
|
||||
4. Update API request body
|
||||
5. Update backend endpoint
|
||||
|
||||
#### Changing Validation Rules
|
||||
Edit the `validateForm()` method in Alpine.js component:
|
||||
|
||||
```javascript
|
||||
validateForm() {
|
||||
this.clearAllErrors();
|
||||
let isValid = true;
|
||||
|
||||
// Add/modify validation rules
|
||||
if (!this.formData.field_name.trim()) {
|
||||
this.errors.field_name = 'Field is required';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
```
|
||||
|
||||
#### Customizing Theme Variables
|
||||
Vendors can customize colors in their theme configuration:
|
||||
|
||||
```python
|
||||
theme = {
|
||||
"colors": {
|
||||
"primary": "#6366f1", # Changes all primary elements
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#ec4899"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### For Vendors
|
||||
Vendors can customize:
|
||||
- Primary brand color (buttons, links, left panel)
|
||||
- Logo (displayed in left column)
|
||||
- Custom CSS (additional styling)
|
||||
- Dark mode logo variant
|
||||
|
||||
No code changes needed - all controlled via theme configuration.
|
||||
|
||||
---
|
||||
|
||||
## 📊 File Locations
|
||||
|
||||
```
|
||||
app/
|
||||
├── templates/shop/account/
|
||||
│ ├── login.html ← Customer login page
|
||||
│ ├── register.html ← Customer registration page
|
||||
│ └── forgot-password.html ← Password reset page
|
||||
│
|
||||
└── api/v1/shop/
|
||||
└── auth.py ← Authentication endpoints
|
||||
|
||||
static/shared/css/
|
||||
├── base.css ← Base CSS (optional reference)
|
||||
└── auth.css ← Auth CSS (optional reference)
|
||||
|
||||
Note: Templates use Tailwind CSS classes directly, not the CSS files above.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Testing Checklist
|
||||
|
||||
### Functionality
|
||||
- [ ] Login form submits correctly
|
||||
- [ ] Register form creates new account
|
||||
- [ ] Forgot password sends email
|
||||
- [ ] Validation errors display properly
|
||||
- [ ] Success messages show correctly
|
||||
- [ ] Loading states appear during API calls
|
||||
- [ ] Redirects work after success
|
||||
- [ ] Remember me checkbox persists
|
||||
- [ ] Password visibility toggle works
|
||||
|
||||
### Theme Integration
|
||||
- [ ] Vendor colors apply correctly
|
||||
- [ ] Vendor logo displays
|
||||
- [ ] Dark mode works with vendor colors
|
||||
- [ ] Custom fonts load
|
||||
- [ ] Left panel uses primary color
|
||||
- [ ] Buttons use primary color
|
||||
|
||||
### Responsive Design
|
||||
- [ ] Mobile layout works (<640px)
|
||||
- [ ] Tablet layout works (640-1024px)
|
||||
- [ ] Desktop layout works (>1024px)
|
||||
- [ ] Touch targets are adequate
|
||||
- [ ] Forms are usable on mobile
|
||||
|
||||
### Security
|
||||
- [ ] Passwords are masked by default
|
||||
- [ ] No sensitive data in URLs
|
||||
- [ ] API calls use HTTPS
|
||||
- [ ] Tokens stored securely
|
||||
- [ ] Input validation works
|
||||
|
||||
### Accessibility
|
||||
- [ ] Keyboard navigation works
|
||||
- [ ] Focus states visible
|
||||
- [ ] Form labels present
|
||||
- [ ] Error messages announced
|
||||
- [ ] Color contrast sufficient
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Future Enhancements
|
||||
|
||||
Possible additions:
|
||||
- Social login (Google, Facebook)
|
||||
- Two-factor authentication (2FA)
|
||||
- Password strength meter
|
||||
- Email verification flow
|
||||
- OAuth integration
|
||||
- Account recovery via phone
|
||||
- Security questions
|
||||
- Biometric authentication
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- [Shop Frontend Architecture](./architecture.md)
|
||||
- [Page Template Guide](./page-templates.md)
|
||||
- [Theme System Overview](../../architecture/theme-system/overview.md)
|
||||
- [Theme Presets](../../architecture/theme-system/presets.md)
|
||||
- [API Authentication Documentation](../../api/authentication.md)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-11-24
|
||||
**Status:** ✅ Production Ready
|
||||
**Maintainer:** Development Team
|
||||
622
docs/frontend/storefront/ecommerce-components-proposal.md
Normal file
622
docs/frontend/storefront/ecommerce-components-proposal.md
Normal file
@@ -0,0 +1,622 @@
|
||||
# E-commerce Components Proposal
|
||||
|
||||
**Version:** 1.0
|
||||
**Created:** December 2025
|
||||
**Status:** Proposal
|
||||
**Author:** Development Team
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document proposes a comprehensive set of reusable Jinja macro components for the shop frontend. These components will standardize the e-commerce experience across all vendor shops while supporting vendor-specific theming via CSS variables.
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Vendor Theming** - Use CSS variables (`var(--color-primary)`) for brand colors
|
||||
2. **Mobile First** - Responsive design starting from mobile
|
||||
3. **Performance** - Lazy loading, optimized images, minimal JS
|
||||
4. **Accessibility** - WCAG 2.1 AA compliant
|
||||
5. **Consistency** - Same UX patterns across all shops
|
||||
|
||||
---
|
||||
|
||||
## Proposed Components
|
||||
|
||||
### Priority 1: Core Shopping Components
|
||||
|
||||
#### 1.1 Product Card
|
||||
**File:** `app/templates/shared/macros/shop/product-card.html`
|
||||
|
||||
A versatile product card for grids, carousels, and lists.
|
||||
|
||||
```jinja
|
||||
{{ product_card(
|
||||
product=product,
|
||||
show_vendor=false,
|
||||
show_rating=true,
|
||||
show_quick_add=true,
|
||||
size='md', {# sm, md, lg #}
|
||||
layout='vertical' {# vertical, horizontal #}
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Product image with lazy loading
|
||||
- Sale badge / "New" badge
|
||||
- Product title (truncated)
|
||||
- Price (with strikethrough for sales)
|
||||
- Star rating
|
||||
- Quick "Add to Cart" button
|
||||
- Wishlist heart icon
|
||||
- Hover effects
|
||||
|
||||
**Variants:**
|
||||
| Size | Use Case |
|
||||
|------|----------|
|
||||
| `sm` | Mobile grids, sidebars |
|
||||
| `md` | Standard product grids |
|
||||
| `lg` | Featured products, hero sections |
|
||||
|
||||
---
|
||||
|
||||
#### 1.2 Product Grid
|
||||
**File:** `app/templates/shared/macros/shop/product-grid.html`
|
||||
|
||||
Responsive grid layout for product listings.
|
||||
|
||||
```jinja
|
||||
{{ product_grid(
|
||||
products=products,
|
||||
columns={'sm': 2, 'md': 3, 'lg': 4},
|
||||
gap='md',
|
||||
show_empty_state=true
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Responsive column configuration
|
||||
- Loading skeleton state
|
||||
- Empty state with CTA
|
||||
- "Load more" or pagination integration
|
||||
|
||||
---
|
||||
|
||||
#### 1.3 Add to Cart Button
|
||||
**File:** `app/templates/shared/macros/shop/add-to-cart.html`
|
||||
|
||||
Standardized add-to-cart functionality.
|
||||
|
||||
```jinja
|
||||
{{ add_to_cart_button(
|
||||
product_id='product.id',
|
||||
variant_id='selectedVariant?.id',
|
||||
quantity_model='quantity',
|
||||
show_quantity=true,
|
||||
size='md',
|
||||
full_width=false
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Loading state during add
|
||||
- Success animation
|
||||
- Out of stock state
|
||||
- Quantity selector integration (uses `number_stepper`)
|
||||
- "Added!" feedback
|
||||
|
||||
---
|
||||
|
||||
#### 1.4 Quantity Selector (Shop Version)
|
||||
**File:** Already exists as `number_stepper` in `inputs.html`
|
||||
|
||||
Shop-specific wrapper with stock validation:
|
||||
|
||||
```jinja
|
||||
{{ shop_quantity_selector(
|
||||
model='quantity',
|
||||
max='product.stock',
|
||||
disabled_var='addingToCart',
|
||||
out_of_stock_var='product.stock === 0'
|
||||
) }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Priority 2: Cart Components
|
||||
|
||||
#### 2.1 Mini Cart (Header Dropdown)
|
||||
**File:** `app/templates/shared/macros/shop/mini-cart.html`
|
||||
|
||||
Dropdown cart preview in the header.
|
||||
|
||||
```jinja
|
||||
{{ mini_cart(
|
||||
cart_var='cart',
|
||||
max_items=3,
|
||||
show_checkout_button=true
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Cart item count badge
|
||||
- Item thumbnails
|
||||
- Quick quantity adjust
|
||||
- Remove item button
|
||||
- Subtotal display
|
||||
- "View Cart" and "Checkout" CTAs
|
||||
|
||||
---
|
||||
|
||||
#### 2.2 Cart Item Row
|
||||
**File:** `app/templates/shared/macros/shop/cart-item.html`
|
||||
|
||||
Individual cart line item.
|
||||
|
||||
```jinja
|
||||
{{ cart_item(
|
||||
item='item',
|
||||
index='index',
|
||||
show_image=true,
|
||||
editable=true,
|
||||
compact=false
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Product thumbnail
|
||||
- Product name + variant info
|
||||
- Unit price
|
||||
- Quantity stepper (using `number_stepper` with `size='sm'`)
|
||||
- Line total
|
||||
- Remove button
|
||||
- Stock warning if low
|
||||
|
||||
---
|
||||
|
||||
#### 2.3 Cart Summary
|
||||
**File:** `app/templates/shared/macros/shop/cart-summary.html`
|
||||
|
||||
Order summary sidebar/section.
|
||||
|
||||
```jinja
|
||||
{{ cart_summary(
|
||||
cart_var='cart',
|
||||
show_promo_code=true,
|
||||
show_shipping_estimate=true,
|
||||
checkout_url='/checkout'
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Subtotal
|
||||
- Discount code input
|
||||
- Applied discounts
|
||||
- Estimated shipping
|
||||
- Tax display
|
||||
- Order total
|
||||
- Checkout CTA
|
||||
- Secure payment badges
|
||||
|
||||
---
|
||||
|
||||
### Priority 3: Product Detail Components
|
||||
|
||||
#### 3.1 Product Gallery
|
||||
**File:** `app/templates/shared/macros/shop/product-gallery.html`
|
||||
|
||||
Image gallery with thumbnails and zoom.
|
||||
|
||||
```jinja
|
||||
{{ product_gallery(
|
||||
images='product.images',
|
||||
show_thumbnails=true,
|
||||
enable_zoom=true,
|
||||
enable_fullscreen=true
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Main image display
|
||||
- Thumbnail navigation
|
||||
- Image zoom on hover
|
||||
- Fullscreen lightbox
|
||||
- Swipe support on mobile
|
||||
- Video support
|
||||
|
||||
---
|
||||
|
||||
#### 3.2 Variant Selector
|
||||
**File:** `app/templates/shared/macros/shop/variant-selector.html`
|
||||
|
||||
Product variant selection (size, color, etc.).
|
||||
|
||||
```jinja
|
||||
{{ variant_selector(
|
||||
variants='product.variants',
|
||||
selected_var='selectedVariant',
|
||||
type='buttons' {# buttons, dropdown, swatches #}
|
||||
) }}
|
||||
```
|
||||
|
||||
**Variant Types:**
|
||||
- **Buttons** - Size selection (S, M, L, XL)
|
||||
- **Dropdown** - Many options
|
||||
- **Swatches** - Color selection with preview
|
||||
|
||||
**Features:**
|
||||
- Out of stock variants disabled
|
||||
- Low stock warning
|
||||
- Price change on selection
|
||||
- Image change on color selection
|
||||
|
||||
---
|
||||
|
||||
#### 3.3 Product Info Block
|
||||
**File:** `app/templates/shared/macros/shop/product-info.html`
|
||||
|
||||
Product details section.
|
||||
|
||||
```jinja
|
||||
{{ product_info(
|
||||
product='product',
|
||||
show_sku=true,
|
||||
show_stock=true,
|
||||
show_vendor=false
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Product title (H1)
|
||||
- Price display (sale price handling)
|
||||
- Rating stars + review count
|
||||
- Short description
|
||||
- SKU display
|
||||
- Stock status
|
||||
- Vendor name (marketplace)
|
||||
|
||||
---
|
||||
|
||||
#### 3.4 Product Tabs
|
||||
**File:** `app/templates/shared/macros/shop/product-tabs.html`
|
||||
|
||||
Tabbed product information.
|
||||
|
||||
```jinja
|
||||
{{ product_tabs(
|
||||
product='product',
|
||||
tabs=['description', 'specifications', 'reviews', 'shipping']
|
||||
) }}
|
||||
```
|
||||
|
||||
**Tab Options:**
|
||||
- Description (HTML content)
|
||||
- Specifications (key-value table)
|
||||
- Reviews (review list + form)
|
||||
- Shipping info
|
||||
- Returns policy
|
||||
|
||||
---
|
||||
|
||||
### Priority 4: Navigation & Discovery
|
||||
|
||||
#### 4.1 Category Navigation
|
||||
**File:** `app/templates/shared/macros/shop/category-nav.html`
|
||||
|
||||
Category browsing sidebar/menu.
|
||||
|
||||
```jinja
|
||||
{{ category_nav(
|
||||
categories='categories',
|
||||
current_category='currentCategory',
|
||||
show_count=true,
|
||||
collapsible=true
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Nested category tree
|
||||
- Product counts
|
||||
- Active state highlighting
|
||||
- Collapsible sections
|
||||
- Mobile drawer version
|
||||
|
||||
---
|
||||
|
||||
#### 4.2 Breadcrumbs
|
||||
**File:** `app/templates/shared/macros/shop/breadcrumbs.html`
|
||||
|
||||
Navigation breadcrumb trail.
|
||||
|
||||
```jinja
|
||||
{{ shop_breadcrumbs(
|
||||
items=[
|
||||
{'label': 'Home', 'url': '/'},
|
||||
{'label': 'Electronics', 'url': '/category/electronics'},
|
||||
{'label': 'Headphones', 'url': none}
|
||||
]
|
||||
) }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 4.3 Search Bar
|
||||
**File:** `app/templates/shared/macros/shop/search-bar.html`
|
||||
|
||||
Product search with autocomplete.
|
||||
|
||||
```jinja
|
||||
{{ search_bar(
|
||||
placeholder='Search products...',
|
||||
show_suggestions=true,
|
||||
show_categories=true
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Autocomplete suggestions
|
||||
- Recent searches
|
||||
- Category filter
|
||||
- Voice search (optional)
|
||||
- Mobile fullscreen mode
|
||||
|
||||
---
|
||||
|
||||
#### 4.4 Filter Sidebar
|
||||
**File:** `app/templates/shared/macros/shop/filter-sidebar.html`
|
||||
|
||||
Product filtering panel.
|
||||
|
||||
```jinja
|
||||
{{ filter_sidebar(
|
||||
filters='availableFilters',
|
||||
active_filters='activeFilters',
|
||||
show_price_range=true,
|
||||
show_rating_filter=true
|
||||
) }}
|
||||
```
|
||||
|
||||
**Filter Types:**
|
||||
- Checkbox (brand, size)
|
||||
- Color swatches
|
||||
- Price range slider
|
||||
- Rating stars
|
||||
- In-stock toggle
|
||||
|
||||
---
|
||||
|
||||
### Priority 5: Social Proof & Trust
|
||||
|
||||
#### 5.1 Star Rating
|
||||
**File:** `app/templates/shared/macros/shop/star-rating.html`
|
||||
|
||||
Reusable star rating display.
|
||||
|
||||
```jinja
|
||||
{{ star_rating(
|
||||
rating=4.5,
|
||||
max=5,
|
||||
size='md',
|
||||
show_count=true,
|
||||
count=127
|
||||
) }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5.2 Review Card
|
||||
**File:** `app/templates/shared/macros/shop/review-card.html`
|
||||
|
||||
Individual product review.
|
||||
|
||||
```jinja
|
||||
{{ review_card(
|
||||
review='review',
|
||||
show_verified_badge=true,
|
||||
show_helpful_buttons=true
|
||||
) }}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Reviewer name/avatar
|
||||
- Star rating
|
||||
- Review date
|
||||
- Review text
|
||||
- Verified purchase badge
|
||||
- Helpful/Not helpful buttons
|
||||
- Review images
|
||||
|
||||
---
|
||||
|
||||
#### 5.3 Trust Badges
|
||||
**File:** `app/templates/shared/macros/shop/trust-badges.html`
|
||||
|
||||
Trust and security indicators.
|
||||
|
||||
```jinja
|
||||
{{ trust_badges(
|
||||
badges=['secure_payment', 'free_shipping', 'returns', 'support']
|
||||
) }}
|
||||
```
|
||||
|
||||
**Badge Options:**
|
||||
- Secure payment
|
||||
- Free shipping over X
|
||||
- Easy returns
|
||||
- 24/7 support
|
||||
- Money back guarantee
|
||||
- SSL secured
|
||||
|
||||
---
|
||||
|
||||
### Priority 6: Promotional Components
|
||||
|
||||
#### 6.1 Sale Banner
|
||||
**File:** `app/templates/shared/macros/shop/sale-banner.html`
|
||||
|
||||
Promotional banner with countdown.
|
||||
|
||||
```jinja
|
||||
{{ sale_banner(
|
||||
title='Summer Sale',
|
||||
subtitle='Up to 50% off',
|
||||
end_date='2025-08-31',
|
||||
show_countdown=true,
|
||||
cta_text='Shop Now',
|
||||
cta_url='/sale'
|
||||
) }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 6.2 Product Badge
|
||||
**File:** `app/templates/shared/macros/shop/product-badge.html`
|
||||
|
||||
Product overlay badges.
|
||||
|
||||
```jinja
|
||||
{{ product_badge(type='sale', value='-20%') }}
|
||||
{{ product_badge(type='new') }}
|
||||
{{ product_badge(type='bestseller') }}
|
||||
{{ product_badge(type='low_stock', value='Only 3 left') }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 6.3 Recently Viewed
|
||||
**File:** `app/templates/shared/macros/shop/recently-viewed.html`
|
||||
|
||||
Recently viewed products carousel.
|
||||
|
||||
```jinja
|
||||
{{ recently_viewed(
|
||||
products='recentlyViewed',
|
||||
max_items=6,
|
||||
title='Recently Viewed'
|
||||
) }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Components (Week 1-2)
|
||||
- [ ] Product Card
|
||||
- [ ] Product Grid
|
||||
- [ ] Add to Cart Button
|
||||
- [ ] Mini Cart
|
||||
- [ ] Cart Item Row
|
||||
- [ ] Cart Summary
|
||||
|
||||
### Phase 2: Product Detail (Week 3-4)
|
||||
- [ ] Product Gallery
|
||||
- [ ] Variant Selector
|
||||
- [ ] Product Info Block
|
||||
- [ ] Product Tabs
|
||||
- [ ] Star Rating
|
||||
|
||||
### Phase 3: Navigation (Week 5-6)
|
||||
- [ ] Category Navigation
|
||||
- [ ] Breadcrumbs
|
||||
- [ ] Search Bar
|
||||
- [ ] Filter Sidebar
|
||||
|
||||
### Phase 4: Social & Promo (Week 7-8)
|
||||
- [ ] Review Card
|
||||
- [ ] Trust Badges
|
||||
- [ ] Sale Banner
|
||||
- [ ] Product Badge
|
||||
- [ ] Recently Viewed
|
||||
|
||||
---
|
||||
|
||||
## CSS Variables for Theming
|
||||
|
||||
All shop components will use these CSS variables set by the vendor theme:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Primary brand colors */
|
||||
--color-primary: #7c3aed;
|
||||
--color-primary-hover: #6d28d9;
|
||||
--color-primary-light: #8b5cf6;
|
||||
|
||||
/* Secondary colors */
|
||||
--color-secondary: #f3f4f6;
|
||||
--color-secondary-hover: #e5e7eb;
|
||||
|
||||
/* Text colors */
|
||||
--color-text-primary: #111827;
|
||||
--color-text-secondary: #6b7280;
|
||||
|
||||
/* Status colors */
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-error: #ef4444;
|
||||
--color-sale: #dc2626;
|
||||
|
||||
/* UI colors */
|
||||
--color-border: #e5e7eb;
|
||||
--color-background: #ffffff;
|
||||
--color-surface: #f9fafb;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-unit: 0.25rem;
|
||||
|
||||
/* Border radius */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
app/templates/shared/macros/shop/
|
||||
├── product-card.html
|
||||
├── product-grid.html
|
||||
├── add-to-cart.html
|
||||
├── mini-cart.html
|
||||
├── cart-item.html
|
||||
├── cart-summary.html
|
||||
├── product-gallery.html
|
||||
├── variant-selector.html
|
||||
├── product-info.html
|
||||
├── product-tabs.html
|
||||
├── category-nav.html
|
||||
├── breadcrumbs.html
|
||||
├── search-bar.html
|
||||
├── filter-sidebar.html
|
||||
├── star-rating.html
|
||||
├── review-card.html
|
||||
├── trust-badges.html
|
||||
├── sale-banner.html
|
||||
├── product-badge.html
|
||||
└── recently-viewed.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review & Approve** - Team reviews this proposal
|
||||
2. **Prioritize** - Confirm component priority order
|
||||
3. **Design Mockups** - Create visual designs for key components
|
||||
4. **Implementation** - Build components in priority order
|
||||
5. **Documentation** - Add to component reference page
|
||||
6. **Testing** - Test across vendors and themes
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Component Standards](../shared/component-standards.md)
|
||||
- [Shop Architecture](architecture.md)
|
||||
- [Theme System](../../architecture/theme-system/overview.md)
|
||||
- [UI Components Quick Reference](../shared/ui-components-quick-reference.md)
|
||||
318
docs/frontend/storefront/navigation-flow.md
Normal file
318
docs/frontend/storefront/navigation-flow.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Shop Navigation Flow
|
||||
|
||||
Complete guide to navigation structure and URL hierarchy with landing pages.
|
||||
|
||||
## URL Hierarchy
|
||||
|
||||
```
|
||||
/ (Vendor Root) → Landing Page (if exists) OR redirect to /shop/
|
||||
├── /shop/ → E-commerce Homepage (Product Catalog)
|
||||
│ ├── /shop/products → Product Catalog (same as /shop/)
|
||||
│ ├── /shop/products/{id} → Product Detail Page
|
||||
│ ├── /shop/cart → Shopping Cart
|
||||
│ ├── /shop/checkout → Checkout Process
|
||||
│ ├── /shop/account/login → Customer Login
|
||||
│ ├── /shop/account/register → Customer Registration
|
||||
│ ├── /shop/account/dashboard → Customer Dashboard (auth required)
|
||||
│ ├── /shop/about → CMS Content Page
|
||||
│ ├── /shop/contact → CMS Content Page
|
||||
│ └── /shop/{slug} → Other CMS Pages
|
||||
```
|
||||
|
||||
## Navigation Patterns
|
||||
|
||||
### Pattern 1: With Landing Page (Recommended)
|
||||
|
||||
**URL Structure:**
|
||||
```
|
||||
customdomain.com/ → Landing Page (marketing/brand)
|
||||
customdomain.com/shop/ → E-commerce Shop
|
||||
customdomain.com/shop/products → Product Catalog
|
||||
```
|
||||
|
||||
**Navigation Flow:**
|
||||
1. User visits vendor domain → **Landing Page**
|
||||
2. Clicks "Shop Now" → **/shop/** (product catalog)
|
||||
3. Clicks "Home" in breadcrumb → **/** (back to landing page)
|
||||
4. Clicks logo → **/** (back to landing page)
|
||||
|
||||
**Breadcrumb Example (on Products page):**
|
||||
```
|
||||
Home > Products
|
||||
↓
|
||||
/ (Landing Page)
|
||||
```
|
||||
|
||||
### Pattern 2: Without Landing Page (Auto-Redirect)
|
||||
|
||||
**URL Structure:**
|
||||
```
|
||||
customdomain.com/ → Redirects to /shop/
|
||||
customdomain.com/shop/ → E-commerce Shop
|
||||
customdomain.com/shop/products → Product Catalog
|
||||
```
|
||||
|
||||
**Navigation Flow:**
|
||||
1. User visits vendor domain → **Redirects to /shop/**
|
||||
2. User browses shop
|
||||
3. Clicks "Home" in breadcrumb → **/** (redirects to /shop/)
|
||||
4. Clicks logo → **/** (redirects to /shop/)
|
||||
|
||||
**Breadcrumb Example (on Products page):**
|
||||
```
|
||||
Home > Products
|
||||
↓
|
||||
/ → /shop/ (redirects)
|
||||
```
|
||||
|
||||
## Link References
|
||||
|
||||
### Base URL Calculation
|
||||
|
||||
The `base_url` variable is calculated in `shop_pages.py:get_shop_context()`:
|
||||
|
||||
```python
|
||||
# For domain/subdomain access
|
||||
base_url = "/"
|
||||
# Result: /shop/products, /shop/cart, etc.
|
||||
|
||||
# For path-based access
|
||||
base_url = "/vendors/wizamart/"
|
||||
# Result: /vendors/wizamart/shop/products, /vendors/wizamart/shop/cart, etc.
|
||||
```
|
||||
|
||||
### Template Links
|
||||
|
||||
**Logo / Home Link (Header):**
|
||||
```jinja2
|
||||
{# Points to vendor root (landing page or shop) #}
|
||||
<a href="{{ base_url }}shop/">{{ vendor.name }}</a>
|
||||
```
|
||||
|
||||
**Breadcrumb Home Link:**
|
||||
```jinja2
|
||||
{# Points to vendor root (landing page) #}
|
||||
<a href="{{ base_url }}">Home</a>
|
||||
```
|
||||
|
||||
**Shop Links:**
|
||||
```jinja2
|
||||
{# All shop pages include /shop/ prefix #}
|
||||
<a href="{{ base_url }}shop/products">Products</a>
|
||||
<a href="{{ base_url }}shop/cart">Cart</a>
|
||||
<a href="{{ base_url }}shop/checkout">Checkout</a>
|
||||
```
|
||||
|
||||
**CMS Page Links:**
|
||||
```jinja2
|
||||
{# CMS pages are under /shop/ #}
|
||||
<a href="{{ base_url }}shop/about">About</a>
|
||||
<a href="{{ base_url }}shop/contact">Contact</a>
|
||||
<a href="{{ base_url }}shop/{{ page.slug }}">{{ page.title }}</a>
|
||||
```
|
||||
|
||||
## Complete URL Examples
|
||||
|
||||
### Path-Based Access (Development)
|
||||
|
||||
```
|
||||
http://localhost:8000/vendors/wizamart/
|
||||
├── / (root) → Landing Page OR redirect to shop
|
||||
├── /shop/ → Shop Homepage
|
||||
├── /shop/products → Product Catalog
|
||||
├── /shop/products/4 → Product Detail
|
||||
├── /shop/cart → Shopping Cart
|
||||
├── /shop/checkout → Checkout
|
||||
├── /shop/account/login → Customer Login
|
||||
├── /shop/about → About Page (CMS)
|
||||
└── /shop/contact → Contact Page (CMS)
|
||||
```
|
||||
|
||||
### Subdomain Access (Production)
|
||||
|
||||
```
|
||||
https://wizamart.platform.com/
|
||||
├── / (root) → Landing Page OR redirect to shop
|
||||
├── /shop/ → Shop Homepage
|
||||
├── /shop/products → Product Catalog
|
||||
├── /shop/products/4 → Product Detail
|
||||
├── /shop/cart → Shopping Cart
|
||||
├── /shop/checkout → Checkout
|
||||
├── /shop/account/login → Customer Login
|
||||
├── /shop/about → About Page (CMS)
|
||||
└── /shop/contact → Contact Page (CMS)
|
||||
```
|
||||
|
||||
### Custom Domain Access (Production)
|
||||
|
||||
```
|
||||
https://customdomain.com/
|
||||
├── / (root) → Landing Page OR redirect to shop
|
||||
├── /shop/ → Shop Homepage
|
||||
├── /shop/products → Product Catalog
|
||||
├── /shop/products/4 → Product Detail
|
||||
├── /shop/cart → Shopping Cart
|
||||
├── /shop/checkout → Checkout
|
||||
├── /shop/account/login → Customer Login
|
||||
├── /shop/about → About Page (CMS)
|
||||
└── /shop/contact → Contact Page (CMS)
|
||||
```
|
||||
|
||||
## User Journeys
|
||||
|
||||
### Journey 1: First-Time Visitor (With Landing Page)
|
||||
|
||||
1. Visit `customdomain.com/` → **Landing Page**
|
||||
- Sees brand story, features, CTA
|
||||
2. Clicks "Shop Now" → `/shop/` → **Product Catalog**
|
||||
- Browses products
|
||||
3. Clicks product → `/shop/products/4` → **Product Detail**
|
||||
- Views product
|
||||
4. Clicks "Home" in breadcrumb → `/` → **Back to Landing Page**
|
||||
5. Clicks logo → `/` → **Back to Landing Page**
|
||||
|
||||
### Journey 2: Returning Customer (Direct to Shop)
|
||||
|
||||
1. Visit `customdomain.com/shop/` → **Product Catalog**
|
||||
- Already knows the brand, goes straight to shop
|
||||
2. Adds to cart → `/shop/cart` → **Shopping Cart**
|
||||
3. Checkout → `/shop/checkout` → **Checkout**
|
||||
4. Clicks "Home" → `/` → **Landing Page** (brand homepage)
|
||||
|
||||
### Journey 3: Customer Account Management
|
||||
|
||||
1. Visit `customdomain.com/shop/account/login` → **Login**
|
||||
2. After login → `/shop/account/dashboard` → **Dashboard**
|
||||
3. View orders → `/shop/account/orders` → **Order History**
|
||||
4. Clicks logo → `/` → **Back to Landing Page**
|
||||
|
||||
## Navigation Components
|
||||
|
||||
### Header Navigation (base.html)
|
||||
|
||||
```jinja2
|
||||
{# Logo - always points to vendor root #}
|
||||
<a href="{{ base_url }}shop/">
|
||||
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}">
|
||||
</a>
|
||||
|
||||
{# Main Navigation #}
|
||||
<nav>
|
||||
<a href="{{ base_url }}shop/">Home</a>
|
||||
<a href="{{ base_url }}shop/products">Products</a>
|
||||
<a href="{{ base_url }}shop/about">About</a>
|
||||
<a href="{{ base_url }}shop/contact">Contact</a>
|
||||
</nav>
|
||||
|
||||
{# Actions #}
|
||||
<a href="{{ base_url }}shop/cart">Cart</a>
|
||||
<a href="{{ base_url }}shop/account">Account</a>
|
||||
```
|
||||
|
||||
### Footer Navigation (base.html)
|
||||
|
||||
```jinja2
|
||||
{# Quick Links #}
|
||||
<a href="{{ base_url }}shop/products">Products</a>
|
||||
|
||||
{# CMS Pages (dynamic) #}
|
||||
{% for page in footer_pages %}
|
||||
<a href="{{ base_url }}shop/{{ page.slug }}">{{ page.title }}</a>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Breadcrumbs (products.html, content-page.html)
|
||||
|
||||
```jinja2
|
||||
{# Points to vendor root (landing page) #}
|
||||
<a href="{{ base_url }}">Home</a> / Products
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO:
|
||||
|
||||
1. **Use Landing Pages**: Create engaging landing pages at vendor root
|
||||
2. **Clear Navigation**: Make it easy to get from landing to shop and back
|
||||
3. **Consistent "Home"**: Logo and "Home" breadcrumb both point to `/` (landing)
|
||||
4. **Shop Links**: All shop-related links include `/shop/` prefix
|
||||
5. **CMS Under Shop**: Keep CMS pages under `/shop/` for consistency
|
||||
|
||||
### ❌ DON'T:
|
||||
|
||||
1. **Hardcode URLs**: Always use `{{ base_url }}` for vendor-aware links
|
||||
2. **Skip /shop/**: Don't link directly to `/products`, use `/shop/products`
|
||||
3. **Mix Landing & Shop**: Keep landing page separate from shop catalog
|
||||
4. **Forget Breadcrumbs**: Always provide "Home" link to go back
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Before Landing Pages
|
||||
|
||||
All links pointed to `/shop/`:
|
||||
```jinja2
|
||||
<a href="{{ base_url }}shop/">Home</a> {# Logo #}
|
||||
<a href="{{ base_url }}shop/">Home</a> {# Breadcrumb #}
|
||||
```
|
||||
|
||||
### After Landing Pages
|
||||
|
||||
Separation of concerns:
|
||||
```jinja2
|
||||
<a href="{{ base_url }}shop/">Home</a> {# Logo - still goes to shop #}
|
||||
<a href="{{ base_url }}">Home</a> {# Breadcrumb - goes to landing #}
|
||||
```
|
||||
|
||||
This allows:
|
||||
- Landing page at `/` for marketing/branding
|
||||
- Shop catalog at `/shop/` for e-commerce
|
||||
- Clean navigation between the two
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Route Handlers (main.py)
|
||||
|
||||
```python
|
||||
# Vendor root - serves landing page or redirects to shop
|
||||
@app.get("/")
|
||||
@app.get("/vendors/{vendor_code}/")
|
||||
async def root(request: Request):
|
||||
if has_landing_page():
|
||||
return render_landing_page()
|
||||
else:
|
||||
return redirect_to_shop()
|
||||
|
||||
# Shop routes
|
||||
@app.include_router(shop_pages.router, prefix="/shop")
|
||||
@app.include_router(shop_pages.router, prefix="/vendors/{vendor_code}/shop")
|
||||
```
|
||||
|
||||
### Context Calculation (shop_pages.py)
|
||||
|
||||
```python
|
||||
def get_shop_context(request: Request):
|
||||
base_url = "/"
|
||||
if access_method == "path":
|
||||
base_url = f"/vendors/{vendor.subdomain}/"
|
||||
|
||||
return {
|
||||
"base_url": base_url,
|
||||
"vendor": vendor,
|
||||
"theme": theme,
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The navigation system creates a **two-tier structure**:
|
||||
|
||||
1. **Landing Page** (`/`) - Marketing, branding, vendor story
|
||||
2. **Shop** (`/shop/`) - E-commerce, products, cart, checkout
|
||||
|
||||
This gives vendors flexibility to:
|
||||
- Have a marketing homepage separate from their store
|
||||
- Choose different landing page designs (minimal, modern, full)
|
||||
- Or skip the landing page and go straight to the shop
|
||||
|
||||
All while maintaining clean, consistent navigation throughout the experience.
|
||||
994
docs/frontend/storefront/page-templates.md
Normal file
994
docs/frontend/storefront/page-templates.md
Normal file
@@ -0,0 +1,994 @@
|
||||
# Shop Frontend - Alpine.js/Jinja2 Page Template Guide
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
This guide provides complete templates for creating new customer-facing shop pages using the established Alpine.js + Jinja2 + Multi-Theme architecture. Follow these patterns to ensure consistency across all vendor shops while maintaining unique branding.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication Pages (Available)
|
||||
|
||||
Three fully-implemented authentication pages are available for reference:
|
||||
|
||||
- **Login** (`app/templates/shop/account/login.html`) - Customer sign-in with email/password
|
||||
- **Register** (`app/templates/shop/account/register.html`) - New customer account creation
|
||||
- **Forgot Password** (`app/templates/shop/account/forgot-password.html`) - Password reset flow
|
||||
|
||||
All authentication pages feature:
|
||||
- ✅ Tailwind CSS styling
|
||||
- ✅ Alpine.js interactivity
|
||||
- ✅ Theme integration (vendor colors, logos, fonts)
|
||||
- ✅ Dark mode support
|
||||
- ✅ Mobile responsive design
|
||||
- ✅ Form validation
|
||||
- ✅ Loading states
|
||||
- ✅ Error handling
|
||||
|
||||
See the [Shop Architecture Documentation](./architecture.md) (Authentication Pages section) for complete details.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Reference
|
||||
|
||||
### File Structure for New Page
|
||||
```
|
||||
app/
|
||||
├── templates/shop/
|
||||
│ └── [page-name].html # Jinja2 template
|
||||
├── static/shop/js/
|
||||
│ └── [page-name].js # Alpine.js component
|
||||
└── api/v1/shop/
|
||||
└── pages.py # Route registration
|
||||
```
|
||||
|
||||
### Checklist for New Page
|
||||
- [ ] Create Jinja2 template extending shop/base.html
|
||||
- [ ] Create Alpine.js JavaScript component
|
||||
- [ ] Register route in pages.py
|
||||
- [ ] Test with multiple vendor themes
|
||||
- [ ] Test responsive design (mobile/tablet/desktop)
|
||||
- [ ] Test dark mode
|
||||
- [ ] Test cart integration (if applicable)
|
||||
- [ ] Verify theme CSS variables work
|
||||
- [ ] Check image optimization
|
||||
|
||||
---
|
||||
|
||||
## 📄 Template Structure
|
||||
|
||||
### 1. Jinja2 Template
|
||||
|
||||
**File:** `app/templates/shop/[page-name].html`
|
||||
|
||||
```jinja2
|
||||
{# app/templates/shop/[page-name].html #}
|
||||
{% extends "shop/base.html" %}
|
||||
|
||||
{# Page title for browser tab - includes vendor name #}
|
||||
{% block title %}[Page Name] - {{ vendor.name }}{% endblock %}
|
||||
|
||||
{# Meta description for SEO #}
|
||||
{% block meta_description %}[Page description for SEO]{% endblock %}
|
||||
|
||||
{# Alpine.js component name #}
|
||||
{% block alpine_data %}shop[PageName](){% endblock %}
|
||||
|
||||
{# Page content #}
|
||||
{% block content %}
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- PAGE HEADER -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex mb-6 text-sm" aria-label="Breadcrumb">
|
||||
<ol class="inline-flex items-center space-x-1 md:space-x-3">
|
||||
<li class="inline-flex items-center">
|
||||
<a href="/" class="text-gray-700 hover:text-primary dark:text-gray-400">
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 text-gray-400 mx-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="text-gray-500 dark:text-gray-400">[Page Name]</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Page Title -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white"
|
||||
style="font-family: var(--font-heading)">
|
||||
[Page Name]
|
||||
</h1>
|
||||
|
||||
<!-- Optional action button -->
|
||||
<button
|
||||
@click="someAction()"
|
||||
class="px-6 py-2 text-white rounded-lg transition-colors"
|
||||
style="background-color: var(--color-primary)"
|
||||
:style="{ 'background-color': 'var(--color-primary)' }"
|
||||
>
|
||||
Action
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- LOADING STATE -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div x-show="loading" class="text-center py-20">
|
||||
<div class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-gray-200"
|
||||
:style="{ 'border-top-color': 'var(--color-primary)' }">
|
||||
</div>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-400">Loading...</p>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- ERROR STATE -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div x-show="error && !loading"
|
||||
class="mb-6 p-6 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 text-red-600 dark:text-red-400 mr-3 flex-shrink-0"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="text-red-800 dark:text-red-200 font-semibold">Error</h3>
|
||||
<p class="text-red-700 dark:text-red-300 text-sm mt-1" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- MAIN CONTENT -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<div x-show="!loading">
|
||||
|
||||
<!-- Empty State -->
|
||||
<div x-show="items.length === 0" class="text-center py-20">
|
||||
<svg class="w-24 h-24 mx-auto text-gray-300 dark:text-gray-600 mb-4"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
|
||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
No items found
|
||||
</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
Try adjusting your filters or check back later.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Grid Layout (for products, items, etc.) -->
|
||||
<div x-show="items.length > 0"
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700">
|
||||
|
||||
<!-- Item Image -->
|
||||
<div class="aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-t-lg bg-gray-100 dark:bg-gray-700">
|
||||
<img :src="item.image || '/static/shop/img/placeholder-product.png'"
|
||||
:alt="item.name"
|
||||
class="w-full h-full object-cover object-center hover:scale-105 transition-transform"
|
||||
loading="lazy">
|
||||
</div>
|
||||
|
||||
<!-- Item Info -->
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2"
|
||||
x-text="item.name"></h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-2"
|
||||
x-text="item.description"></p>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-2xl font-bold"
|
||||
:style="{ color: 'var(--color-primary)' }"
|
||||
x-text="formatPrice(item.price)"></span>
|
||||
|
||||
<button @click="addToCart(item)"
|
||||
class="px-4 py-2 text-white rounded-lg hover:opacity-90 transition-opacity"
|
||||
:style="{ 'background-color': 'var(--color-primary)' }">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ═════════════════════════════════════════════════════════════ -->
|
||||
<!-- PAGINATION -->
|
||||
<!-- ═════════════════════════════════════════════════════════════ -->
|
||||
<div x-show="pagination.totalPages > 1"
|
||||
class="flex justify-center items-center space-x-2 mt-12">
|
||||
|
||||
<!-- Previous Button -->
|
||||
<button
|
||||
@click="goToPage(pagination.currentPage - 1)"
|
||||
:disabled="pagination.currentPage === 1"
|
||||
class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<!-- Page Numbers -->
|
||||
<template x-for="page in paginationRange" :key="page">
|
||||
<button
|
||||
@click="goToPage(page)"
|
||||
:class="page === pagination.currentPage
|
||||
? 'text-white'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'"
|
||||
:style="page === pagination.currentPage ? { 'background-color': 'var(--color-primary)' } : {}"
|
||||
class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600"
|
||||
x-text="page"
|
||||
></button>
|
||||
</template>
|
||||
|
||||
<!-- Next Button -->
|
||||
<button
|
||||
@click="goToPage(pagination.currentPage + 1)"
|
||||
:disabled="pagination.currentPage === pagination.totalPages"
|
||||
class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# Page-specific JavaScript #}
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='shop/js/[page-name].js') }}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Alpine.js Component
|
||||
|
||||
**File:** `app/static/shop/js/[page-name].js`
|
||||
|
||||
```javascript
|
||||
// static/shop/js/[page-name].js
|
||||
/**
|
||||
* [Page Name] Component
|
||||
* Handles [describe functionality]
|
||||
*/
|
||||
|
||||
const pageLog = {
|
||||
info: (...args) => console.info('🛍️ [PAGE]', ...args),
|
||||
warn: (...args) => console.warn('⚠️ [PAGE]', ...args),
|
||||
error: (...args) => console.error('❌ [PAGE]', ...args),
|
||||
debug: (...args) => console.log('🔍 [PAGE]', ...args)
|
||||
};
|
||||
|
||||
/**
|
||||
* Main Alpine.js component for [page name]
|
||||
*/
|
||||
function shop[PageName]() {
|
||||
return {
|
||||
// ─────────────────────────────────────────────────────
|
||||
// STATE
|
||||
// ─────────────────────────────────────────────────────
|
||||
loading: false,
|
||||
error: '',
|
||||
items: [],
|
||||
|
||||
// Pagination
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
perPage: 12,
|
||||
total: 0
|
||||
},
|
||||
|
||||
// Filters
|
||||
filters: {
|
||||
search: '',
|
||||
category: '',
|
||||
sortBy: 'created_at:desc'
|
||||
},
|
||||
|
||||
// Vendor info (from template)
|
||||
vendorCode: '{{ vendor.code }}',
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// LIFECYCLE
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Initialize component
|
||||
*/
|
||||
async init() {
|
||||
pageLog.info('[PageName] initializing...');
|
||||
await this.loadData();
|
||||
pageLog.info('[PageName] initialized');
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// DATA LOADING
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Load main data from API
|
||||
*/
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: this.pagination.currentPage,
|
||||
per_page: this.pagination.perPage,
|
||||
...this.filters
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`/api/v1/shop/${this.vendorCode}/items?${params}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Update state
|
||||
this.items = data.items || [];
|
||||
this.pagination.total = data.total || 0;
|
||||
this.pagination.totalPages = Math.ceil(
|
||||
this.pagination.total / this.pagination.perPage
|
||||
);
|
||||
|
||||
pageLog.info('Data loaded:', this.items.length, 'items');
|
||||
|
||||
} catch (error) {
|
||||
pageLog.error('Failed to load data:', error);
|
||||
this.error = error.message || 'Failed to load data';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh data
|
||||
*/
|
||||
async refresh() {
|
||||
pageLog.info('Refreshing data...');
|
||||
this.error = '';
|
||||
await this.loadData();
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// FILTERS & SEARCH
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Apply filters and reload data
|
||||
*/
|
||||
async applyFilters() {
|
||||
pageLog.debug('Applying filters:', this.filters);
|
||||
this.pagination.currentPage = 1; // Reset to first page
|
||||
await this.loadData();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset filters to default
|
||||
*/
|
||||
async resetFilters() {
|
||||
this.filters = {
|
||||
search: '',
|
||||
category: '',
|
||||
sortBy: 'created_at:desc'
|
||||
};
|
||||
await this.applyFilters();
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// PAGINATION
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Navigate to specific page
|
||||
*/
|
||||
async goToPage(page) {
|
||||
if (page < 1 || page > this.pagination.totalPages) return;
|
||||
|
||||
this.pagination.currentPage = page;
|
||||
await this.loadData();
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
|
||||
/**
|
||||
* Get pagination range for display
|
||||
*/
|
||||
get paginationRange() {
|
||||
const current = this.pagination.currentPage;
|
||||
const total = this.pagination.totalPages;
|
||||
const range = [];
|
||||
|
||||
// Show max 7 page numbers
|
||||
let start = Math.max(1, current - 3);
|
||||
let end = Math.min(total, start + 6);
|
||||
|
||||
// Adjust start if we're near the end
|
||||
if (end - start < 6) {
|
||||
start = Math.max(1, end - 6);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// CART INTEGRATION
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Add item to cart
|
||||
*/
|
||||
addToCart(item, quantity = 1) {
|
||||
pageLog.info('Adding to cart:', item.name);
|
||||
|
||||
// Get cart from shop layout
|
||||
const shopLayout = Alpine.store('shop') || window.shopLayoutData();
|
||||
|
||||
if (shopLayout && typeof shopLayout.addToCart === 'function') {
|
||||
shopLayout.addToCart(item, quantity);
|
||||
this.showToast(`${item.name} added to cart`, 'success');
|
||||
} else {
|
||||
pageLog.error('Shop layout not available');
|
||||
}
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// UI HELPERS
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
showToast(message, type = 'info') {
|
||||
const shopLayout = Alpine.store('shop') || window.shopLayoutData();
|
||||
if (shopLayout && typeof shopLayout.showToast === 'function') {
|
||||
shopLayout.showToast(message, type);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format price as currency
|
||||
*/
|
||||
formatPrice(price) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(price);
|
||||
},
|
||||
|
||||
/**
|
||||
* Format date
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Truncate text
|
||||
*/
|
||||
truncate(text, length = 100) {
|
||||
if (!text) return '';
|
||||
if (text.length <= length) return text;
|
||||
return text.substring(0, length) + '...';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.shop[PageName] = shop[PageName];
|
||||
|
||||
pageLog.info('[PageName] module loaded');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Route Registration
|
||||
|
||||
**File:** `app/api/v1/shop/pages.py`
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/[page-route]")
|
||||
async def [page_name]_page(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
[Page Name] page
|
||||
Displays [description]
|
||||
"""
|
||||
# Vendor and theme come from middleware
|
||||
vendor = request.state.vendor
|
||||
theme = request.state.theme
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/[page-name].html",
|
||||
{
|
||||
"request": request,
|
||||
"vendor": vendor,
|
||||
"theme": theme,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Common Page Patterns
|
||||
|
||||
### Pattern 1: Product Grid Page (Homepage, Category)
|
||||
|
||||
**Use for:** Homepage, category pages, search results
|
||||
|
||||
```javascript
|
||||
async init() {
|
||||
await this.loadProducts();
|
||||
}
|
||||
|
||||
async loadProducts() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/shop/${this.vendorCode}/products?category=${this.category}`
|
||||
);
|
||||
const data = await response.json();
|
||||
this.products = data.products || [];
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Template:**
|
||||
```html
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<template x-for="product in products" :key="product.id">
|
||||
{% include 'shop/partials/product-card.html' %}
|
||||
</template>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Product Detail Page
|
||||
|
||||
**Use for:** Single product pages
|
||||
|
||||
```javascript
|
||||
async init() {
|
||||
const productId = this.getProductIdFromUrl();
|
||||
await this.loadProduct(productId);
|
||||
await this.loadRelatedProducts(productId);
|
||||
}
|
||||
|
||||
async loadProduct(id) {
|
||||
const product = await fetch(
|
||||
`/api/v1/shop/${this.vendorCode}/products/${id}`
|
||||
).then(r => r.json());
|
||||
|
||||
this.product = product;
|
||||
this.selectedImage = product.images[0];
|
||||
}
|
||||
|
||||
addToCartWithQuantity() {
|
||||
const shopLayout = window.shopLayoutData();
|
||||
shopLayout.addToCart(this.product, this.quantity);
|
||||
}
|
||||
```
|
||||
|
||||
**Template:**
|
||||
```html
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<!-- Image Gallery -->
|
||||
<div>
|
||||
<img :src="selectedImage" class="w-full rounded-lg">
|
||||
<div class="grid grid-cols-4 gap-2 mt-4">
|
||||
<template x-for="img in product.images">
|
||||
<img @click="selectedImage = img"
|
||||
:src="img"
|
||||
class="cursor-pointer rounded border-2"
|
||||
:class="selectedImage === img ? 'border-primary' : 'border-gray-200'">
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Product Info -->
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-4" x-text="product.name"></h1>
|
||||
<p class="text-2xl font-bold mb-6"
|
||||
:style="{ color: 'var(--color-primary)' }"
|
||||
x-text="formatPrice(product.price)"></p>
|
||||
<p class="text-gray-600 mb-8" x-text="product.description"></p>
|
||||
|
||||
<!-- Quantity -->
|
||||
<div class="flex items-center space-x-4 mb-6">
|
||||
<label>Quantity:</label>
|
||||
<input type="number" x-model="quantity" min="1" class="w-20 px-3 py-2 border rounded">
|
||||
</div>
|
||||
|
||||
<!-- Add to Cart -->
|
||||
<button @click="addToCartWithQuantity()"
|
||||
class="w-full py-3 text-white rounded-lg text-lg font-semibold"
|
||||
:style="{ 'background-color': 'var(--color-primary)' }">
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Cart Page
|
||||
|
||||
**Use for:** Shopping cart
|
||||
|
||||
```javascript
|
||||
async init() {
|
||||
this.loadCart();
|
||||
}
|
||||
|
||||
loadCart() {
|
||||
const shopLayout = window.shopLayoutData();
|
||||
this.cart = shopLayout.cart;
|
||||
this.calculateTotals();
|
||||
}
|
||||
|
||||
updateQuantity(productId, quantity) {
|
||||
const shopLayout = window.shopLayoutData();
|
||||
shopLayout.updateCartItem(productId, quantity);
|
||||
this.loadCart();
|
||||
}
|
||||
|
||||
removeItem(productId) {
|
||||
const shopLayout = window.shopLayoutData();
|
||||
shopLayout.removeFromCart(productId);
|
||||
this.loadCart();
|
||||
}
|
||||
|
||||
get subtotal() {
|
||||
return this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
}
|
||||
|
||||
get shipping() {
|
||||
return this.subtotal > 50 ? 0 : 9.99;
|
||||
}
|
||||
|
||||
get total() {
|
||||
return this.subtotal + this.shipping;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 4: Search & Filter Page
|
||||
|
||||
**Use for:** Search results, filtered product lists
|
||||
|
||||
```javascript
|
||||
filters: {
|
||||
search: '',
|
||||
category: '',
|
||||
minPrice: 0,
|
||||
maxPrice: 1000,
|
||||
sortBy: 'relevance',
|
||||
inStock: true
|
||||
},
|
||||
|
||||
async performSearch() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/shop/${this.vendorCode}/search`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.filters)
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
this.results = data.results || [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Best Practices
|
||||
|
||||
### 1. Theme Integration
|
||||
|
||||
Always use CSS variables for vendor colors:
|
||||
|
||||
```html
|
||||
<!-- ✅ GOOD: Uses theme variable -->
|
||||
<button :style="{ 'background-color': 'var(--color-primary)' }">
|
||||
Buy Now
|
||||
</button>
|
||||
|
||||
<!-- ❌ BAD: Hardcoded color -->
|
||||
<button class="bg-blue-500">
|
||||
Buy Now
|
||||
</button>
|
||||
```
|
||||
|
||||
### 2. Cart Integration
|
||||
|
||||
Always use the shop layout's cart methods:
|
||||
|
||||
```javascript
|
||||
// ✅ GOOD: Uses shop layout
|
||||
const shopLayout = window.shopLayoutData();
|
||||
shopLayout.addToCart(product, quantity);
|
||||
|
||||
// ❌ BAD: Direct localStorage manipulation
|
||||
localStorage.setItem('cart', JSON.stringify(cart));
|
||||
```
|
||||
|
||||
### 3. Loading States
|
||||
|
||||
Always show loading indicators:
|
||||
|
||||
```javascript
|
||||
this.loading = true;
|
||||
try {
|
||||
// ... async operation
|
||||
} finally {
|
||||
this.loading = false; // Always executes
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
Always handle errors gracefully:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
await this.loadData();
|
||||
} catch (error) {
|
||||
console.error('Load failed:', error);
|
||||
this.error = 'Unable to load products. Please try again.';
|
||||
// Don't throw - let UI handle gracefully
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Responsive Images
|
||||
|
||||
Use lazy loading and responsive images:
|
||||
|
||||
```html
|
||||
<img :src="product.image"
|
||||
:alt="product.name"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover">
|
||||
```
|
||||
|
||||
### 6. Dark Mode
|
||||
|
||||
Support both light and dark modes:
|
||||
|
||||
```html
|
||||
<div class="bg-white dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||
Content
|
||||
</div>
|
||||
```
|
||||
|
||||
### 7. Accessibility
|
||||
|
||||
Add proper ARIA labels and keyboard navigation:
|
||||
|
||||
```html
|
||||
<button @click="addToCart(product)"
|
||||
aria-label="Add to cart"
|
||||
role="button">
|
||||
Add to Cart
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 Responsive Design Checklist
|
||||
|
||||
- [ ] Mobile (< 640px): Single column layout
|
||||
- [ ] Tablet (640px - 1024px): 2-3 column layout
|
||||
- [ ] Desktop (> 1024px): 4 column layout
|
||||
- [ ] Images scale properly on all devices
|
||||
- [ ] Touch targets are at least 44x44px
|
||||
- [ ] Text is readable without zooming
|
||||
- [ ] Navigation adapts to screen size
|
||||
- [ ] Modals are scrollable on small screens
|
||||
- [ ] Forms are easy to fill on mobile
|
||||
|
||||
---
|
||||
|
||||
## ✅ Testing Checklist
|
||||
|
||||
### Functionality
|
||||
- [ ] Page loads without errors
|
||||
- [ ] Data loads correctly
|
||||
- [ ] Loading state displays
|
||||
- [ ] Error state handles failures
|
||||
- [ ] Empty state shows when no data
|
||||
- [ ] Filters work correctly
|
||||
- [ ] Pagination works
|
||||
- [ ] Cart integration works
|
||||
|
||||
### Theme Integration
|
||||
- [ ] Vendor colors display correctly
|
||||
- [ ] Vendor logo displays
|
||||
- [ ] Custom fonts load
|
||||
- [ ] Custom CSS applies
|
||||
- [ ] Dark mode works with vendor colors
|
||||
|
||||
### Responsive Design
|
||||
- [ ] Mobile layout works
|
||||
- [ ] Tablet layout works
|
||||
- [ ] Desktop layout works
|
||||
- [ ] Images are responsive
|
||||
- [ ] Touch interactions work
|
||||
|
||||
### Performance
|
||||
- [ ] Page loads quickly
|
||||
- [ ] Images load progressively
|
||||
- [ ] No console errors
|
||||
- [ ] No memory leaks
|
||||
|
||||
### Accessibility
|
||||
- [ ] Keyboard navigation works
|
||||
- [ ] Screen reader compatible
|
||||
- [ ] Color contrast sufficient
|
||||
- [ ] ARIA labels present
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Component Library
|
||||
|
||||
### Reusable Partials
|
||||
|
||||
Create reusable components in `templates/shop/partials/`:
|
||||
|
||||
**product-card.html:**
|
||||
```html
|
||||
<div class="product-card bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg">
|
||||
<img :src="product.image" :alt="product.name" class="w-full h-64 object-cover rounded-t-lg">
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold text-lg" x-text="product.name"></h3>
|
||||
<p class="text-2xl font-bold"
|
||||
:style="{ color: 'var(--color-primary)' }"
|
||||
x-text="formatPrice(product.price)"></p>
|
||||
<button @click="addToCart(product)"
|
||||
class="w-full mt-4 py-2 text-white rounded"
|
||||
:style="{ 'background-color': 'var(--color-primary)' }">
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**filter-sidebar.html:**
|
||||
```html
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="font-semibold mb-4">Filters</h3>
|
||||
|
||||
<!-- Category -->
|
||||
<div class="mb-6">
|
||||
<label class="block mb-2 font-medium">Category</label>
|
||||
<select x-model="filters.category" @change="applyFilters()"
|
||||
class="w-full px-3 py-2 border rounded">
|
||||
<option value="">All Categories</option>
|
||||
<template x-for="cat in categories">
|
||||
<option :value="cat.id" x-text="cat.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Price Range -->
|
||||
<div class="mb-6">
|
||||
<label class="block mb-2 font-medium">Price Range</label>
|
||||
<input type="range" x-model="filters.maxPrice"
|
||||
min="0" max="1000" step="10"
|
||||
class="w-full">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span>$0</span>
|
||||
<span x-text="'$' + filters.maxPrice"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apply Button -->
|
||||
<button @click="applyFilters()"
|
||||
class="w-full py-2 text-white rounded"
|
||||
:style="{ 'background-color': 'var(--color-primary)' }">
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start Commands
|
||||
|
||||
```bash
|
||||
# Create new page files
|
||||
touch app/templates/shop/new-page.html
|
||||
touch app/static/shop/js/new-page.js
|
||||
|
||||
# Copy templates
|
||||
cp template.html app/templates/shop/new-page.html
|
||||
cp template.js app/static/shop/js/new-page.js
|
||||
|
||||
# Update placeholders:
|
||||
# - Replace [page-name] with actual name
|
||||
# - Replace [PageName] with PascalCase name
|
||||
# - Add route in pages.py
|
||||
# - Test with multiple vendor themes!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
### Theme System
|
||||
- **CSS Variables**: All vendor colors in `var(--color-name)` format
|
||||
- **Fonts**: `var(--font-heading)` and `var(--font-body)`
|
||||
- **Logo**: Available in both light and dark versions
|
||||
- **Custom CSS**: Vendor-specific styles automatically injected
|
||||
|
||||
### Shop Layout Functions
|
||||
- `addToCart(product, quantity)`: Add item to cart
|
||||
- `showToast(message, type)`: Show notification
|
||||
- `formatPrice(amount)`: Format as currency
|
||||
- `formatDate(date)`: Format date string
|
||||
|
||||
### Icons
|
||||
Use the global icon helper:
|
||||
```html
|
||||
<span x-html="$icon('shopping-cart', 'w-5 h-5')"></span>
|
||||
<span x-html="$icon('heart', 'w-6 h-6 text-red-500')"></span>
|
||||
```
|
||||
|
||||
### API Client
|
||||
Shared API wrapper for authenticated requests:
|
||||
```javascript
|
||||
const data = await apiClient.get('/endpoint');
|
||||
await apiClient.post('/endpoint', { data });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This template provides a complete, theme-aware pattern for building shop pages with consistent structure, vendor branding, cart integration, and excellent user experience across all devices.
|
||||
Reference in New Issue
Block a user