diff --git a/TEST_LANDING_PAGES.md b/TEST_LANDING_PAGES.md new file mode 100644 index 00000000..0a50fe46 --- /dev/null +++ b/TEST_LANDING_PAGES.md @@ -0,0 +1,209 @@ +# ๐Ÿงช Landing Pages Test Guide + +## โœ… Setup Complete! + +Landing pages have been created for three vendors with different templates. + +## ๐Ÿ“ Test URLs + +### 1. WizaMart - Modern Template +**Landing Page:** +- http://localhost:8000/vendors/wizamart/ + +**Shop Page:** +- http://localhost:8000/vendors/wizamart/shop/ + +**What to expect:** +- Full-screen hero section with animations +- "Why Choose Us" feature grid +- Gradient CTA section +- Modern, animated design + +--- + +### 2. Fashion Hub - Minimal Template +**Landing Page:** +- http://localhost:8000/vendors/fashionhub/ + +**Shop Page:** +- http://localhost:8000/vendors/fashionhub/shop/ + +**What to expect:** +- Ultra-simple centered design +- Single "Enter Shop" button +- Clean, minimal aesthetic +- No distractions + +--- + +### 3. The Book Store - Full Template +**Landing Page:** +- http://localhost:8000/vendors/bookstore/ + +**Shop Page:** +- http://localhost:8000/vendors/bookstore/shop/ + +**What to expect:** +- Split-screen hero layout +- Stats section (100+ Products, 24/7 Support, โญโญโญโญโญ) +- 4-feature grid +- Multiple CTA sections +- Most comprehensive layout + +--- + +## ๐Ÿงช Test Scenarios + +### Test 1: Landing Page Display +1. Visit each vendor's landing page URL +2. Verify the correct template is rendered +3. Check that vendor name, logo, and theme colors appear correctly + +### Test 2: Navigation +1. Click "Shop Now" / "Enter Shop" button on landing page +2. Should navigate to `/shop/` (product catalog) +3. Click logo or "Home" in navigation +4. Should return to landing page + +### Test 3: Theme Integration +1. Each vendor uses their theme colors +2. Logo should display correctly +3. Dark mode toggle should work + +### Test 4: Responsive Design +1. Resize browser window +2. All templates should be mobile-friendly +3. Navigation should collapse on mobile + +### Test 5: Fallback Behavior (No Landing Page) +To test fallback: +```python +# Delete a landing page +python -c " +from app.core.database import SessionLocal +from models.database.content_page import ContentPage + +db = SessionLocal() +page = db.query(ContentPage).filter(ContentPage.vendor_id == 1, ContentPage.slug == 'landing').first() +if page: + db.delete(page) + db.commit() +print('Landing page deleted') +db.close() +" +``` + +Then visit: http://localhost:8000/vendors/wizamart/ +- Should automatically redirect to: http://localhost:8000/vendors/wizamart/shop/ + +--- + +## ๐ŸŽจ Change Templates + +Want to try a different template? Run: + +```bash +python scripts/create_landing_page.py +``` + +Or programmatically: + +```python +from scripts.create_landing_page import create_landing_page + +# Change WizaMart to default template +create_landing_page('wizamart', template='default') + +# Change to minimal +create_landing_page('wizamart', template='minimal') + +# Change to full +create_landing_page('wizamart', template='full') + +# Change back to modern +create_landing_page('wizamart', template='modern') +``` + +--- + +## ๐Ÿ“Š Current Setup + +| Vendor | Subdomain | Template | Landing Page URL | +|--------|-----------|----------|------------------| +| WizaMart | wizamart | **modern** | http://localhost:8000/vendors/wizamart/ | +| Fashion Hub | fashionhub | **minimal** | http://localhost:8000/vendors/fashionhub/ | +| The Book Store | bookstore | **full** | http://localhost:8000/vendors/bookstore/ | + +--- + +## ๐Ÿ” Verify Database + +Check landing pages in database: + +```bash +sqlite3 letzshop.db "SELECT id, vendor_id, slug, title, template, is_published FROM content_pages WHERE slug='landing';" +``` + +Expected output: +``` +8|1|landing|Welcome to WizaMart|modern|1 +9|2|landing|Fashion Hub - Style & Elegance|minimal|1 +10|3|landing|The Book Store - Your Literary Haven|full|1 +``` + +--- + +## ๐Ÿ› Troubleshooting + +### Landing Page Not Showing +1. Check database: `SELECT * FROM content_pages WHERE slug='landing';` +2. Verify `is_published=1` +3. Check vendor ID matches +4. Look at server logs for errors + +### Wrong Template Rendering +1. Check `template` field in database +2. Verify template file exists: `app/templates/vendor/landing-{template}.html` +3. Restart server if needed + +### Theme Not Applied +1. Verify vendor has theme: `SELECT * FROM vendor_themes WHERE vendor_id=1;` +2. Check theme middleware is running (see server logs) +3. Inspect CSS variables in browser: `var(--color-primary)` + +### 404 Error +1. Ensure server is running: `python main.py` or `make run` +2. Check middleware is detecting vendor (see logs) +3. Verify route registration in startup logs + +--- + +## โœ… Success Checklist + +- [ ] WizaMart landing page loads (modern template) +- [ ] Fashion Hub landing page loads (minimal template) +- [ ] Book Store landing page loads (full template) +- [ ] "Shop Now" buttons work correctly +- [ ] Theme colors and logos display +- [ ] Mobile responsive design works +- [ ] Dark mode toggle works +- [ ] Navigation back to landing page works +- [ ] Fallback redirect to /shop/ works (when no landing page) + +--- + +## ๐ŸŽ‰ Next Steps + +Once testing is complete: + +1. **Create Landing Pages via Admin Panel** (future feature) +2. **Build Visual Page Builder** (Phase 2) +3. **Add More Templates** as needed +4. **Customize Content** for each vendor + +For now, use the Python script to manage landing pages: +```bash +python scripts/create_landing_page.py +``` + +Happy testing! ๐Ÿš€ diff --git a/alembic/versions/f68d8da5315a_add_template_field_to_content_pages_for_.py b/alembic/versions/f68d8da5315a_add_template_field_to_content_pages_for_.py new file mode 100644 index 00000000..2b40dda2 --- /dev/null +++ b/alembic/versions/f68d8da5315a_add_template_field_to_content_pages_for_.py @@ -0,0 +1,28 @@ +"""add template field to content pages for landing page designs + +Revision ID: f68d8da5315a +Revises: 72aa309d4007 +Create Date: 2025-11-22 23:51:40.694983 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f68d8da5315a' +down_revision: Union[str, None] = '72aa309d4007' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add template column to content_pages table + op.add_column('content_pages', sa.Column('template', sa.String(length=50), nullable=False, server_default='default')) + + +def downgrade() -> None: + # Remove template column from content_pages table + op.drop_column('content_pages', 'template') diff --git a/app/api/main.py b/app/api/main.py index 221a914d..a6997bd1 100644 --- a/app/api/main.py +++ b/app/api/main.py @@ -4,12 +4,12 @@ API router configuration for multi-tenant ecommerce platform. This module provides: - API version 1 route aggregation -- Route organization by user type (admin, vendor, public) +- Route organization by user type (admin, vendor, shop) - Proper route prefixing and tagging """ from fastapi import APIRouter -from app.api.v1 import admin, vendor, public, shop +from app.api.v1 import admin, vendor, shop api_router = APIRouter() @@ -35,17 +35,6 @@ api_router.include_router( tags=["vendor"] ) -# ============================================================================ -# PUBLIC/CUSTOMER ROUTES (Customer-facing) -# Prefix: /api/v1/public -# ============================================================================ - -api_router.include_router( - public.router, - prefix="/v1/public", - tags=["public"] -) - # ============================================================================ # SHOP ROUTES (Public shop frontend API) # Prefix: /api/v1/shop diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index 201a5815..8890d586 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -3,6 +3,6 @@ API Version 1 - All endpoints """ -from . import admin, vendor, public, shop +from . import admin, vendor, shop -__all__ = ["admin", "vendor", "public", "shop"] \ No newline at end of file +__all__ = ["admin", "vendor", "shop"] \ No newline at end of file diff --git a/app/api/v1/public/__init__.py b/app/api/v1/public/__init__.py deleted file mode 100644 index 09938782..00000000 --- a/app/api/v1/public/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# app/api/v1/public/__init__.py -""" -Public API endpoints (non-shop, non-authenticated). - -Note: Shop-related endpoints have been migrated to /api/v1/shop/* -This module now only contains truly public endpoints: -- Vendor lookup (by code, subdomain, ID) -""" - -from fastapi import APIRouter -from .vendors import vendors - -# Create public router -router = APIRouter() - -# Include vendor lookup endpoints (not shop-specific) -router.include_router(vendors.router, prefix="/vendors", tags=["public-vendors"]) - -__all__ = ["router"] diff --git a/app/api/v1/public/vendors/__init__.py b/app/api/v1/public/vendors/__init__.py deleted file mode 100644 index 7c1b54d0..00000000 --- a/app/api/v1/public/vendors/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# app/api/v1/public/vendors/__init__.py -"""Vendor-specific public API endpoints""" \ No newline at end of file diff --git a/app/api/v1/public/vendors/vendors.py b/app/api/v1/public/vendors/vendors.py deleted file mode 100644 index 88242760..00000000 --- a/app/api/v1/public/vendors/vendors.py +++ /dev/null @@ -1,128 +0,0 @@ -# app/api/v1/public/vendors/vendors.py -""" -Public vendor information endpoints. - -Provides public-facing vendor lookup and information retrieval. -""" - -import logging -from fastapi import APIRouter, Depends, HTTPException, Path -from sqlalchemy.orm import Session - -from app.core.database import get_db -from models.database.vendor import Vendor - -router = APIRouter() -logger = logging.getLogger(__name__) - - -@router.get("/by-code/{vendor_code}") -def get_vendor_by_code( - vendor_code: str = Path(..., description="Vendor code (e.g., TECHSTORE233)"), - db: Session = Depends(get_db) -): - """ - Get public vendor information by vendor code. - - This endpoint is used by: - - Frontend vendor login page to validate vendor existence - - Customer shop to display vendor information - - Returns basic vendor information (no sensitive data). - """ - vendor = db.query(Vendor).filter( - Vendor.vendor_code == vendor_code.upper(), - Vendor.is_active == True - ).first() - - if not vendor: - logger.warning(f"Vendor lookup failed for code: {vendor_code}") - raise HTTPException( - status_code=404, - detail=f"Vendor '{vendor_code}' not found or inactive" - ) - - logger.info(f"Vendor lookup successful: {vendor.vendor_code}") - - # Return public vendor information (no sensitive data) - return { - "id": vendor.id, - "vendor_code": vendor.vendor_code, - "subdomain": vendor.subdomain, - "name": vendor.name, - "description": vendor.description, - "website": vendor.website, - "is_active": vendor.is_active, - "is_verified": vendor.is_verified - } - - -@router.get("/by-subdomain/{subdomain}") -def get_vendor_by_subdomain( - subdomain: str = Path(..., description="Vendor subdomain (e.g., techstore233)"), - db: Session = Depends(get_db) -): - """ - Get public vendor information by subdomain. - - Used for subdomain-based vendor detection in production environments. - Example: techstore233.platform.com -> subdomain is "techstore233" - """ - vendor = db.query(Vendor).filter( - Vendor.subdomain == subdomain.lower(), - Vendor.is_active == True - ).first() - - if not vendor: - logger.warning(f"Vendor lookup failed for subdomain: {subdomain}") - raise HTTPException( - status_code=404, - detail=f"Vendor with subdomain '{subdomain}' not found or inactive" - ) - - logger.info(f"Vendor lookup by subdomain successful: {vendor.vendor_code}") - - return { - "id": vendor.id, - "vendor_code": vendor.vendor_code, - "subdomain": vendor.subdomain, - "name": vendor.name, - "description": vendor.description, - "website": vendor.website, - "is_active": vendor.is_active, - "is_verified": vendor.is_verified - } - - -@router.get("/{vendor_id}/info") -def get_vendor_info( - vendor_id: int = Path(..., description="Vendor ID"), - db: Session = Depends(get_db) -): - """ - Get public vendor information by ID. - - Used when vendor_id is already known (e.g., from URL parameters). - """ - vendor = db.query(Vendor).filter( - Vendor.id == vendor_id, - Vendor.is_active == True - ).first() - - if not vendor: - logger.warning(f"Vendor lookup failed for ID: {vendor_id}") - raise HTTPException( - status_code=404, - detail=f"Vendor with ID {vendor_id} not found or inactive" - ) - - return { - "id": vendor.id, - "vendor_code": vendor.vendor_code, - "subdomain": vendor.subdomain, - "name": vendor.name, - "description": vendor.description, - "website": vendor.website, - "is_active": vendor.is_active, - "is_verified": vendor.is_verified - } diff --git a/app/routes/shop_pages.py b/app/routes/shop_pages.py index 63b94ad2..513d3fa2 100644 --- a/app/routes/shop_pages.py +++ b/app/routes/shop_pages.py @@ -13,21 +13,21 @@ AUTHENTICATION: * Authorization header - for API calls - Customers CANNOT access admin or vendor routes -Routes: -- GET /shop/ โ†’ Shop homepage / product catalog -- GET /shop/products โ†’ Product catalog -- GET /shop/products/{id} โ†’ Product detail page -- GET /shop/categories/{slug} โ†’ Category products -- GET /shop/cart โ†’ Shopping cart -- GET /shop/checkout โ†’ Checkout process -- GET /shop/account/register โ†’ Customer registration -- GET /shop/account/login โ†’ Customer login -- GET /shop/account/dashboard โ†’ Customer dashboard (auth required) -- GET /shop/account/orders โ†’ Order history (auth required) -- GET /shop/account/orders/{id} โ†’ Order detail (auth required) -- GET /shop/account/profile โ†’ Customer profile (auth required) -- GET /shop/account/addresses โ†’ Address management (auth required) -- GET /shop/{slug} โ†’ Dynamic content pages (CMS): /about, /faq, /contact, etc. +Routes (all mounted at /shop/* or /vendors/{code}/shop/* prefix): +- GET / โ†’ Shop homepage / product catalog +- GET /products โ†’ Product catalog +- GET /products/{id} โ†’ Product detail page +- GET /categories/{slug} โ†’ Category products +- GET /cart โ†’ Shopping cart +- GET /checkout โ†’ Checkout process +- GET /account/register โ†’ Customer registration +- GET /account/login โ†’ Customer login +- GET /account/dashboard โ†’ Customer dashboard (auth required) +- GET /account/orders โ†’ Order history (auth required) +- GET /account/orders/{id} โ†’ Order detail (auth required) +- GET /account/profile โ†’ Customer profile (auth required) +- GET /account/addresses โ†’ Address management (auth required) +- GET /{slug} โ†’ Dynamic content pages (CMS): /about, /faq, /contact, etc. """ import logging @@ -188,7 +188,7 @@ async def shop_products_page(request: Request, db: Session = Depends(get_db)): ) -@router.get("/shop/products/{product_id}", response_class=HTMLResponse, include_in_schema=False) +@router.get("/products/{product_id}", response_class=HTMLResponse, include_in_schema=False) async def shop_product_detail_page( request: Request, product_id: int = Path(..., description="Product ID") @@ -212,7 +212,7 @@ async def shop_product_detail_page( ) -@router.get("/shop/categories/{category_slug}", response_class=HTMLResponse, include_in_schema=False) +@router.get("/categories/{category_slug}", response_class=HTMLResponse, include_in_schema=False) async def shop_category_page( request: Request, category_slug: str = Path(..., description="Category slug") @@ -236,7 +236,7 @@ async def shop_category_page( ) -@router.get("/shop/cart", response_class=HTMLResponse, include_in_schema=False) +@router.get("/cart", response_class=HTMLResponse, include_in_schema=False) async def shop_cart_page(request: Request): """ Render shopping cart page. @@ -257,7 +257,7 @@ async def shop_cart_page(request: Request): ) -@router.get("/shop/checkout", response_class=HTMLResponse, include_in_schema=False) +@router.get("/checkout", response_class=HTMLResponse, include_in_schema=False) async def shop_checkout_page(request: Request): """ Render checkout page. @@ -278,7 +278,7 @@ async def shop_checkout_page(request: Request): ) -@router.get("/shop/search", response_class=HTMLResponse, include_in_schema=False) +@router.get("/search", response_class=HTMLResponse, include_in_schema=False) async def shop_search_page(request: Request): """ Render search results page. @@ -303,7 +303,7 @@ async def shop_search_page(request: Request): # CUSTOMER ACCOUNT - PUBLIC ROUTES (No Authentication) # ============================================================================ -@router.get("/shop/account/register", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/register", response_class=HTMLResponse, include_in_schema=False) async def shop_register_page(request: Request): """ Render customer registration page. @@ -324,7 +324,7 @@ async def shop_register_page(request: Request): ) -@router.get("/shop/account/login", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/login", response_class=HTMLResponse, include_in_schema=False) async def shop_login_page(request: Request): """ Render customer login page. @@ -345,7 +345,7 @@ async def shop_login_page(request: Request): ) -@router.get("/shop/account/forgot-password", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/forgot-password", response_class=HTMLResponse, include_in_schema=False) async def shop_forgot_password_page(request: Request): """ Render forgot password page. @@ -370,8 +370,8 @@ async def shop_forgot_password_page(request: Request): # CUSTOMER ACCOUNT - AUTHENTICATED ROUTES # ============================================================================ -@router.get("/shop/account/", response_class=RedirectResponse, include_in_schema=False) -async def shop_account_root(): +@router.get("/account/", response_class=RedirectResponse, include_in_schema=False) +async def shop_account_root(request: Request): """ Redirect /shop/account/ to dashboard. """ @@ -384,10 +384,20 @@ async def shop_account_root(): } ) - return RedirectResponse(url="/shop/account/dashboard", status_code=302) + # Get base_url from context for proper redirect + vendor = getattr(request.state, 'vendor', None) + vendor_context = getattr(request.state, 'vendor_context', None) + access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown' + + base_url = "/" + if access_method == "path" and vendor: + full_prefix = vendor_context.get('full_prefix', '/vendor/') if vendor_context else '/vendor/' + base_url = f"{full_prefix}{vendor.subdomain}/" + + return RedirectResponse(url=f"{base_url}shop/account/dashboard", status_code=302) -@router.get("/shop/account/dashboard", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/dashboard", response_class=HTMLResponse, include_in_schema=False) async def shop_account_dashboard_page( request: Request, current_user: User = Depends(get_current_customer_from_cookie_or_header), @@ -413,7 +423,7 @@ async def shop_account_dashboard_page( ) -@router.get("/shop/account/orders", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/orders", response_class=HTMLResponse, include_in_schema=False) async def shop_orders_page( request: Request, current_user: User = Depends(get_current_customer_from_cookie_or_header), @@ -439,7 +449,7 @@ async def shop_orders_page( ) -@router.get("/shop/account/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False) async def shop_order_detail_page( request: Request, order_id: int = Path(..., description="Order ID"), @@ -466,7 +476,7 @@ async def shop_order_detail_page( ) -@router.get("/shop/account/profile", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/profile", response_class=HTMLResponse, include_in_schema=False) async def shop_profile_page( request: Request, current_user: User = Depends(get_current_customer_from_cookie_or_header), @@ -492,7 +502,7 @@ async def shop_profile_page( ) -@router.get("/shop/account/addresses", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/addresses", response_class=HTMLResponse, include_in_schema=False) async def shop_addresses_page( request: Request, current_user: User = Depends(get_current_customer_from_cookie_or_header), @@ -518,7 +528,7 @@ async def shop_addresses_page( ) -@router.get("/shop/account/wishlist", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/wishlist", response_class=HTMLResponse, include_in_schema=False) async def shop_wishlist_page( request: Request, current_user: User = Depends(get_current_customer_from_cookie_or_header), @@ -544,7 +554,7 @@ async def shop_wishlist_page( ) -@router.get("/shop/account/settings", response_class=HTMLResponse, include_in_schema=False) +@router.get("/account/settings", response_class=HTMLResponse, include_in_schema=False) async def shop_settings_page( request: Request, current_user: User = Depends(get_current_customer_from_cookie_or_header), diff --git a/app/templates/shop/account/login.html b/app/templates/shop/account/login.html index 360d110d..2b050525 100644 --- a/app/templates/shop/account/login.html +++ b/app/templates/shop/account/login.html @@ -94,7 +94,7 @@ > - + Forgot password? @@ -113,12 +113,12 @@ diff --git a/app/templates/shop/account/register.html b/app/templates/shop/account/register.html index 58aca8dc..955b31a2 100644 --- a/app/templates/shop/account/register.html +++ b/app/templates/shop/account/register.html @@ -175,7 +175,7 @@ diff --git a/app/templates/shop/base.html b/app/templates/shop/base.html index d3ef7a4f..0ec817db 100644 --- a/app/templates/shop/base.html +++ b/app/templates/shop/base.html @@ -57,7 +57,7 @@ {# Vendor Logo #}
- + {% if theme.branding.logo %} {# Show light logo in light mode, dark logo in dark mode #} Home - + Products - + About - + Contact @@ -106,7 +106,7 @@ {# Cart #} - + @@ -132,7 +132,7 @@ {# Account #} - + @@ -207,9 +207,9 @@

Quick Links

@@ -220,7 +220,7 @@

Information

@@ -230,18 +230,18 @@

Quick Links

Information

{% endif %} diff --git a/app/templates/shop/cart.html b/app/templates/shop/cart.html index abd2de1f..a1c814b8 100644 --- a/app/templates/shop/cart.html +++ b/app/templates/shop/cart.html @@ -19,7 +19,7 @@

๐Ÿ›’ Shopping Cart

- โ† Continue Shopping + โ† Continue Shopping
@@ -35,7 +35,7 @@
๐Ÿ›’

Your cart is empty

Add some products to get started!

- Browse Products + Browse Products @@ -135,7 +135,7 @@ Proceed to Checkout - + Continue Shopping diff --git a/app/templates/shop/errors/404.html b/app/templates/shop/errors/404.html index 092fce0a..d4ac4f5d 100644 --- a/app/templates/shop/errors/404.html +++ b/app/templates/shop/errors/404.html @@ -17,12 +17,12 @@
- Continue Shopping - View All Products + Continue Shopping + View All Products
{% if vendor %} diff --git a/app/templates/shop/home.html b/app/templates/shop/home.html index b7041226..b3d9f022 100644 --- a/app/templates/shop/home.html +++ b/app/templates/shop/home.html @@ -24,7 +24,7 @@ {{ vendor.description }}

{% endif %} - + Shop Now @@ -61,7 +61,7 @@

Featured Products

- + View All โ†’ diff --git a/app/templates/shop/product.html b/app/templates/shop/product.html index 58e7922d..40a50e8a 100644 --- a/app/templates/shop/product.html +++ b/app/templates/shop/product.html @@ -17,11 +17,11 @@
- โ† Back to Products + โ† Back to Products

{{ vendor.name }}

- + ๐Ÿ›’ Cart ()
diff --git a/app/templates/vendor/landing-default.html b/app/templates/vendor/landing-default.html new file mode 100644 index 00000000..9db23806 --- /dev/null +++ b/app/templates/vendor/landing-default.html @@ -0,0 +1,127 @@ +{# app/templates/vendor/landing-default.html #} +{# Default/Minimal Landing Page Template #} +{% extends "shop/base.html" %} + +{% block title %}{{ vendor.name }}{% endblock %} +{% block meta_description %}{{ page.meta_description or vendor.description or vendor.name }}{% endblock %} + +{% block content %} +
+ + {# Hero Section - Simple and Clean #} +
+
+
+ {# Logo #} + {% if theme.branding.logo %} +
+ {{ vendor.name }} +
+ {% endif %} + + {# Title #} +

+ {{ page.title or vendor.name }} +

+ + {# Tagline #} + {% if vendor.tagline %} +

+ {{ vendor.tagline }} +

+ {% endif %} + + {# CTA Button #} +
+ + Browse Our Shop + + + + + {% if page.content %} + + Learn More + + {% endif %} +
+
+
+
+ + {# Content Section (if provided) #} + {% if page.content %} +
+
+
+ {{ page.content | safe }} +
+
+
+ {% endif %} + + {# Quick Links Section #} +
+ +
+ +
+{% endblock %} diff --git a/app/templates/vendor/landing-full.html b/app/templates/vendor/landing-full.html new file mode 100644 index 00000000..c4e1ba2f --- /dev/null +++ b/app/templates/vendor/landing-full.html @@ -0,0 +1,269 @@ +{# app/templates/vendor/landing-full.html #} +{# Full Landing Page Template - Maximum Features #} +{% extends "shop/base.html" %} + +{% block title %}{{ vendor.name }}{% endblock %} +{% block meta_description %}{{ page.meta_description or vendor.description or vendor.name }}{% endblock %} + +{# Alpine.js component #} +{% block alpine_data %}shopLayoutData(){% endblock %} + +{% block content %} +
+ + {# Hero Section - Split Design #} +
+
+
+ {# Left - Content #} +
+ {% if theme.branding.logo %} +
+ {{ vendor.name }} +
+ {% endif %} + +

+ {{ page.title or vendor.name }} +

+ + {% if vendor.tagline %} +

+ {{ vendor.tagline }} +

+ {% endif %} + + {% if vendor.description %} +

+ {{ vendor.description }} +

+ {% endif %} + + + + {# Stats/Badges #} +
+
+
100+
+
Products
+
+
+
24/7
+
Support
+
+
+
โญโญโญโญโญ
+
Rated
+
+
+
+ + {# Right - Visual #} + +
+
+
+ + {# Features Grid #} +
+
+
+

+ What We Offer +

+

+ Everything you need for an exceptional shopping experience +

+
+ +
+
+
+ + + +
+

+ Premium Quality +

+

+ Top-tier products carefully selected for you +

+
+ +
+
+ + + +
+

+ Fast Shipping +

+

+ Quick delivery right to your door +

+
+ +
+
+ + + +
+

+ Best Value +

+

+ Competitive prices and great deals +

+
+ +
+
+ + + +
+

+ 24/7 Support +

+

+ Always here to help you +

+
+
+
+
+ + {# About Section (with content) #} + {% if page.content %} +
+
+
+ {{ page.content | safe }} +
+
+
+ {% endif %} + + {# Quick Navigation #} +
+ +
+ + {# Final CTA #} +
+
+

+ Ready to Start Shopping? +

+

+ Join thousands of satisfied customers today +

+ + View All Products + + + + +
+
+ +
+{% endblock %} diff --git a/app/templates/vendor/landing-minimal.html b/app/templates/vendor/landing-minimal.html new file mode 100644 index 00000000..1d05f912 --- /dev/null +++ b/app/templates/vendor/landing-minimal.html @@ -0,0 +1,67 @@ +{# app/templates/vendor/landing-minimal.html #} +{# Minimal Landing Page Template - Ultra Clean #} +{% extends "shop/base.html" %} + +{% block title %}{{ vendor.name }}{% endblock %} +{% block meta_description %}{{ page.meta_description or vendor.description or vendor.name }}{% endblock %} + +{% block content %} +
+
+ + {# Logo #} + {% if theme.branding.logo %} +
+ {{ vendor.name }} +
+ {% endif %} + + {# Title #} +

+ {{ page.title or vendor.name }} +

+ + {# Description/Content #} + {% if page.content %} +
+ {{ page.content | safe }} +
+ {% elif vendor.description %} +

+ {{ vendor.description }} +

+ {% endif %} + + {# Single CTA #} +
+ + Enter Shop + + + + +
+ + {# Optional Links Below #} + {% if header_pages or footer_pages %} +
+
+ + Products + + {% for page in (header_pages or footer_pages)[:4] %} + + {{ page.title }} + + {% endfor %} +
+
+ {% endif %} + +
+
+{% endblock %} diff --git a/app/templates/vendor/landing-modern.html b/app/templates/vendor/landing-modern.html new file mode 100644 index 00000000..1258f363 --- /dev/null +++ b/app/templates/vendor/landing-modern.html @@ -0,0 +1,205 @@ +{# app/templates/vendor/landing-modern.html #} +{# Modern Landing Page Template - Feature Rich #} +{% extends "shop/base.html" %} + +{% block title %}{{ vendor.name }}{% endblock %} +{% block meta_description %}{{ page.meta_description or vendor.description or vendor.name }}{% endblock %} + +{# Alpine.js component #} +{% block alpine_data %}shopLayoutData(){% endblock %} + +{% block content %} +
+ + {# Hero Section - Full Width with Overlay #} +
+
+ +
+ {# Logo #} + {% if theme.branding.logo %} +
+ {{ vendor.name }} +
+ {% endif %} + + {# Main Heading #} +

+ {{ page.title or vendor.name }} +

+ + {# Tagline #} + {% if vendor.tagline %} +

+ {{ vendor.tagline }} +

+ {% endif %} + + {# CTAs #} + + + {# Scroll Indicator #} +
+ + + +
+
+
+ + {# Features Section #} +
+
+
+

+ Why Choose Us +

+

+ {% if vendor.description %}{{ vendor.description }}{% else %}Experience excellence in every purchase{% endif %} +

+
+ +
+ {# Feature 1 #} +
+
+ + + +
+

+ Quality Products +

+

+ Carefully curated selection of premium items +

+
+ + {# Feature 2 #} +
+
+ + + +
+

+ Fast Delivery +

+

+ Quick and reliable shipping to your doorstep +

+
+ + {# Feature 3 #} +
+
+ + + +
+

+ Best Prices +

+

+ Competitive pricing with great value +

+
+
+
+
+ + {# Content Section (if provided) #} + {% if page.content %} +
+
+
+ {{ page.content | safe }} +
+
+
+ {% endif %} + + {# CTA Section #} +
+
+

+ Ready to Get Started? +

+

+ Explore our collection and find what you're looking for +

+ + Browse Products + + + + +
+
+ +
+ + +{% endblock %} diff --git a/docs/features/vendor-landing-pages.md b/docs/features/vendor-landing-pages.md new file mode 100644 index 00000000..89fb6293 --- /dev/null +++ b/docs/features/vendor-landing-pages.md @@ -0,0 +1,313 @@ +# Vendor Landing Pages + +Complete guide to creating custom landing pages for vendor storefronts. + +## Overview + +Each vendor can have a custom landing page at their root URL with different design templates. This landing page serves as the vendor's homepage, separate from the e-commerce shop section. + +### URL Structure + +``` +Root Landing Page: +- Custom Domain: https://customdomain.com/ โ†’ Landing Page +- Subdomain: https://wizamart.platform.com/ โ†’ Landing Page +- Path-based: http://localhost:8000/vendors/wizamart/ โ†’ Landing Page + +E-commerce Shop: +- Custom Domain: https://customdomain.com/shop/ โ†’ Shop Homepage +- Subdomain: https://wizamart.platform.com/shop/ โ†’ Shop Homepage +- Path-based: http://localhost:8000/vendors/wizamart/shop/ โ†’ Shop Homepage +``` + +## Features + +โœ… **Multiple Templates**: Choose from 4 different landing page designs +โœ… **CMS-Powered**: Content managed through ContentPage model +โœ… **Per-Vendor Customization**: Each vendor can have unique design +โœ… **Auto-Fallback**: Redirects to shop if no landing page exists +โœ… **Theme-Aware**: Uses vendor's theme colors and branding + +## Available Templates + +### 1. **Default** (`landing-default.html`) +- Clean and professional +- Hero section with logo and CTA +- Optional content section +- Quick links grid (3 cards) +- **Best for**: General purpose, simple storefronts + +### 2. **Minimal** (`landing-minimal.html`) +- Ultra-simple centered design +- Single page, no scrolling +- One primary CTA +- Minimal navigation +- **Best for**: Single-product vendors, portfolio sites + +### 3. **Modern** (`landing-modern.html`) +- Full-screen hero with animations +- Feature showcase section +- Gradient backgrounds +- Multiple CTAs +- **Best for**: Tech products, modern brands + +### 4. **Full** (`landing-full.html`) +- Split-screen hero layout +- Stats/badges section +- 4-feature grid +- Multiple content sections +- Final CTA section +- **Best for**: Established brands, full product catalogs + +## How to Create a Landing Page + +### Step 1: Create ContentPage in Database + +```python +# Using Python/SQL +from models.database.content_page import ContentPage + +landing_page = ContentPage( + vendor_id=1, # Your vendor ID + slug="landing", # Must be "landing" or "home" + title="Welcome to Our Store", + content="

Your custom HTML content here...

", + template="modern", # Choose: default, minimal, modern, full + is_published=True, + show_in_footer=False, + show_in_header=False +) +db.add(landing_page) +db.commit() +``` + +### Step 2: Choose Template + +Set the `template` field to one of: +- `"default"` - Clean professional layout +- `"minimal"` - Ultra-simple centered design +- `"modern"` - Full-screen with animations +- `"full"` - Maximum features and sections + +### Step 3: Add Content (Optional) + +The `content` field supports HTML. This appears in a dedicated section on all templates: + +```html +

About Our Store

+

We've been serving customers since 2020...

+ +``` + +### Step 4: Publish + +Set `is_published=True` to make the landing page live. + +## Fallback Behavior + +If no landing page exists: +1. System checks for `slug="landing"` +2. If not found, checks for `slug="home"` +3. If neither exists, **redirects to `/shop/`** + +This ensures vendors always have a working homepage even without a landing page. + +## Template Variables + +All templates have access to: + +### From Request Context +```python +{ + "vendor": Vendor object, + "theme": Theme object with colors/branding, + "base_url": "/" or "/vendors/{code}/", + "page": ContentPage object, + "header_pages": List of header navigation pages, + "footer_pages": List of footer navigation pages +} +``` + +### Vendor Properties +```jinja2 +{{ vendor.name }} +{{ vendor.tagline }} +{{ vendor.description }} +{{ vendor.website }} +``` + +### Theme Properties +```jinja2 +{{ theme.branding.logo }} +{{ theme.branding.logo_dark }} +{{ theme.colors.primary }} +{{ theme.colors.accent }} +``` + +### Page Properties +```jinja2 +{{ page.title }} +{{ page.content | safe }} +{{ page.meta_description }} +{{ page.template }} +``` + +## Migration Applied + +Database migration `f68d8da5315a` adds `template` field to `content_pages` table: + +```sql +ALTER TABLE content_pages +ADD COLUMN template VARCHAR(50) NOT NULL DEFAULT 'default'; +``` + +## API Endpoints + +### Get Landing Page +```http +GET /api/v1/shop/content-pages/landing +Referer: https://wizamart.platform.com/ + +Response: +{ + "id": 1, + "slug": "landing", + "title": "Welcome to WizaMart", + "content": "

...

", + "template": "modern", + "is_published": true +} +``` + +## Testing + +### Test Landing Page +1. Create a landing page via database or admin panel +2. Access vendor root URL: + - Path-based: `http://localhost:8000/vendors/wizamart/` + - Subdomain: `https://wizamart.platform.com/` +3. Should see your custom landing page + +### Test Fallback +1. Delete or unpublish landing page +2. Access vendor root URL +3. Should redirect to `/shop/` + +## Customization Guide + +### Adding a New Template + +1. Create new template file: + ``` + app/templates/vendor/landing-{name}.html + ``` + +2. Extend shop base: + ```jinja2 + {% extends "shop/base.html" %} + ``` + +3. Use template variables as needed + +4. Update `page.template` to `{name}` in database + +### Modifying Existing Templates + +Templates are in: +``` +app/templates/vendor/ +โ”œโ”€โ”€ landing-default.html (Clean professional) +โ”œโ”€โ”€ landing-minimal.html (Ultra-simple) +โ”œโ”€โ”€ landing-modern.html (Full-screen hero) +โ””โ”€โ”€ landing-full.html (Maximum features) +``` + +Edit directly and changes apply immediately (no rebuild needed). + +## Best Practices + +1. **Choose Template Based on Content**: + - Minimal: Little content, single CTA + - Default: Moderate content, professional + - Modern: Tech/modern brands, animated + - Full: Lots of content, established brand + +2. **Keep Content Concise**: Landing pages should be scannable + +3. **Strong CTAs**: Always link to `/shop/` for e-commerce + +4. **Use Theme Colors**: Templates automatically use vendor theme + +5. **Test Responsiveness**: All templates are mobile-friendly + +6. **SEO**: Fill in `meta_description` for better search results + +## Examples + +### Example 1: Simple Store +```python +ContentPage( + vendor_id=1, + slug="landing", + title="Welcome to TechStore", + content="

Your one-stop shop for electronics

", + template="default", + is_published=True +) +``` + +### Example 2: Portfolio Site +```python +ContentPage( + vendor_id=2, + slug="landing", + title="John's Artwork", + content="

Handcrafted pieces since 2015

", + template="minimal", + is_published=True +) +``` + +### Example 3: Modern Brand +```python +ContentPage( + vendor_id=3, + slug="landing", + title="FutureWear - Style Redefined", + content="

Our Story

Innovation meets fashion...

", + template="modern", + is_published=True +) +``` + +## Troubleshooting + +### Landing Page Not Showing +- Check `is_published=True` +- Verify `slug="landing"` or `slug="home"` +- Check vendor ID matches +- Verify template field is valid + +### Wrong Template Rendering +- Check `template` field value +- Ensure template file exists at `app/templates/vendor/landing-{template}.html` +- Check for typos in template name + +### Theme Colors Not Applied +- Verify vendor has theme configured +- Check CSS variables in template: `var(--color-primary)` +- Inspect theme object in template context + +## Future Enhancements + +**Phase 2: Section Builder** (Future) +- Drag-and-drop section components +- Hero, Features, Testimonials, Gallery sections +- Per-section customization +- Visual page builder interface + +This will allow vendors to build completely custom layouts without template limitations. diff --git a/docs/frontend/shop/architecture.md b/docs/frontend/shop/architecture.md index ac204c8f..dc4ce5ab 100644 --- a/docs/frontend/shop/architecture.md +++ b/docs/frontend/shop/architecture.md @@ -108,7 +108,21 @@ Layer 1: ROUTES (FastAPI) Purpose: Vendor Detection + Template Rendering Location: app/routes/shop_pages.py -Example: +โš ๏ธ ROUTE REGISTRATION (main.py): +The shop router is mounted at TWO prefixes to support both access methods: + + # main.py + app.include_router(shop_pages.router, prefix="/shop", ...) # Domain/subdomain + app.include_router(shop_pages.router, prefix="/vendors/{vendor_code}/shop", ...) # Path-based + +This means routes defined WITHOUT /shop prefix in shop_pages.py: + @router.get("/products") โ†’ /shop/products OR /vendors/{code}/shop/products + +โŒ COMMON MISTAKE: Don't add /shop prefix in route definitions! + @router.get("/shop/products") โŒ WRONG - creates /shop/shop/products + @router.get("/products") โœ… CORRECT - creates /shop/products + +Example Route Handler: @router.get("/", response_class=HTMLResponse, include_in_schema=False) @router.get("/products", response_class=HTMLResponse, include_in_schema=False) async def shop_products_page(request: Request): @@ -164,31 +178,31 @@ Responsibilities: The shop frontend supports THREE access methods: 1. **Custom Domain** (Production) - URL: https://customdomain.com/products + URL: https://customdomain.com/shop/products - Vendor has their own domain - base_url = "/" - - Links: /products, /about, /contact + - Links: /shop/products, /shop/about, /shop/contact 2. **Subdomain** (Production) - URL: https://wizamart.letzshop.com/products + URL: https://wizamart.letzshop.com/shop/products - Vendor uses platform subdomain - base_url = "/" - - Links: /products, /about, /contact + - Links: /shop/products, /shop/about, /shop/contact 3. **Path-Based** (Development/Testing) - URL: http://localhost:8000/vendor/wizamart/products + URL: http://localhost:8000/vendors/wizamart/shop/products - Vendor accessed via path prefix - - base_url = "/vendor/wizamart/" - - Links: /vendor/wizamart/products, /vendor/wizamart/about + - base_url = "/vendors/wizamart/" + - Links: /vendors/wizamart/shop/products, /vendors/wizamart/shop/about -โš ๏ธ CRITICAL: All template links MUST use {{ base_url }} prefix +โš ๏ธ CRITICAL: All template links MUST use {{ base_url }}shop/ prefix Example: โŒ BAD: Products - โœ… GOOD: Products + โŒ BAD: Products + โœ… GOOD: Products - โŒ BAD: Contact - โœ… GOOD: Contact +Note: The router is mounted at /shop prefix in main.py, so all links need shop/ after base_url How It Works: 1. VendorContextMiddleware detects access method diff --git a/docs/frontend/shop/navigation-flow.md b/docs/frontend/shop/navigation-flow.md new file mode 100644 index 00000000..81d49d2f --- /dev/null +++ b/docs/frontend/shop/navigation-flow.md @@ -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) #} +{{ vendor.name }} +``` + +**Breadcrumb Home Link:** +```jinja2 +{# Points to vendor root (landing page) #} +Home +``` + +**Shop Links:** +```jinja2 +{# All shop pages include /shop/ prefix #} +Products +Cart +Checkout +``` + +**CMS Page Links:** +```jinja2 +{# CMS pages are under /shop/ #} +About +Contact +{{ page.title }} +``` + +## 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 #} + + {{ vendor.name }} + + +{# Main Navigation #} + + +{# Actions #} +Cart +Account +``` + +### Footer Navigation (base.html) + +```jinja2 +{# Quick Links #} +Products + +{# CMS Pages (dynamic) #} +{% for page in footer_pages %} +{{ page.title }} +{% endfor %} +``` + +### Breadcrumbs (products.html, content-page.html) + +```jinja2 +{# Points to vendor root (landing page) #} +Home / 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 +Home {# Logo #} +Home {# Breadcrumb #} +``` + +### After Landing Pages + +Separation of concerns: +```jinja2 +Home {# Logo - still goes to shop #} +Home {# 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. diff --git a/main.py b/main.py index 6a981a30..7262407c 100644 --- a/main.py +++ b/main.py @@ -230,6 +230,48 @@ app.include_router( include_in_schema=False ) +# Add handler for /vendors/{vendor_code}/ root path +@app.get("/vendors/{vendor_code}/", response_class=HTMLResponse, include_in_schema=False) +async def vendor_root_path(vendor_code: str, request: Request, db: Session = Depends(get_db)): + """Handle vendor root path (e.g., /vendors/wizamart/)""" + # Vendor should already be in request.state from middleware + vendor = getattr(request.state, 'vendor', None) + + if not vendor: + raise HTTPException(status_code=404, detail=f"Vendor '{vendor_code}' not found") + + from app.services.content_page_service import content_page_service + from app.routes.shop_pages import get_shop_context + + # Try to find landing page + landing_page = content_page_service.get_page_for_vendor( + db, + slug="landing", + vendor_id=vendor.id, + include_unpublished=False + ) + + if not landing_page: + landing_page = content_page_service.get_page_for_vendor( + db, + slug="home", + vendor_id=vendor.id, + include_unpublished=False + ) + + if landing_page: + # Render landing page with selected template + template_name = landing_page.template or "default" + template_path = f"vendor/landing-{template_name}.html" + + return templates.TemplateResponse( + template_path, + get_shop_context(request, db=db, page=landing_page) + ) + else: + # No landing page - redirect to shop + return RedirectResponse(url=f"/vendors/{vendor_code}/shop/", status_code=302) + logger.info("=" * 80) # Log all registered routes @@ -247,10 +289,61 @@ logger.info("=" * 80) # ============================================================================ # Public Routes (no authentication required) -@app.get("/", include_in_schema=False) -async def root(): - """Redirect root to documentation""" - return RedirectResponse(url="/documentation") +@app.get("/", response_class=HTMLResponse, include_in_schema=False) +async def root(request: Request, db: Session = Depends(get_db)): + """ + Smart root handler: + - If vendor detected (domain/subdomain): Show vendor landing page or redirect to shop + - If no vendor (platform root): Redirect to documentation + """ + vendor = getattr(request.state, 'vendor', None) + + if vendor: + # Vendor context detected - serve landing page + from app.services.content_page_service import content_page_service + + # Try to find landing page (slug='landing' or 'home') + landing_page = content_page_service.get_page_for_vendor( + db, + slug="landing", + vendor_id=vendor.id, + include_unpublished=False + ) + + if not landing_page: + # Try 'home' slug as fallback + landing_page = content_page_service.get_page_for_vendor( + db, + slug="home", + vendor_id=vendor.id, + include_unpublished=False + ) + + if landing_page: + # Render landing page with selected template + from app.routes.shop_pages import get_shop_context + + template_name = landing_page.template or "default" + template_path = f"vendor/landing-{template_name}.html" + + return templates.TemplateResponse( + template_path, + get_shop_context(request, db=db, page=landing_page) + ) + else: + # No landing page - redirect to shop + vendor_context = getattr(request.state, 'vendor_context', None) + access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown' + + if access_method == "path": + full_prefix = vendor_context.get('full_prefix', '/vendor/') if vendor_context else '/vendor/' + return RedirectResponse(url=f"{full_prefix}{vendor.subdomain}/shop/", status_code=302) + else: + # Domain/subdomain + return RedirectResponse(url="/shop/", status_code=302) + else: + # No vendor - platform root + return RedirectResponse(url="/documentation") @app.get("/health") diff --git a/mkdocs.yml b/mkdocs.yml index a090636e..5d192e80 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,6 +32,9 @@ nav: - Middleware Stack: architecture/middleware.md - Request Flow: architecture/request-flow.md - Authentication & RBAC: architecture/auth-rbac.md + - API Consolidation: + - Proposal: architecture/API_CONSOLIDATION_PROPOSAL.md + - Migration Status: architecture/API_MIGRATION_STATUS.md - Diagrams: - Multi-Tenant Diagrams: architecture/diagrams/multitenant-diagrams.md - Vendor Domain Diagrams: architecture/diagrams/vendor-domain-diagrams.md @@ -46,6 +49,7 @@ nav: # ============================================ - API Documentation: - Overview: api/index.md + - Shop API Reference: api/shop-api-reference.md - Authentication: - Guide: api/authentication.md - Quick Reference: api/authentication-quick-reference.md diff --git a/models/database/content_page.py b/models/database/content_page.py index 3a74f549..687dfc9f 100644 --- a/models/database/content_page.py +++ b/models/database/content_page.py @@ -49,6 +49,11 @@ class ContentPage(Base): content = Column(Text, nullable=False) # HTML or Markdown content_format = Column(String(20), default="html") # html, markdown + # Template selection (for landing pages) + # Options: 'default', 'minimal', 'modern', 'full' + # Only used for landing pages (slug='landing' or 'home') + template = Column(String(50), default="default", nullable=False) + # SEO meta_description = Column(String(300), nullable=True) meta_keywords = Column(String(300), nullable=True) @@ -110,6 +115,7 @@ class ContentPage(Base): "title": self.title, "content": self.content, "content_format": self.content_format, + "template": self.template, "meta_description": self.meta_description, "meta_keywords": self.meta_keywords, "is_published": self.is_published, diff --git a/scripts/create_landing_page.py b/scripts/create_landing_page.py new file mode 100755 index 00000000..e5d463fc --- /dev/null +++ b/scripts/create_landing_page.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +""" +Create Landing Page for Vendor + +This script creates a landing page for a vendor with the specified template. +Usage: python scripts/create_landing_page.py +""" + +import sys +from pathlib import Path + +# Add project root to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from sqlalchemy.orm import Session +from app.core.database import SessionLocal +from models.database.vendor import Vendor +from models.database.content_page import ContentPage +from datetime import datetime, timezone + + +def create_landing_page( + vendor_subdomain: str, + template: str = "default", + title: str = None, + content: str = None +): + """ + Create a landing page for a vendor. + + Args: + vendor_subdomain: Vendor subdomain (e.g., 'wizamart') + template: Template to use (default, minimal, modern, full) + title: Page title (defaults to vendor name) + content: HTML content (optional) + """ + db: Session = SessionLocal() + + try: + # Find vendor + vendor = db.query(Vendor).filter( + Vendor.subdomain == vendor_subdomain + ).first() + + if not vendor: + print(f"โŒ Vendor '{vendor_subdomain}' not found!") + return False + + print(f"โœ… Found vendor: {vendor.name} (ID: {vendor.id})") + + # Check if landing page already exists + existing = db.query(ContentPage).filter( + ContentPage.vendor_id == vendor.id, + ContentPage.slug == "landing" + ).first() + + if existing: + print(f"โš ๏ธ Landing page already exists (ID: {existing.id})") + print(f" Current template: {existing.template}") + + # Update it + existing.template = template + existing.title = title or existing.title + if content: + existing.content = content + existing.is_published = True + existing.updated_at = datetime.now(timezone.utc) + + db.commit() + print(f"โœ… Updated landing page with template: {template}") + else: + # Create new landing page + landing_page = ContentPage( + vendor_id=vendor.id, + slug="landing", + title=title or f"Welcome to {vendor.name}", + content=content or f""" +

About {vendor.name}

+

{vendor.description or 'Your trusted shopping destination for quality products.'}

+ +

Why Choose Us?

+ + +

Our Story

+

We've been serving customers since 2020, providing exceptional products and service. + Our mission is to make online shopping easy, enjoyable, and reliable.

+ """, + content_format="html", + template=template, + meta_description=f"Shop at {vendor.name} for quality products and great service", + is_published=True, + published_at=datetime.now(timezone.utc), + show_in_footer=False, + show_in_header=False, + display_order=0 + ) + + db.add(landing_page) + db.commit() + db.refresh(landing_page) + + print(f"โœ… Created landing page (ID: {landing_page.id})") + print(f" Template: {template}") + print(f" Title: {landing_page.title}") + + # Print access URLs + print("\n๐Ÿ“ Access your landing page at:") + print(f" Path-based: http://localhost:8000/vendors/{vendor.subdomain}/") + print(f" Shop page: http://localhost:8000/vendors/{vendor.subdomain}/shop/") + + return True + + except Exception as e: + print(f"โŒ Error: {e}") + db.rollback() + return False + finally: + db.close() + + +def list_vendors(): + """List all vendors in the system.""" + db: Session = SessionLocal() + + try: + vendors = db.query(Vendor).filter(Vendor.is_active == True).all() + + if not vendors: + print("โŒ No active vendors found!") + return + + print("\n๐Ÿ“‹ Active Vendors:") + print("=" * 60) + for vendor in vendors: + print(f" โ€ข {vendor.name}") + print(f" Subdomain: {vendor.subdomain}") + print(f" Code: {vendor.vendor_code}") + + # Check if has landing page + landing = db.query(ContentPage).filter( + ContentPage.vendor_id == vendor.id, + ContentPage.slug == "landing" + ).first() + + if landing: + print(f" Landing Page: โœ… ({landing.template})") + else: + print(f" Landing Page: โŒ None") + print() + + finally: + db.close() + + +def show_templates(): + """Show available templates.""" + print("\n๐ŸŽจ Available Templates:") + print("=" * 60) + + templates = [ + ("default", "Clean professional layout with 3-column quick links"), + ("minimal", "Ultra-simple centered design with single CTA"), + ("modern", "Full-screen hero with animations and features"), + ("full", "Maximum features with split-screen hero and stats") + ] + + for name, desc in templates: + print(f" โ€ข {name:<10} - {desc}") + print() + + +if __name__ == "__main__": + print("\n" + "=" * 60) + print(" VENDOR LANDING PAGE CREATOR") + print("=" * 60) + + # List vendors + list_vendors() + + # Show templates + show_templates() + + # Interactive creation + print("๐Ÿ“ Create Landing Page") + print("-" * 60) + + vendor_subdomain = input("Enter vendor subdomain (e.g., wizamart): ").strip() + + if not vendor_subdomain: + print("โŒ Vendor subdomain is required!") + sys.exit(1) + + print("\nAvailable templates: default, minimal, modern, full") + template = input("Enter template (default): ").strip() or "default" + + if template not in ["default", "minimal", "modern", "full"]: + print(f"โš ๏ธ Invalid template '{template}', using 'default'") + template = "default" + + title = input("Enter page title (optional, press Enter to use default): ").strip() + + print("\n๐Ÿš€ Creating landing page...") + print("-" * 60) + + success = create_landing_page( + vendor_subdomain=vendor_subdomain, + template=template, + title=title if title else None + ) + + if success: + print("\nโœ… SUCCESS! Landing page is ready.") + print("\n๐Ÿ’ก Try different templates:") + print(f" python scripts/create_landing_page.py") + print(f" # Then choose: minimal, modern, or full") + else: + print("\nโŒ Failed to create landing page") + sys.exit(1)