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 #}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 @@+ {{ vendor.tagline }} +
+ {% endif %} + + {# CTA Button #} ++ Browse our complete catalog +
+ + + {% if header_pages %} + {% for page in header_pages[:2] %} + ++ {{ page.meta_description or 'Learn more' }} +
+ + {% endfor %} + {% else %} + ++ Learn about our story +
+ + + ++ Get in touch with us +
+ + {% endif %} ++ {{ vendor.tagline }} +
+ {% endif %} + + {% if vendor.description %} ++ {{ vendor.description }} +
+ {% endif %} + + + + {# Stats/Badges #} ++ Your Shopping Destination +
++ Everything you need for an exceptional shopping experience +
++ Top-tier products carefully selected for you +
++ Quick delivery right to your door +
++ Competitive prices and great deals +
++ Always here to help you +
++ Browse our complete collection +
++ {{ page.meta_description or 'Learn more about us' }} +
++ Learn about our story and mission +
++ Get in touch with our team +
++ Join thousands of satisfied customers today +
+ + View All Products ++ {{ vendor.description }} +
+ {% endif %} + + {# Single CTA #} + + + {# Optional Links Below #} + {% if header_pages or footer_pages %} ++ {% if vendor.description %}{{ vendor.description }}{% else %}Experience excellence in every purchase{% endif %} +
++ Carefully curated selection of premium items +
++ Quick and reliable shipping to your doorstep +
++ Competitive pricing with great value +
++ Explore our collection and find what you're looking for +
+ + Browse Products +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 +We've been serving customers since 2020...
+...
", + "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="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.description or 'Your trusted shopping destination for quality products.'}
+ +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)