feat: add platform homepage and content management system with improved UI

Implemented a comprehensive CMS for managing platform homepage and content pages:

- Platform homepage manager with template selection (default, minimal, modern)
- Content pages CRUD with platform defaults and vendor overrides
- Sidebar navigation for Content Management section
- Dedicated API endpoints for creating, updating, deleting pages
- Template support for customizable homepage layouts
- Header/footer navigation integration for content pages
- Comprehensive documentation for platform homepage setup
- Migration script for creating initial platform pages

UI improvements:
- Fixed action buttons styling in content pages table to match design system
- Added proper hover states, rounded corners, and better contrast
- Increased button size and padding for better usability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-28 07:17:30 +01:00
parent 9f8ad71d85
commit 83a6831b2e
20 changed files with 4177 additions and 0 deletions

View File

@@ -32,6 +32,7 @@ class ContentPageCreate(BaseModel):
title: str = Field(..., max_length=200, description="Page title") title: str = Field(..., max_length=200, description="Page title")
content: str = Field(..., description="HTML or Markdown content") content: str = Field(..., description="HTML or Markdown content")
content_format: str = Field(default="html", description="Content format: html or markdown") content_format: str = Field(default="html", description="Content format: html or markdown")
template: str = Field(default="default", max_length=50, description="Template name (default, minimal, modern)")
meta_description: Optional[str] = Field(None, max_length=300, description="SEO meta description") meta_description: Optional[str] = Field(None, max_length=300, description="SEO meta description")
meta_keywords: Optional[str] = Field(None, max_length=300, description="SEO keywords") meta_keywords: Optional[str] = Field(None, max_length=300, description="SEO keywords")
is_published: bool = Field(default=False, description="Publish immediately") is_published: bool = Field(default=False, description="Publish immediately")
@@ -46,6 +47,7 @@ class ContentPageUpdate(BaseModel):
title: Optional[str] = Field(None, max_length=200) title: Optional[str] = Field(None, max_length=200)
content: Optional[str] = None content: Optional[str] = None
content_format: Optional[str] = None content_format: Optional[str] = None
template: Optional[str] = Field(None, max_length=50)
meta_description: Optional[str] = Field(None, max_length=300) meta_description: Optional[str] = Field(None, max_length=300)
meta_keywords: Optional[str] = Field(None, max_length=300) meta_keywords: Optional[str] = Field(None, max_length=300)
is_published: Optional[bool] = None is_published: Optional[bool] = None
@@ -120,6 +122,7 @@ def create_platform_page(
content=page_data.content, content=page_data.content,
vendor_id=None, # Platform default vendor_id=None, # Platform default
content_format=page_data.content_format, content_format=page_data.content_format,
template=page_data.template,
meta_description=page_data.meta_description, meta_description=page_data.meta_description,
meta_keywords=page_data.meta_keywords, meta_keywords=page_data.meta_keywords,
is_published=page_data.is_published, is_published=page_data.is_published,
@@ -202,6 +205,7 @@ def update_page(
title=page_data.title, title=page_data.title,
content=page_data.content, content=page_data.content,
content_format=page_data.content_format, content_format=page_data.content_format,
template=page_data.template,
meta_description=page_data.meta_description, meta_description=page_data.meta_description,
meta_keywords=page_data.meta_keywords, meta_keywords=page_data.meta_keywords,
is_published=page_data.is_published, is_published=page_data.is_published,

View File

@@ -21,6 +21,10 @@ Routes:
- GET /users → User management page (auth required) - GET /users → User management page (auth required)
- GET /imports → Import history page (auth required) - GET /imports → Import history page (auth required)
- GET /settings → Settings page (auth required) - GET /settings → Settings page (auth required)
- GET /platform-homepage → Platform homepage manager (auth required)
- GET /content-pages → Content pages list (auth required)
- GET /content-pages/create → Create content page (auth required)
- GET /content-pages/{page_id}/edit → Edit content page (auth required)
""" """
from fastapi import APIRouter, Request, Depends, Path from fastapi import APIRouter, Request, Depends, Path
@@ -306,6 +310,89 @@ async def admin_settings_page(
) )
# ============================================================================
# CONTENT MANAGEMENT SYSTEM (CMS) ROUTES
# ============================================================================
@router.get("/platform-homepage", response_class=HTMLResponse, include_in_schema=False)
async def admin_platform_homepage_manager(
request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render platform homepage manager.
Allows editing the main platform homepage with template selection.
"""
return templates.TemplateResponse(
"admin/platform-homepage.html",
{
"request": request,
"user": current_user,
}
)
@router.get("/content-pages", response_class=HTMLResponse, include_in_schema=False)
async def admin_content_pages_list(
request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render content pages list.
Shows all platform defaults and vendor overrides with filtering.
"""
return templates.TemplateResponse(
"admin/content-pages.html",
{
"request": request,
"user": current_user,
}
)
@router.get("/content-pages/create", response_class=HTMLResponse, include_in_schema=False)
async def admin_content_page_create(
request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render create content page form.
Allows creating new platform defaults or vendor-specific pages.
"""
return templates.TemplateResponse(
"admin/content-page-edit.html",
{
"request": request,
"user": current_user,
"page_id": None, # Indicates this is a create operation
}
)
@router.get("/content-pages/{page_id}/edit", response_class=HTMLResponse, include_in_schema=False)
async def admin_content_page_edit(
request: Request,
page_id: int = Path(..., description="Content page ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render edit content page form.
Allows editing existing platform or vendor content pages.
"""
return templates.TemplateResponse(
"admin/content-page-edit.html",
{
"request": request,
"user": current_user,
"page_id": page_id,
}
)
# ============================================================================ # ============================================================================
# DEVELOPER TOOLS - COMPONENTS & TESTING # DEVELOPER TOOLS - COMPONENTS & TESTING
# ============================================================================ # ============================================================================

View File

@@ -176,6 +176,7 @@ class ContentPageService:
content: str, content: str,
vendor_id: Optional[int] = None, vendor_id: Optional[int] = None,
content_format: str = "html", content_format: str = "html",
template: str = "default",
meta_description: Optional[str] = None, meta_description: Optional[str] = None,
meta_keywords: Optional[str] = None, meta_keywords: Optional[str] = None,
is_published: bool = False, is_published: bool = False,
@@ -194,6 +195,7 @@ class ContentPageService:
content: HTML or Markdown content content: HTML or Markdown content
vendor_id: Vendor ID (None for platform default) vendor_id: Vendor ID (None for platform default)
content_format: "html" or "markdown" content_format: "html" or "markdown"
template: Template name for homepage/landing pages (default, minimal, modern, etc.)
meta_description: SEO description meta_description: SEO description
meta_keywords: SEO keywords meta_keywords: SEO keywords
is_published: Publish immediately is_published: Publish immediately
@@ -211,6 +213,7 @@ class ContentPageService:
title=title, title=title,
content=content, content=content,
content_format=content_format, content_format=content_format,
template=template,
meta_description=meta_description, meta_description=meta_description,
meta_keywords=meta_keywords, meta_keywords=meta_keywords,
is_published=is_published, is_published=is_published,
@@ -236,6 +239,7 @@ class ContentPageService:
title: Optional[str] = None, title: Optional[str] = None,
content: Optional[str] = None, content: Optional[str] = None,
content_format: Optional[str] = None, content_format: Optional[str] = None,
template: Optional[str] = None,
meta_description: Optional[str] = None, meta_description: Optional[str] = None,
meta_keywords: Optional[str] = None, meta_keywords: Optional[str] = None,
is_published: Optional[bool] = None, is_published: Optional[bool] = None,
@@ -253,6 +257,7 @@ class ContentPageService:
title: New title title: New title
content: New content content: New content
content_format: New format content_format: New format
template: New template name
meta_description: New SEO description meta_description: New SEO description
meta_keywords: New SEO keywords meta_keywords: New SEO keywords
is_published: New publish status is_published: New publish status
@@ -277,6 +282,8 @@ class ContentPageService:
page.content = content page.content = content
if content_format is not None: if content_format is not None:
page.content_format = content_format page.content_format = content_format
if template is not None:
page.template = template
if meta_description is not None: if meta_description is not None:
page.meta_description = meta_description page.meta_description = meta_description
if meta_keywords is not None: if meta_keywords is not None:

View File

@@ -0,0 +1,301 @@
{# app/templates/admin/content-page-edit.html #}
{% extends "admin/base.html" %}
{% block title %}{% if page_id %}Edit{% else %}Create{% endif %} Content Page{% endblock %}
{% block alpine_data %}contentPageEditor({{ page_id if page_id else 'null' }}){% endblock %}
{% block content %}
<!-- Page Header -->
<div class="flex items-center justify-between my-6">
<div>
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
<span x-text="pageId ? 'Edit Content Page' : 'Create Content Page'"></span>
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
<span x-show="!pageId">Create a new platform default or vendor-specific page</span>
<span x-show="pageId">Modify an existing content page</span>
</p>
</div>
<div class="flex gap-2">
<a
href="/admin/content-pages"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-200 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:border-gray-400 dark:hover:border-gray-500 focus:outline-none focus:shadow-outline-purple"
>
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
Back to List
</a>
<button
@click="savePage()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!saving" x-html="$icon('check', 'w-4 h-4 mr-2')"></span>
<span x-show="saving" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="saving ? 'Saving...' : (pageId ? 'Update Page' : 'Create Page')"></span>
</button>
</div>
</div>
<!-- Loading State -->
<div x-show="loading" class="text-center py-12">
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading page...</p>
</div>
<!-- Error State -->
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div>
<p class="font-semibold">Error</p>
<p class="text-sm" x-text="error"></p>
</div>
</div>
<!-- Success Message -->
<div x-show="successMessage" x-transition class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg flex items-start">
<span x-html="$icon('check-circle', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div>
<p class="font-semibold" x-text="successMessage"></p>
</div>
</div>
<!-- Main Form -->
<div x-show="!loading" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
<form @submit.prevent="savePage()">
<!-- Basic Information -->
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Basic Information
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Page Title -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Page Title <span class="text-red-500">*</span>
</label>
<input
type="text"
x-model="form.title"
required
maxlength="200"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
placeholder="About Us"
>
</div>
<!-- Slug -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Slug <span class="text-red-500">*</span>
</label>
<input
type="text"
x-model="form.slug"
required
maxlength="100"
pattern="[a-z0-9_-]+"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
placeholder="about"
>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
URL-safe identifier (lowercase, numbers, hyphens, underscores only)
</p>
</div>
<!-- Vendor ID (Platform vs Vendor-specific) -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Page Type
</label>
<select
x-model="form.vendor_id"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
>
<option :value="null">Platform Default</option>
<!-- TODO: Load vendors dynamically if needed -->
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Platform defaults are shown to all vendors
</p>
</div>
</div>
</div>
<!-- Content -->
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Page Content
</h3>
<!-- Content Format -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Content Format
</label>
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input type="radio" x-model="form.content_format" value="html" class="mr-2">
<span class="text-sm">HTML</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="radio" x-model="form.content_format" value="markdown" class="mr-2">
<span class="text-sm">Markdown</span>
</label>
</div>
</div>
<!-- Content Editor -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Content <span class="text-red-500">*</span>
</label>
<textarea
x-model="form.content"
required
rows="12"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700 font-mono text-sm"
placeholder="<h2>Your content here...</h2>"
></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
<span x-show="form.content_format === 'html'">Enter HTML content. Basic HTML tags are supported.</span>
<span x-show="form.content_format === 'markdown'">Enter Markdown content. Will be converted to HTML.</span>
</p>
</div>
</div>
<!-- SEO Settings -->
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
SEO & Metadata
</h3>
<div class="space-y-4">
<!-- Meta Description -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Meta Description
</label>
<textarea
x-model="form.meta_description"
rows="2"
maxlength="300"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
placeholder="A brief description for search engines"
></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
<span x-text="(form.meta_description || '').length"></span>/300 characters (150-160 recommended)
</p>
</div>
<!-- Meta Keywords -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Meta Keywords
</label>
<input
type="text"
x-model="form.meta_keywords"
maxlength="300"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
placeholder="keyword1, keyword2, keyword3"
>
</div>
</div>
</div>
<!-- Navigation & Display Settings -->
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Navigation & Display
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Display Order -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Display Order
</label>
<input
type="number"
x-model.number="form.display_order"
min="0"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Lower = first</p>
</div>
<!-- Show in Header -->
<div class="flex items-center">
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
x-model="form.show_in_header"
class="w-5 h-5 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-white">
Show in Header
</span>
</label>
</div>
<!-- Show in Footer -->
<div class="flex items-center">
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
x-model="form.show_in_footer"
class="w-5 h-5 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-white">
Show in Footer
</span>
</label>
</div>
</div>
</div>
<!-- Publishing Settings -->
<div class="p-6 bg-gray-50 dark:bg-gray-700/50">
<div class="flex items-center justify-between">
<div>
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
x-model="form.is_published"
class="w-5 h-5 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-white">
Published
</span>
</label>
<p class="ml-8 text-xs text-gray-500 dark:text-gray-400">
Make this page visible to the public
</p>
</div>
<div class="flex gap-2">
<a
href="/admin/content-pages"
class="px-6 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-200 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:border-gray-400 dark:hover:border-gray-500"
>
Cancel
</a>
<button
type="submit"
:disabled="saving"
class="px-6 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-text="saving ? 'Saving...' : (pageId ? 'Update Page' : 'Create Page')"></span>
</button>
</div>
</div>
</div>
</form>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/content-page-edit.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,200 @@
{# app/templates/admin/content-pages.html #}
{% extends "admin/base.html" %}
{% block title %}Content Pages{% endblock %}
{% block alpine_data %}contentPagesManager(){% endblock %}
{% block content %}
<!-- Page Header -->
<div class="flex items-center justify-between my-6">
<div>
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
Content Pages
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
Manage platform defaults and vendor-specific content pages
</p>
</div>
<a
href="/admin/content-pages/create"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple"
>
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
Create Page
</a>
</div>
<!-- Loading State -->
<div x-show="loading" class="text-center py-12">
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading pages...</p>
</div>
<!-- Error State -->
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div>
<p class="font-semibold">Error loading pages</p>
<p class="text-sm" x-text="error"></p>
</div>
</div>
<!-- Tabs and Filters -->
<div x-show="!loading" class="mb-6 bg-white dark:bg-gray-800 rounded-lg shadow-md p-4">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<!-- Tabs -->
<div class="flex space-x-2 border-b border-gray-200 dark:border-gray-700">
<button
@click="activeTab = 'all'"
class="px-4 py-2 text-sm font-medium transition-colors"
:class="activeTab === 'all' ? 'text-purple-600 border-b-2 border-purple-600' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'"
>
All Pages
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-gray-100 dark:bg-gray-700" x-text="allPages.length"></span>
</button>
<button
@click="activeTab = 'platform'"
class="px-4 py-2 text-sm font-medium transition-colors"
:class="activeTab === 'platform' ? 'text-purple-600 border-b-2 border-purple-600' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'"
>
Platform Defaults
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-gray-100 dark:bg-gray-700" x-text="platformPages.length"></span>
</button>
<button
@click="activeTab = 'vendor'"
class="px-4 py-2 text-sm font-medium transition-colors"
:class="activeTab === 'vendor' ? 'text-purple-600 border-b-2 border-purple-600' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'"
>
Vendor Overrides
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-gray-100 dark:bg-gray-700" x-text="vendorPages.length"></span>
</button>
</div>
<!-- Search -->
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
</span>
<input
type="text"
x-model="searchQuery"
placeholder="Search pages..."
class="pl-10 pr-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500"
>
</div>
</div>
</div>
<!-- Pages Table -->
<div x-show="!loading && filteredPages.length > 0" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full whitespace-nowrap">
<thead>
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 dark:text-gray-400">
<th class="px-4 py-3">Page</th>
<th class="px-4 py-3">Slug</th>
<th class="px-4 py-3">Type</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Navigation</th>
<th class="px-4 py-3">Updated</th>
<th class="px-4 py-3">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y dark:divide-gray-700">
<template x-for="page in filteredPages" :key="page.id">
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700/50">
<!-- Page Title -->
<td class="px-4 py-3">
<div>
<p class="font-semibold text-gray-900 dark:text-white" x-text="page.title"></p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-show="page.vendor_name">
Vendor: <span x-text="page.vendor_name"></span>
</p>
</div>
</td>
<!-- Slug -->
<td class="px-4 py-3 text-sm">
<code class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="'/' + page.slug"></code>
</td>
<!-- Type -->
<td class="px-4 py-3 text-sm">
<span
class="px-2 py-1 text-xs font-semibold rounded-full"
:class="page.is_platform_default ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'"
x-text="page.is_platform_default ? 'Platform' : 'Vendor'"
></span>
</td>
<!-- Status -->
<td class="px-4 py-3 text-sm">
<span
class="px-2 py-1 text-xs font-semibold rounded-full"
:class="page.is_published ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'"
x-text="page.is_published ? 'Published' : 'Draft'"
></span>
</td>
<!-- Navigation -->
<td class="px-4 py-3 text-xs">
<div class="flex gap-1">
<span x-show="page.show_in_header" class="px-1.5 py-0.5 bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200 rounded">Header</span>
<span x-show="page.show_in_footer" class="px-1.5 py-0.5 bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200 rounded">Footer</span>
<span x-show="!page.show_in_header && !page.show_in_footer" class="text-gray-400"></span>
</div>
</td>
<!-- Updated -->
<td class="px-4 py-3 text-xs" x-text="formatDate(page.updated_at)"></td>
<!-- Actions -->
<td class="px-4 py-3">
<div class="flex items-center space-x-2 text-sm">
<a
:href="`/admin/content-pages/${page.id}/edit`"
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Edit"
>
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
</a>
<button
@click="deletePage(page)"
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Delete"
>
<span x-html="$icon('trash', 'w-5 h-5')"></span>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Empty State -->
<div x-show="!loading && filteredPages.length === 0" class="text-center py-12 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<span x-html="$icon('document-text', 'inline w-16 h-16 text-gray-400 mb-4')"></span>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">No pages found</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4" x-show="searchQuery">
No pages match your search: "<span x-text="searchQuery"></span>"
</p>
<p class="text-gray-500 dark:text-gray-400 mb-4" x-show="!searchQuery && activeTab === 'vendor'">
No vendor-specific pages have been created yet.
</p>
<a
href="/admin/content-pages/create"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
>
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
Create First Page
</a>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/content-pages.js') }}"></script>
{% endblock %}

View File

@@ -54,6 +54,37 @@
</li> </li>
</ul> </ul>
<!-- Content Management Section -->
<div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" />
</div>
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
Content Management
</p>
<ul class="mt-3">
<!-- Platform Homepage -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'platform-homepage'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'platform-homepage' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/platform-homepage">
<span x-html="$icon('home')"></span>
<span class="ml-4">Platform Homepage</span>
</a>
</li>
<!-- Content Pages -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'content-pages'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'content-pages' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/content-pages">
<span x-html="$icon('document-text')"></span>
<span class="ml-4">Content Pages</span>
</a>
</li>
</ul>
<!-- Developer Tools Section --> <!-- Developer Tools Section -->
<div class="px-6 my-6"> <div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" /> <hr class="border-gray-200 dark:border-gray-700" />
@@ -188,6 +219,37 @@
</li> </li>
</ul> </ul>
<!-- Content Management Section -->
<div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" />
</div>
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
Content Management
</p>
<ul class="mt-3">
<!-- Platform Homepage -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'platform-homepage'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'platform-homepage' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/platform-homepage">
<span x-html="$icon('home')"></span>
<span class="ml-4">Platform Homepage</span>
</a>
</li>
<!-- Content Pages -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'content-pages'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'content-pages' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/content-pages">
<span x-html="$icon('document-text')"></span>
<span class="ml-4">Content Pages</span>
</a>
</li>
</ul>
<!-- Developer Tools Section --> <!-- Developer Tools Section -->
<div class="px-6 my-6"> <div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" /> <hr class="border-gray-200 dark:border-gray-700" />

View File

@@ -0,0 +1,248 @@
{# app/templates/admin/platform-homepage.html #}
{% extends "admin/base.html" %}
{% block title %}Platform Homepage Manager{% endblock %}
{% block alpine_data %}platformHomepageManager(){% endblock %}
{% block content %}
<!-- Page Header -->
<div class="flex items-center justify-between my-6">
<div>
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
Platform Homepage
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
Manage your platform's main landing page at <a href="/" target="_blank" class="text-purple-600 hover:underline">localhost:8000</a>
</p>
</div>
<div class="flex gap-2">
<a
href="/"
target="_blank"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-200 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:border-gray-400 dark:hover:border-gray-500 focus:outline-none focus:shadow-outline-purple"
>
<span x-html="$icon('eye', 'w-4 h-4 mr-2')"></span>
Preview
</a>
<button
@click="savePage()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!saving" x-html="$icon('check', 'w-4 h-4 mr-2')"></span>
<span x-show="saving" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="saving ? 'Saving...' : 'Save Changes'"></span>
</button>
</div>
</div>
<!-- Loading State -->
<div x-show="loading" class="text-center py-12">
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading homepage...</p>
</div>
<!-- Error State -->
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div>
<p class="font-semibold">Error loading homepage</p>
<p class="text-sm" x-text="error"></p>
</div>
</div>
<!-- Success Message -->
<div x-show="successMessage" x-transition class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg flex items-start">
<span x-html="$icon('check-circle', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div>
<p class="font-semibold" x-text="successMessage"></p>
</div>
</div>
<!-- Main Form -->
<div x-show="!loading && page" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
<form @submit.prevent="savePage()">
<!-- Template Selection -->
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Template Selection
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Choose the visual style for your homepage. Each template offers a different layout and design.
</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Default Template -->
<label class="cursor-pointer">
<input type="radio" name="template" value="default" x-model="page.template" class="sr-only">
<div
class="border-2 rounded-lg p-4 transition-all"
:class="page.template === 'default' ? 'border-purple-600 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-300 dark:border-gray-600 hover:border-gray-400'"
>
<div class="flex items-center justify-between mb-2">
<span class="font-semibold text-gray-900 dark:text-white">Default</span>
<span x-show="page.template === 'default'" x-html="$icon('check-circle', 'w-5 h-5 text-purple-600')"></span>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Full-featured with hero section, features grid, vendor cards, and call-to-action.
</p>
<div class="mt-3 h-24 bg-gradient-to-r from-purple-400 to-pink-400 rounded"></div>
</div>
</label>
<!-- Minimal Template -->
<label class="cursor-pointer">
<input type="radio" name="template" value="minimal" x-model="page.template" class="sr-only">
<div
class="border-2 rounded-lg p-4 transition-all"
:class="page.template === 'minimal' ? 'border-purple-600 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-300 dark:border-gray-600 hover:border-gray-400'"
>
<div class="flex items-center justify-between mb-2">
<span class="font-semibold text-gray-900 dark:text-white">Minimal</span>
<span x-show="page.template === 'minimal'" x-html="$icon('check-circle', 'w-5 h-5 text-purple-600')"></span>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Clean and simple design with emoji icons and essential information only.
</p>
<div class="mt-3 h-24 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded"></div>
</div>
</label>
<!-- Modern Template -->
<label class="cursor-pointer">
<input type="radio" name="template" value="modern" x-model="page.template" class="sr-only">
<div
class="border-2 rounded-lg p-4 transition-all"
:class="page.template === 'modern' ? 'border-purple-600 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-300 dark:border-gray-600 hover:border-gray-400'"
>
<div class="flex items-center justify-between mb-2">
<span class="font-semibold text-gray-900 dark:text-white">Modern</span>
<span x-show="page.template === 'modern'" x-html="$icon('check-circle', 'w-5 h-5 text-purple-600')"></span>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Trendy design with animated gradients, stats, and modern UI elements.
</p>
<div class="mt-3 h-24 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 rounded"></div>
</div>
</label>
</div>
</div>
<!-- Page Content -->
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Page Content
</h3>
<!-- Title -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Page Title
</label>
<input
type="text"
x-model="page.title"
required
maxlength="200"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
placeholder="Welcome to Our Multi-Vendor Marketplace"
>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Main heading displayed on the homepage
</p>
</div>
<!-- Content (HTML) -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Content (HTML)
</label>
<textarea
x-model="page.content"
rows="6"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700 font-mono text-sm"
placeholder="<p>Your platform description here...</p>"
></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
HTML content shown below the title. Basic HTML tags are supported.
</p>
</div>
</div>
<!-- SEO Settings -->
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
SEO Settings
</h3>
<!-- Meta Description -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Meta Description
</label>
<textarea
x-model="page.meta_description"
rows="2"
maxlength="300"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
placeholder="A brief description for search engines (150-160 characters recommended)"
></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
<span x-text="(page.meta_description || '').length"></span>/300 characters
</p>
</div>
<!-- Meta Keywords -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Meta Keywords
</label>
<input
type="text"
x-model="page.meta_keywords"
maxlength="300"
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
placeholder="marketplace, multi-vendor, e-commerce"
>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Comma-separated keywords (optional)
</p>
</div>
</div>
<!-- Publishing Settings -->
<div class="p-6 bg-gray-50 dark:bg-gray-700/50">
<div class="flex items-center justify-between">
<div>
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
x-model="page.is_published"
class="w-5 h-5 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-white">
Published
</span>
</label>
<p class="ml-8 text-xs text-gray-500 dark:text-gray-400">
Make this homepage visible to the public
</p>
</div>
<button
type="submit"
:disabled="saving"
class="px-6 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-text="saving ? 'Saving...' : 'Save Changes'"></span>
</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/platform-homepage.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,299 @@
{# app/templates/platform/base.html #}
{# Base template for platform public pages (homepage, about, faq, etc.) #}
<!DOCTYPE html>
<html lang="en" x-data="platformLayoutData()" x-bind:class="{ 'dark': dark }">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{# Dynamic page title #}
<title>{% block title %}Multi-Vendor Marketplace Platform{% endblock %}</title>
{# SEO Meta Tags #}
<meta name="description" content="{% block meta_description %}Leading multi-vendor marketplace platform connecting vendors with customers worldwide.{% endblock %}">
<meta name="keywords" content="{% block meta_keywords %}marketplace, multi-vendor, e-commerce, online shopping{% endblock %}">
{# Favicon #}
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='favicon.ico') }}">
{# Platform color scheme #}
<style id="platform-theme-variables">
:root {
/* Platform Colors */
--color-primary: #6366f1; /* Indigo */
--color-secondary: #8b5cf6; /* Purple */
--color-accent: #ec4899; /* Pink */
/* Typography */
--font-heading: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* Dark mode colors */
.dark {
--color-primary: #818cf8;
--color-secondary: #a78bfa;
--color-accent: #f472b6;
}
</style>
{# Fonts: Local fallback + Google Fonts #}
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
{# Tailwind CSS with local fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Platform-specific styles #}
<style>
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Custom gradients */
.gradient-primary {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
}
.gradient-accent {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-accent) 100%);
}
/* Button styles */
.btn-primary {
background-color: var(--color-primary);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
transition: all 0.2s;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
/* Card hover effect */
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
</style>
{% block extra_head %}{% endblock %}
</head>
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200">
{# Header / Navigation #}
<header class="sticky top-0 z-50 bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
{# Logo / Brand #}
<div class="flex items-center">
<a href="/" class="flex items-center space-x-3">
<div class="w-8 h-8 rounded-lg gradient-primary flex items-center justify-center">
<span class="text-white font-bold text-xl">M</span>
</div>
<span class="text-xl font-bold text-gray-900 dark:text-white">
Marketplace
</span>
</a>
</div>
{# Desktop Navigation #}
<div class="hidden md:flex items-center space-x-8">
{# Dynamic header navigation from CMS #}
{% if header_pages %}
{% for page in header_pages %}
<a href="/{{ page.slug }}"
class="text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary font-medium transition-colors">
{{ page.title }}
</a>
{% endfor %}
{% endif %}
{# Dark mode toggle #}
<button
@click="toggleDarkMode()"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle dark mode"
>
<svg x-show="!dark" class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
<svg x-show="dark" class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</button>
</div>
{# Mobile menu button #}
<div class="md:hidden">
<button
@click="mobileMenuOpen = !mobileMenuOpen"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
aria-label="Toggle menu"
>
<svg x-show="!mobileMenuOpen" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<svg x-show="mobileMenuOpen" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
{# Mobile menu #}
<div x-show="mobileMenuOpen" x-cloak class="md:hidden py-4 border-t border-gray-200 dark:border-gray-700">
{% if header_pages %}
{% for page in header_pages %}
<a href="/{{ page.slug }}"
class="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
{{ page.title }}
</a>
{% endfor %}
{% endif %}
</div>
</nav>
</header>
{# Main Content #}
<main class="min-h-screen">
{% block content %}{% endblock %}
</main>
{# Footer #}
<footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
{# Brand Column #}
<div class="col-span-1">
<div class="flex items-center space-x-3 mb-4">
<div class="w-8 h-8 rounded-lg gradient-primary flex items-center justify-center">
<span class="text-white font-bold text-xl">M</span>
</div>
<span class="text-xl font-bold text-gray-900 dark:text-white">
Marketplace
</span>
</div>
<p class="text-gray-600 dark:text-gray-400 text-sm">
Connecting vendors with customers worldwide. Build your online store today.
</p>
</div>
{# Quick Links #}
<div>
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">Quick Links</h4>
<ul class="space-y-2">
{% if footer_pages %}
{% for page in footer_pages %}
<li>
<a href="/{{ page.slug }}"
class="text-gray-600 dark:text-gray-400 hover:text-primary dark:hover:text-primary transition-colors">
{{ page.title }}
</a>
</li>
{% endfor %}
{% endif %}
</ul>
</div>
{# Platform Links #}
<div>
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">Platform</h4>
<ul class="space-y-2">
<li>
<a href="/admin/login" class="text-gray-600 dark:text-gray-400 hover:text-primary transition-colors">
Admin Login
</a>
</li>
<li>
<a href="/vendor/wizamart/login" class="text-gray-600 dark:text-gray-400 hover:text-primary transition-colors">
Vendor Login
</a>
</li>
</ul>
</div>
{# Contact Info #}
<div>
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">Contact</h4>
<ul class="space-y-2 text-gray-600 dark:text-gray-400 text-sm">
<li>support@marketplace.com</li>
<li>+1 (555) 123-4567</li>
<li>123 Business St, Suite 100</li>
<li>San Francisco, CA 94102</li>
</ul>
</div>
</div>
{# Bottom Bar #}
<div class="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<div class="flex flex-col md:flex-row justify-between items-center">
<p class="text-gray-600 dark:text-gray-400 text-sm">
© 2024 Marketplace Platform. All rights reserved.
</p>
<div class="flex space-x-6 mt-4 md:mt-0">
<a href="/privacy" class="text-gray-600 dark:text-gray-400 hover:text-primary text-sm transition-colors">
Privacy Policy
</a>
<a href="/terms" class="text-gray-600 dark:text-gray-400 hover:text-primary text-sm transition-colors">
Terms of Service
</a>
</div>
</div>
</div>
</div>
</footer>
{# Scripts #}
<!-- Alpine.js for interactivity -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js"></script>
<!-- Platform layout data -->
<script>
function platformLayoutData() {
return {
// Dark mode
dark: localStorage.getItem('darkMode') === 'true',
// Mobile menu
mobileMenuOpen: false,
// Initialize
init() {
// Apply dark mode on load
if (this.dark) {
document.documentElement.classList.add('dark');
}
},
// Toggle dark mode
toggleDarkMode() {
this.dark = !this.dark;
localStorage.setItem('darkMode', this.dark);
if (this.dark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
};
}
</script>
{% block extra_scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,246 @@
{# app/templates/platform/content-page.html #}
{# Generic template for platform content pages (About, FAQ, Terms, Contact, etc.) #}
{% extends "platform/base.html" %}
{% block title %}{{ page.title }} - Marketplace{% endblock %}
{% block meta_description %}
{% if page.meta_description %}
{{ page.meta_description }}
{% else %}
{{ page.title }} - Multi-Vendor Marketplace Platform
{% endif %}
{% endblock %}
{% block meta_keywords %}
{% if page.meta_keywords %}
{{ page.meta_keywords }}
{% else %}
{{ page.title }}, marketplace, platform
{% endif %}
{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{# Breadcrumbs #}
<nav class="flex mb-8 text-sm" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-2">
<li class="inline-flex items-center">
<a href="/" class="text-gray-600 dark:text-gray-400 hover:text-primary dark:hover:text-primary transition-colors">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"></path>
</svg>
Home
</a>
</li>
<li>
<div class="flex items-center">
<svg class="w-4 h-4 text-gray-400 mx-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
<span class="text-gray-700 dark:text-gray-300 font-medium">{{ page.title }}</span>
</div>
</li>
</ol>
</nav>
{# Page Header #}
<div class="mb-12">
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-4">
{{ page.title }}
</h1>
{# Published date (if available) #}
{% if page.published_at %}
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<span>Published {{ page.published_at.strftime('%B %d, %Y') }}</span>
</div>
{% endif %}
</div>
{# Page Content #}
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm p-8 md:p-12">
<div class="prose prose-lg dark:prose-invert max-w-none">
{% if page.content_format == 'markdown' %}
{# Future enhancement: Render with markdown library #}
<div class="markdown-content">
{{ page.content | safe }}
</div>
{% else %}
{# HTML content (default) #}
{{ page.content | safe }}
{% endif %}
</div>
</div>
{# Last updated timestamp #}
{% if page.updated_at %}
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400">
Last updated: {{ page.updated_at.strftime('%B %d, %Y') }}
</p>
</div>
{% endif %}
{# Call-to-action section (for specific pages) #}
{% if page.slug in ['about', 'contact'] %}
<div class="mt-12 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-2xl p-8 text-center">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
{% if page.slug == 'about' %}
Ready to Get Started?
{% elif page.slug == 'contact' %}
Have Questions?
{% endif %}
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-6">
{% if page.slug == 'about' %}
Join thousands of vendors already selling on our platform
{% elif page.slug == 'contact' %}
Our team is here to help you succeed
{% endif %}
</p>
<a href="/contact" class="inline-block bg-gray-900 dark:bg-white text-white dark:text-gray-900 px-8 py-3 rounded-lg font-semibold hover:opacity-90 transition">
{% if page.slug == 'about' %}
Contact Sales
{% elif page.slug == 'contact' %}
Send Us a Message
{% endif %}
</a>
</div>
{% endif %}
</div>
{# Additional styling for prose content #}
<style>
/* Enhanced prose styling for content pages */
.prose {
color: inherit;
}
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
color: inherit;
font-weight: 700;
margin-top: 2em;
margin-bottom: 1em;
}
.prose h2 {
font-size: 1.875rem;
line-height: 2.25rem;
border-bottom: 2px solid var(--color-primary);
padding-bottom: 0.5rem;
}
.prose h3 {
font-size: 1.5rem;
line-height: 2rem;
}
.prose p {
margin-bottom: 1.5em;
line-height: 1.75;
}
.prose ul, .prose ol {
margin-bottom: 1.5em;
padding-left: 1.5em;
}
.prose li {
margin-bottom: 0.5em;
}
.prose a {
color: var(--color-primary);
text-decoration: underline;
font-weight: 500;
}
.prose a:hover {
opacity: 0.8;
}
.prose strong {
font-weight: 600;
color: inherit;
}
.prose code {
background-color: rgba(0, 0, 0, 0.05);
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-size: 0.9em;
}
.dark .prose code {
background-color: rgba(255, 255, 255, 0.1);
}
.prose pre {
background-color: rgba(0, 0, 0, 0.05);
padding: 1em;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1.5em;
}
.dark .prose pre {
background-color: rgba(255, 255, 255, 0.05);
}
.prose blockquote {
border-left: 4px solid var(--color-primary);
padding-left: 1em;
font-style: italic;
opacity: 0.9;
margin: 1.5em 0;
}
.prose table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5em;
}
.prose th, .prose td {
border: 1px solid rgba(0, 0, 0, 0.1);
padding: 0.75em;
text-align: left;
}
.dark .prose th, .dark .prose td {
border-color: rgba(255, 255, 255, 0.1);
}
.prose th {
background-color: rgba(0, 0, 0, 0.05);
font-weight: 600;
}
.dark .prose th {
background-color: rgba(255, 255, 255, 0.05);
}
.prose hr {
border: 0;
border-top: 2px solid rgba(0, 0, 0, 0.1);
margin: 3em 0;
}
.dark .prose hr {
border-top-color: rgba(255, 255, 255, 0.1);
}
.prose img {
border-radius: 0.5rem;
margin: 2em auto;
max-width: 100%;
height: auto;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,228 @@
{# app/templates/platform/homepage-default.html #}
{# Default platform homepage template #}
{% extends "platform/base.html" %}
{% block title %}
{% if page %}{{ page.title }}{% else %}Home{% endif %} - Multi-Vendor Marketplace
{% endblock %}
{% block meta_description %}
{% if page and page.meta_description %}
{{ page.meta_description }}
{% else %}
Leading multi-vendor marketplace platform. Connect with thousands of vendors and discover millions of products.
{% endif %}
{% endblock %}
{% block content %}
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- HERO SECTION -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section class="gradient-primary text-white py-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center">
{% if page %}
{# CMS-driven content #}
<h1 class="text-4xl md:text-6xl font-bold mb-6">
{{ page.title }}
</h1>
<div class="text-xl md:text-2xl mb-8 opacity-90 max-w-3xl mx-auto">
{{ page.content | safe }}
</div>
{% else %}
{# Default fallback content #}
<h1 class="text-4xl md:text-6xl font-bold mb-6">
Welcome to Our Marketplace
</h1>
<p class="text-xl md:text-2xl mb-8 opacity-90 max-w-3xl mx-auto">
Connect vendors with customers worldwide. Build your online store and reach millions of shoppers.
</p>
{% endif %}
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
<a href="/vendors"
class="btn-primary inline-flex items-center space-x-2">
<span>Browse Vendors</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
<a href="/contact"
class="bg-white text-gray-900 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100 transition">
Get Started
</a>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- FEATURES SECTION -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section class="py-16 bg-white dark:bg-gray-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
Why Choose Our Platform?
</h2>
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
Everything you need to launch and grow your online business
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Feature 1 -->
<div class="card-hover bg-gray-50 dark:bg-gray-700 rounded-xl p-8 text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full gradient-primary flex items-center justify-center">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">
Lightning Fast
</h3>
<p class="text-gray-600 dark:text-gray-400">
Optimized for speed and performance. Your store loads in milliseconds.
</p>
</div>
<!-- Feature 2 -->
<div class="card-hover bg-gray-50 dark:bg-gray-700 rounded-xl p-8 text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full gradient-primary flex items-center justify-center">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">
Secure & Reliable
</h3>
<p class="text-gray-600 dark:text-gray-400">
Enterprise-grade security with 99.9% uptime guarantee.
</p>
</div>
<!-- Feature 3 -->
<div class="card-hover bg-gray-50 dark:bg-gray-700 rounded-xl p-8 text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full gradient-primary flex items-center justify-center">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">
Fully Customizable
</h3>
<p class="text-gray-600 dark:text-gray-400">
Brand your store with custom themes, colors, and layouts.
</p>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- FEATURED VENDORS SECTION -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section class="py-16 bg-gray-50 dark:bg-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
Featured Vendors
</h2>
<p class="text-lg text-gray-600 dark:text-gray-400">
Discover amazing shops from around the world
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Vendor Card 1 - Placeholder -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm hover:shadow-lg transition-shadow overflow-hidden">
<div class="h-48 bg-gradient-to-r from-purple-400 to-pink-400"></div>
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Sample Vendor 1
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
Premium products and exceptional service
</p>
<a href="/vendors/vendor1/shop"
class="text-primary hover:underline font-medium inline-flex items-center">
Visit Store
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
</div>
<!-- Vendor Card 2 - Placeholder -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm hover:shadow-lg transition-shadow overflow-hidden">
<div class="h-48 bg-gradient-to-r from-blue-400 to-cyan-400"></div>
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Sample Vendor 2
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
Quality craftsmanship meets modern design
</p>
<a href="/vendors/vendor2/shop"
class="text-primary hover:underline font-medium inline-flex items-center">
Visit Store
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
</div>
<!-- Vendor Card 3 - Placeholder -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm hover:shadow-lg transition-shadow overflow-hidden">
<div class="h-48 bg-gradient-to-r from-green-400 to-teal-400"></div>
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Sample Vendor 3
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
Eco-friendly products for sustainable living
</p>
<a href="/vendors/vendor3/shop"
class="text-primary hover:underline font-medium inline-flex items-center">
Visit Store
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
</div>
</div>
<div class="text-center mt-12">
<a href="/vendors" class="btn-primary inline-block">
View All Vendors
</a>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- CTA SECTION -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section class="py-16 bg-white dark:bg-gray-800">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
Ready to Get Started?
</h2>
<p class="text-lg text-gray-600 dark:text-gray-400 mb-8">
Join thousands of vendors already selling on our platform
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="/contact" class="btn-primary">
Contact Sales
</a>
<a href="/about"
class="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white px-6 py-3 rounded-lg font-semibold hover:bg-gray-200 dark:hover:bg-gray-600 transition">
Learn More
</a>
</div>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,100 @@
{# app/templates/platform/homepage-minimal.html #}
{# Minimal/clean platform homepage template #}
{% extends "platform/base.html" %}
{% block title %}
{% if page %}{{ page.title }}{% else %}Home{% endif %} - Marketplace
{% endblock %}
{% block content %}
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- MINIMAL HERO -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section class="py-32 bg-white dark:bg-gray-800">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
{% if page %}
<h1 class="text-5xl md:text-7xl font-bold text-gray-900 dark:text-white mb-8 leading-tight">
{{ page.title }}
</h1>
<div class="text-xl text-gray-600 dark:text-gray-400 mb-12 max-w-2xl mx-auto">
{{ page.content | safe }}
</div>
{% else %}
<h1 class="text-5xl md:text-7xl font-bold text-gray-900 dark:text-white mb-8 leading-tight">
Multi-Vendor<br>Marketplace
</h1>
<p class="text-xl text-gray-600 dark:text-gray-400 mb-12 max-w-2xl mx-auto">
The simplest way to launch your online store and connect with customers worldwide.
</p>
{% endif %}
<a href="/contact"
class="inline-block bg-gray-900 dark:bg-white text-white dark:text-gray-900 px-8 py-4 rounded-lg font-semibold hover:opacity-90 transition text-lg">
Get Started
</a>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- MINIMAL FEATURES -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section class="py-24 bg-gray-50 dark:bg-gray-900">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-12">
<div class="text-center">
<div class="text-4xl mb-4"></div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Fast
</h3>
<p class="text-gray-600 dark:text-gray-400 text-sm">
Lightning-fast performance optimized for conversions
</p>
</div>
<div class="text-center">
<div class="text-4xl mb-4">🔒</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Secure
</h3>
<p class="text-gray-600 dark:text-gray-400 text-sm">
Enterprise-grade security for your peace of mind
</p>
</div>
<div class="text-center">
<div class="text-4xl mb-4">🎨</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Custom
</h3>
<p class="text-gray-600 dark:text-gray-400 text-sm">
Fully customizable to match your brand identity
</p>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- MINIMAL CTA -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section class="py-24 bg-white dark:bg-gray-800">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-6">
Ready to launch?
</h2>
<p class="text-gray-600 dark:text-gray-400 mb-8">
Join our marketplace today
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="/contact"
class="inline-block bg-gray-900 dark:bg-white text-white dark:text-gray-900 px-6 py-3 rounded-lg font-semibold hover:opacity-90 transition">
Contact Us
</a>
<a href="/about"
class="inline-block border-2 border-gray-900 dark:border-white text-gray-900 dark:text-white px-6 py-3 rounded-lg font-semibold hover:bg-gray-50 dark:hover:bg-gray-700 transition">
Learn More
</a>
</div>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,258 @@
{# app/templates/platform/homepage-modern.html #}
{# Modern/trendy platform homepage template with animations #}
{% extends "platform/base.html" %}
{% block title %}
{% if page %}{{ page.title }}{% else %}Home{% endif %} - Marketplace
{% endblock %}
{% block extra_head %}
<style>
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.float-animation {
animation: float 6s ease-in-out infinite;
}
@keyframes gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animated-gradient {
background: linear-gradient(270deg, #6366f1, #8b5cf6, #ec4899, #f43f5e);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
}
</style>
{% endblock %}
{% block content %}
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- MODERN HERO WITH GRADIENT -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section class="relative overflow-hidden animated-gradient text-white py-24 md:py-32">
{# Decorative elements #}
<div class="absolute top-0 right-0 w-1/3 h-full opacity-20">
<div class="absolute top-20 right-20 w-72 h-72 rounded-full bg-white blur-3xl"></div>
<div class="absolute bottom-20 right-40 w-96 h-96 rounded-full bg-white blur-3xl"></div>
</div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
{# Left column - Content #}
<div>
{% if page %}
<h1 class="text-5xl md:text-7xl font-black mb-6 leading-tight">
{{ page.title }}
</h1>
<div class="text-xl md:text-2xl mb-8 opacity-95">
{{ page.content | safe }}
</div>
{% else %}
<div class="inline-block px-4 py-2 bg-white/20 backdrop-blur-sm rounded-full text-sm font-semibold mb-6">
✨ The Future of E-Commerce
</div>
<h1 class="text-5xl md:text-7xl font-black mb-6 leading-tight">
Build Your<br>
<span class="text-transparent bg-clip-text bg-white">
Dream Store
</span>
</h1>
<p class="text-xl md:text-2xl mb-8 opacity-95">
Launch a stunning online marketplace in minutes. No coding required. Scale effortlessly.
</p>
{% endif %}
<div class="flex flex-col sm:flex-row gap-4">
<a href="/contact"
class="inline-flex items-center justify-center bg-white text-gray-900 px-8 py-4 rounded-xl font-bold hover:shadow-2xl transform hover:scale-105 transition-all duration-200">
<span>Start Free Trial</span>
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
<a href="#features"
class="inline-flex items-center justify-center border-2 border-white text-white px-8 py-4 rounded-xl font-bold hover:bg-white/10 backdrop-blur-sm transition-all duration-200">
Learn More
</a>
</div>
{# Stats #}
<div class="grid grid-cols-3 gap-6 mt-12 pt-12 border-t border-white/20">
<div>
<div class="text-3xl font-bold mb-1">10K+</div>
<div class="text-sm opacity-80">Active Vendors</div>
</div>
<div>
<div class="text-3xl font-bold mb-1">50M+</div>
<div class="text-sm opacity-80">Products Sold</div>
</div>
<div>
<div class="text-3xl font-bold mb-1">99.9%</div>
<div class="text-sm opacity-80">Uptime</div>
</div>
</div>
</div>
{# Right column - Visual element #}
<div class="hidden lg:block float-animation">
<div class="relative">
<div class="w-full h-96 bg-white/10 backdrop-blur-xl rounded-3xl shadow-2xl p-8">
<div class="w-full h-full bg-gradient-to-br from-white/20 to-transparent rounded-2xl flex items-center justify-center">
<div class="text-center">
<div class="text-8xl mb-4">🚀</div>
<div class="text-2xl font-bold">Launch Today</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- FEATURES WITH CARDS -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section id="features" class="py-24 bg-gray-50 dark:bg-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<div class="inline-block px-4 py-2 bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full text-sm font-semibold mb-4">
✨ Features
</div>
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-4">
Everything You Need
</h2>
<p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
Powerful features to help you succeed in the digital marketplace
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{# Feature cards with hover effects #}
<div class="group bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">
Blazing Fast
</h3>
<p class="text-gray-600 dark:text-gray-400">
Optimized for performance with sub-second page loads and instant search results.
</p>
</div>
<div class="group bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">
Bank-Level Security
</h3>
<p class="text-gray-600 dark:text-gray-400">
Enterprise-grade encryption and security measures to protect your business.
</p>
</div>
<div class="group bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-green-500 to-teal-500 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">
Fully Customizable
</h3>
<p class="text-gray-600 dark:text-gray-400">
Brand your store with custom themes, colors, fonts, and layouts.
</p>
</div>
<div class="group bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-orange-500 to-red-500 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">
Analytics & Insights
</h3>
<p class="text-gray-600 dark:text-gray-400">
Powerful analytics to track sales, customer behavior, and growth metrics.
</p>
</div>
<div class="group bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">
Mobile-First Design
</h3>
<p class="text-gray-600 dark:text-gray-400">
Beautiful, responsive design that works perfectly on all devices.
</p>
</div>
<div class="group bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-pink-500 to-rose-500 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">
24/7 Support
</h3>
<p class="text-gray-600 dark:text-gray-400">
Round-the-clock customer support to help you succeed at every step.
</p>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- MODERN CTA WITH GRADIENT -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<section class="py-24 relative overflow-hidden">
<div class="absolute inset-0 gradient-accent opacity-90"></div>
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-white">
<h2 class="text-4xl md:text-5xl font-bold mb-6">
Start Your Journey Today
</h2>
<p class="text-xl mb-10 opacity-90">
Join thousands of successful vendors on our platform
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="/contact"
class="inline-flex items-center justify-center bg-white text-gray-900 px-8 py-4 rounded-xl font-bold hover:shadow-2xl transform hover:scale-105 transition-all duration-200">
<span>Get Started Free</span>
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
<a href="/about"
class="inline-flex items-center justify-center border-2 border-white text-white px-8 py-4 rounded-xl font-bold hover:bg-white/10 backdrop-blur-sm transition-all duration-200">
Learn More About Us
</a>
</div>
<p class="mt-8 text-sm opacity-75">
No credit card required · Free 14-day trial · Cancel anytime
</p>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,710 @@
# Platform Homepage & Content Pages
## Overview
The Platform Homepage feature provides a customizable, CMS-driven public homepage and content pages for your multi-vendor marketplace platform at the root domain (e.g., `localhost:8000` or `platform.com`).
**Key Features:**
- ✅ CMS-driven customizable homepage with multiple templates
- ✅ Multiple content pages (About, FAQ, Contact, Terms, Privacy)
- ✅ Dynamic header and footer navigation
- ✅ Responsive design with dark mode support
- ✅ SEO-optimized with meta tags
- ✅ Template selection system (default, minimal, modern)
---
## Architecture
### URL Structure
| URL | Purpose | CMS Slug | Template |
|-----|---------|----------|----------|
| `/` | Platform Homepage | `platform_homepage` | Customizable (default/minimal/modern) |
| `/about` | About Us | `about` | `platform/content-page.html` |
| `/faq` | FAQ | `faq` | `platform/content-page.html` |
| `/contact` | Contact Us | `contact` | `platform/content-page.html` |
| `/terms` | Terms of Service | `terms` | `platform/content-page.html` |
| `/privacy` | Privacy Policy | `privacy` | `platform/content-page.html` |
### Database Model
All platform pages are stored in the `content_pages` table with `vendor_id = NULL`:
```sql
-- Platform homepage
INSERT INTO content_pages (vendor_id, slug, title, template, ...)
VALUES (NULL, 'platform_homepage', 'Welcome', 'modern', ...);
-- Content pages
INSERT INTO content_pages (vendor_id, slug, title, ...)
VALUES (NULL, 'about', 'About Us', ...);
```
### Navigation System
Pages are configured to appear in header/footer navigation using flags:
| Field | Purpose | Example |
|-------|---------|---------|
| `show_in_header` | Display in top navigation | About, FAQ, Contact |
| `show_in_footer` | Display in footer links | All pages |
| `display_order` | Sort order in menus | 1, 2, 3, ... |
---
## File Structure
```
app/
├── templates/platform/
│ ├── base.html # Base layout with nav/footer
│ ├── homepage-default.html # Default homepage template
│ ├── homepage-minimal.html # Minimal/clean template
│ ├── homepage-modern.html # Modern template with animations
│ └── content-page.html # Generic content page template
├── services/
│ └── content_page_service.py # CMS business logic
└── main.py # Platform routes (lines 284-404)
scripts/
└── create_platform_pages.py # Seeder script
docs/features/
└── platform-homepage.md # This file
```
---
## Templates
### 1. Homepage Templates
Three homepage templates are available:
#### **Default Template** (`platform/homepage-default.html`)
- **Style:** Professional, feature-rich
- **Sections:** Hero, Features, Featured Vendors, CTA
- **Best for:** Comprehensive platform showcase
#### **Minimal Template** (`platform/homepage-minimal.html`)
- **Style:** Clean, minimalist
- **Sections:** Simple hero, minimal features, simple CTA
- **Best for:** Focus on simplicity
#### **Modern Template** (`platform/homepage-modern.html`)
- **Style:** Trendy, animated, gradient-heavy
- **Sections:** Animated hero, feature cards with hover effects, stats
- **Best for:** Modern, tech-forward branding
### 2. Content Page Template
**Template:** `platform/content-page.html`
Used for all content pages (About, FAQ, Contact, etc.)
**Features:**
- Breadcrumb navigation
- Responsive design
- Enhanced prose styling for content
- Context-aware CTAs (for about/contact pages)
- Last updated timestamp
- Dark mode support
---
## Usage
### Admin CMS Interface (Recommended)
#### Platform Homepage Manager (`/admin/platform-homepage`)
The easiest way to manage your platform homepage is through the admin interface:
1. **Login to Admin Panel:**
```
http://localhost:8000/admin/login
```
2. **Navigate to Platform Homepage:**
- Click "Content Management" in the sidebar
- Click "Platform Homepage"
3. **Customize Homepage:**
- **Template Selection:** Choose between Default, Minimal, or Modern templates with visual previews
- **Page Title:** Edit the main heading
- **Content:** Add HTML content for the hero section
- **SEO Settings:** Set meta description and keywords
- **Publishing:** Toggle published status
- **Preview:** Click "Preview" button to see live homepage
- **Save:** Click "Save Changes" to update
**Features:**
- ✅ Visual template selector with previews
- ✅ Real-time save with success/error feedback
- ✅ Character counter for SEO fields
- ✅ One-click preview
- ✅ Dark mode support
#### Content Pages Manager (`/admin/content-pages`)
Manage all platform content pages from a single interface:
1. **Navigate to Content Pages:**
- Click "Content Management" in the sidebar
- Click "Content Pages"
2. **View All Pages:**
- **Tabs:** Switch between All Pages, Platform Defaults, or Vendor Overrides
- **Search:** Type to filter by title, slug, or vendor name
- **Table View:** See status, navigation settings, and last updated
3. **Edit Existing Page:**
- Click the edit icon (pencil) next to any page
- Modify content, SEO, navigation settings
- Click "Update Page" to save
4. **Create New Page:**
- Click "Create Page" button
- Fill in:
- Page title and URL slug
- Content format (HTML or Markdown)
- Content (large textarea)
- SEO metadata
- Navigation settings (header/footer visibility)
- Display order
- Click "Create Page"
5. **Delete Page:**
- Click the delete icon (trash) next to any page
- Confirm deletion
**Features:**
- ✅ Tabbed interface for easy filtering
- ✅ Real-time search
- ✅ Status badges (Published/Draft, Platform/Vendor)
- ✅ Navigation badges (Header/Footer)
- ✅ Quick edit and delete actions
- ✅ Empty state with helpful CTAs
### Creating Platform Pages
#### Method 1: Using the Seeder Script
```bash
# Create all default platform pages
python scripts/create_platform_pages.py
```
This creates:
- Platform Homepage (with modern template)
- About Us
- FAQ
- Contact Us
- Terms of Service
- Privacy Policy
#### Method 2: Via API
**Create Platform Homepage:**
```bash
POST /api/v1/admin/content-pages/platform
{
"slug": "platform_homepage",
"title": "Welcome to Our Marketplace",
"content": "<p>Your custom content...</p>",
"template": "modern",
"vendor_id": null,
"is_published": true,
"show_in_header": false,
"show_in_footer": false
}
```
**Create Content Page:**
```bash
POST /api/v1/admin/content-pages/platform
{
"slug": "about",
"title": "About Us",
"content": "<h2>Our Story</h2><p>...</p>",
"vendor_id": null,
"is_published": true,
"show_in_header": true,
"show_in_footer": true,
"display_order": 1
}
```
#### Method 3: Programmatically
```python
from app.core.database import SessionLocal
from app.services.content_page_service import content_page_service
db = SessionLocal()
# Create homepage
homepage = content_page_service.create_page(
db,
slug="platform_homepage",
title="Welcome",
content="<p>Content...</p>",
template="modern", # default, minimal, or modern
vendor_id=None,
is_published=True,
show_in_header=False,
show_in_footer=False
)
# Create content page
about = content_page_service.create_page(
db,
slug="about",
title="About Us",
content="<h2>Our Story</h2>",
vendor_id=None,
is_published=True,
show_in_header=True, # Show in top nav
show_in_footer=True, # Show in footer
display_order=1
)
db.close()
```
---
## Customization
### Changing Homepage Template
Update the `template` field in the database:
```sql
UPDATE content_pages
SET template = 'minimal' -- or 'default', 'modern'
WHERE slug = 'platform_homepage' AND vendor_id IS NULL;
```
Or via API:
```bash
PUT /api/v1/admin/content-pages/{id}
{
"template": "minimal"
}
```
### Customizing Navigation
**Add page to header menu:**
```python
content_page_service.update_page(
db,
page_id=page.id,
show_in_header=True,
display_order=2 # Position in menu
)
```
**Remove page from footer:**
```python
content_page_service.update_page(
db,
page_id=page.id,
show_in_footer=False
)
```
### Updating Page Content
```python
content_page_service.update_page(
db,
page_id=page.id,
title="New Title",
content="<h1>Updated Content</h1>",
meta_description="New SEO description",
template="modern" # Change template
)
```
---
## How It Works
### 1. Request Flow
```
User visits: http://localhost:8000/
VendorContextMiddleware (no vendor detected, platform mode)
ContextMiddleware (context = unknown)
Route: platform_homepage() in main.py:284
Load page from CMS (slug='platform_homepage', vendor_id=NULL)
Load header/footer navigation pages
Render template: platform/homepage-{template}.html
Response with navigation menu
```
### 2. CMS Lookup Logic
**For Homepage (`/`):**
1. Query: `SELECT * FROM content_pages WHERE slug='platform_homepage' AND vendor_id IS NULL`
2. If found → Render with selected template (default/minimal/modern)
3. If not found → Render fallback static template
**For Content Pages (`/about`, `/faq`, etc.):**
1. Query: `SELECT * FROM content_pages WHERE slug='{slug}' AND vendor_id IS NULL`
2. If found → Render `platform/content-page.html`
3. If not found → Return 404
**Navigation Loading:**
```python
# Header pages (show_in_header=True)
header_pages = content_page_service.list_pages_for_vendor(
db, vendor_id=None, header_only=True
)
# Footer pages (show_in_footer=True)
footer_pages = content_page_service.list_pages_for_vendor(
db, vendor_id=None, footer_only=True
)
```
### 3. Template Rendering
**Homepage:**
```python
# In main.py:284
if homepage:
template_name = homepage.template or "default" # Get from CMS
template_path = f"platform/homepage-{template_name}.html"
return templates.TemplateResponse(template_path, context)
```
**Content Page:**
```python
# In main.py:349
return templates.TemplateResponse(
"platform/content-page.html",
{
"request": request,
"page": page,
"header_pages": header_pages,
"footer_pages": footer_pages
}
)
```
---
## Navigation Menu Example
Given these pages in the database:
| Slug | Title | show_in_header | show_in_footer | display_order |
|------|-------|----------------|----------------|---------------|
| `about` | About Us | ✅ | ✅ | 1 |
| `faq` | FAQ | ✅ | ✅ | 2 |
| `contact` | Contact Us | ✅ | ✅ | 3 |
| `terms` | Terms of Service | ❌ | ✅ | 10 |
| `privacy` | Privacy Policy | ❌ | ✅ | 11 |
**Header Navigation:**
```
Marketplace | About Us | FAQ | Contact Us | 🌙
```
**Footer Navigation:**
```
Quick Links
- About Us
- FAQ
- Contact Us
- Terms of Service
- Privacy Policy
```
---
## SEO Optimization
All pages support SEO meta tags:
```python
content_page_service.create_page(
db,
slug="about",
title="About Our Platform",
meta_description="Learn about our mission to democratize e-commerce...",
meta_keywords="about us, mission, vision, values, company",
# ...
)
```
**Rendered in template:**
```html
<title>About Our Platform - Marketplace</title>
<meta name="description" content="Learn about our mission...">
<meta name="keywords" content="about us, mission, vision...">
```
---
## Testing
### Manual Testing
1. **Visit homepage:**
```
http://localhost:8000/
```
Should show platform homepage with selected template.
2. **Visit content pages:**
```
http://localhost:8000/about
http://localhost:8000/faq
http://localhost:8000/contact
http://localhost:8000/terms
http://localhost:8000/privacy
```
3. **Check navigation:**
- Header should show: About Us, FAQ, Contact Us
- Footer should show all pages
4. **Test dark mode:**
- Click moon icon in header
- Verify dark mode works on all pages
5. **Test responsive design:**
- Resize browser window
- Verify mobile menu works
- Check layouts on different screen sizes
### API Testing
**Get page content:**
```bash
GET /api/v1/shop/content-pages/about
```
**Get navigation:**
```bash
GET /api/v1/shop/content-pages/navigation
```
---
## Advanced Features
### Multiple Homepage Templates
Create custom homepage templates:
1. Create template file:
```
app/templates/platform/homepage-custom.html
```
2. Update CMS page:
```python
content_page_service.update_page(
db,
page_id=homepage.id,
template="custom"
)
```
3. Visit homepage → Renders `platform/homepage-custom.html`
### Dynamic Content Loading
Homepage templates can load dynamic data:
```jinja2
{# In template #}
{% if featured_vendors %}
{% for vendor in featured_vendors %}
<div>{{ vendor.name }}</div>
{% endfor %}
{% endif %}
```
Pass data in route handler:
```python
# In main.py
featured_vendors = db.query(Vendor).filter(Vendor.is_featured==True).all()
return templates.TemplateResponse(
template_path,
{
"request": request,
"page": homepage,
"header_pages": header_pages,
"footer_pages": footer_pages,
"featured_vendors": featured_vendors, # Additional data
}
)
```
---
## Troubleshooting
### Homepage shows 404
**Problem:** Platform homepage not rendering
**Solutions:**
1. Run seeder script:
```bash
python scripts/create_platform_pages.py
```
2. Verify page exists:
```sql
SELECT * FROM content_pages
WHERE slug='platform_homepage' AND vendor_id IS NULL;
```
3. Check page is published:
```sql
UPDATE content_pages
SET is_published=true
WHERE slug='platform_homepage';
```
### Navigation menu is empty
**Problem:** No pages showing in header/footer
**Solutions:**
1. Check navigation flags:
```sql
SELECT slug, title, show_in_header, show_in_footer
FROM content_pages
WHERE vendor_id IS NULL;
```
2. Update flags:
```sql
UPDATE content_pages
SET show_in_header=true, show_in_footer=true
WHERE slug='about' AND vendor_id IS NULL;
```
### Content page returns 404
**Problem:** `/about` or other pages not found
**Solutions:**
1. Verify slug:
```sql
SELECT slug FROM content_pages WHERE vendor_id IS NULL;
```
2. Check published status:
```sql
UPDATE content_pages
SET is_published=true
WHERE slug='about';
```
### Wrong template loads
**Problem:** Homepage uses wrong template
**Solution:**
```sql
UPDATE content_pages
SET template='modern'
WHERE slug='platform_homepage' AND vendor_id IS NULL;
```
---
## Best Practices
1. **Always set SEO meta tags:**
```python
meta_description="Clear 150-160 char description",
meta_keywords="relevant, keywords, comma, separated"
```
2. **Use semantic HTML in content:**
```html
<h2>Main Heading</h2>
<p>Paragraph text...</p>
<ul><li>List item</li></ul>
```
3. **Set appropriate display_order:**
```python
display_order=1 # First in menu
display_order=2 # Second in menu
display_order=10 # Legal pages (lower priority)
```
4. **Use navigation flags wisely:**
- `show_in_header`: Only 3-5 main pages
- `show_in_footer`: Can include more pages
5. **Keep homepage content concise:**
- Use templates for structure
- Add key messaging only
- Link to detailed content pages
---
## API Reference
### Admin Endpoints
```
POST /api/v1/admin/content-pages/platform # Create platform page
GET /api/v1/admin/content-pages/platform # List platform pages
GET /api/v1/admin/content-pages/{id} # Get specific page
PUT /api/v1/admin/content-pages/{id} # Update page
DELETE /api/v1/admin/content-pages/{id} # Delete page
```
### Shop (Public) Endpoints
```
GET /api/v1/shop/content-pages/navigation # Get nav links
GET /api/v1/shop/content-pages/{slug} # Get page content
```
---
## Related Documentation
- [Content Management System](./content-management-system.md) - Full CMS documentation
- [CMS Implementation Guide](./cms-implementation-guide.md) - Technical implementation
- [Admin Frontend Guide](../frontend/admin/page-templates.md) - Admin UI patterns
---
## Summary
The Platform Homepage system provides:
**Customizable homepage** with 3 template options
**Multiple content pages** (About, FAQ, Contact, Terms, Privacy)
**CMS-driven** with full CRUD support
**Dynamic navigation** loaded from database
**SEO-optimized** with meta tags
**Responsive** with dark mode support
**Easy to extend** with custom templates
All pages are managed through the existing CMS system, allowing admins to customize content without touching code.

View File

@@ -0,0 +1,316 @@
# Platform Homepage - Quick Start Guide
Quick reference for setting up and customizing the platform homepage and content pages.
---
## 🚀 Quick Setup
### Step 1: Create Platform Pages
Run the seeder script to create all default platform pages:
```bash
python scripts/create_platform_pages.py
```
This creates:
- ✅ Platform Homepage (modern template)
- ✅ About Us
- ✅ FAQ
- ✅ Contact Us
- ✅ Terms of Service
- ✅ Privacy Policy
### Step 2: Verify Pages
Visit your platform homepage:
```
http://localhost:8000/
```
Visit content pages:
```
http://localhost:8000/about
http://localhost:8000/faq
http://localhost:8000/contact
http://localhost:8000/terms
http://localhost:8000/privacy
```
### Step 3: Customize (Optional)
**Change homepage template:**
```sql
UPDATE content_pages
SET template = 'minimal' -- or 'default', 'modern'
WHERE slug = 'platform_homepage' AND vendor_id IS NULL;
```
**Update homepage content:**
```sql
UPDATE content_pages
SET content = '<p>Your custom homepage content...</p>'
WHERE slug = 'platform_homepage';
```
---
## 📄 Available Templates
### Homepage Templates
| Template | Style | Best For |
|----------|-------|----------|
| `default` | Professional, feature-rich | Comprehensive showcase |
| `minimal` | Clean, simple | Minimalist branding |
| `modern` | Animated, gradient-heavy | Tech-forward platforms |
### Content Page Template
All content pages use `platform/content-page.html`:
- Breadcrumb navigation
- Responsive design
- Enhanced prose styling
- Dark mode support
---
## 🎯 Common Tasks
### Add New Content Page
```python
from app.core.database import SessionLocal
from app.services.content_page_service import content_page_service
db = SessionLocal()
page = content_page_service.create_page(
db,
slug="careers",
title="Careers",
content="<h1>Join Our Team</h1><p>...</p>",
vendor_id=None, # Platform page
is_published=True,
show_in_header=True,
show_in_footer=True,
display_order=4
)
db.close()
```
### Update Page Content
```python
content_page_service.update_page(
db,
page_id=page.id,
title="New Title",
content="<h1>Updated Content</h1>",
meta_description="New description"
)
```
### Change Navigation Visibility
```python
# Show in header menu
content_page_service.update_page(
db,
page_id=page.id,
show_in_header=True,
display_order=2
)
# Remove from footer
content_page_service.update_page(
db,
page_id=page.id,
show_in_footer=False
)
```
---
## 🔍 Verification
### Check Database
```sql
-- List all platform pages
SELECT id, slug, title, template, is_published, show_in_header, show_in_footer
FROM content_pages
WHERE vendor_id IS NULL
ORDER BY display_order;
```
### Test Navigation
1. Visit homepage: `http://localhost:8000/`
2. Check header menu (should show: About, FAQ, Contact)
3. Scroll to footer (should show all pages)
4. Click links to verify they work
### Test Dark Mode
1. Click moon icon in header
2. Verify all pages render correctly in dark mode
3. Toggle back to light mode
---
## 📝 Page Content Examples
### Homepage Content
```html
<p class="lead">
Welcome to our multi-vendor marketplace platform.
Connect with thousands of vendors and discover amazing products.
</p>
<p>
Join our growing community of successful online businesses today.
</p>
```
### About Us Content
```html
<h2>Our Mission</h2>
<p>We're democratizing e-commerce...</p>
<h2>Our Values</h2>
<ul>
<li><strong>Innovation:</strong> Constantly improving</li>
<li><strong>Transparency:</strong> No hidden fees</li>
</ul>
```
### FAQ Content
```html
<h2>Getting Started</h2>
<h3>How do I create an account?</h3>
<p>Contact our sales team...</p>
<h3>What are your pricing plans?</h3>
<p>We offer flexible pricing...</p>
```
---
## 🎨 Customization Tips
### Custom Homepage Template
1. Create new template:
```
app/templates/platform/homepage-custom.html
```
2. Extend base template:
```jinja2
{% extends "platform/base.html" %}
{% block content %}
<!-- Your custom layout -->
{% endblock %}
```
3. Update database:
```sql
UPDATE content_pages
SET template = 'custom'
WHERE slug = 'platform_homepage';
```
### Customize Navigation Order
```sql
UPDATE content_pages
SET display_order = 1
WHERE slug = 'about';
UPDATE content_pages
SET display_order = 2
WHERE slug = 'faq';
UPDATE content_pages
SET display_order = 3
WHERE slug = 'contact';
```
Result in header: `About | FAQ | Contact`
---
## 🐛 Troubleshooting
### Homepage shows 404
**Fix:** Run seeder script
```bash
python scripts/create_platform_pages.py
```
### Navigation menu is empty
**Fix:** Update navigation flags
```sql
UPDATE content_pages
SET show_in_header = true, show_in_footer = true
WHERE slug IN ('about', 'faq', 'contact')
AND vendor_id IS NULL;
```
### Content not updating
**Fix:** Clear any caching and restart server
```bash
# If using uvicorn with reload
# Changes should auto-reload
# If manual restart needed
pkill -f uvicorn
uvicorn main:app --reload
```
---
## 📚 Full Documentation
For complete details, see:
- [Platform Homepage Documentation](../features/platform-homepage.md)
- [CMS Feature Documentation](../features/content-management-system.md)
- [CMS Implementation Guide](../features/cms-implementation-guide.md)
---
## ✅ Checklist
After setup, verify:
- [ ] Homepage loads at `http://localhost:8000/`
- [ ] All content pages are accessible
- [ ] Header navigation shows correct pages
- [ ] Footer navigation shows all pages
- [ ] Dark mode works
- [ ] Mobile responsive design works
- [ ] SEO meta tags are present
- [ ] Links in navigation work correctly
---
## 🎉 You're Done!
Your platform homepage and content pages are now set up and ready to customize!
**Next Steps:**
1. Customize homepage content via admin panel at `/admin/platform-homepage`
2. Manage all content pages at `/admin/content-pages`
3. Add your own branding and colors
4. Create additional content pages as needed
5. Set up analytics tracking

132
main.py
View File

@@ -272,6 +272,138 @@ async def vendor_root_path(vendor_code: str, request: Request, db: Session = Dep
# No landing page - redirect to shop # No landing page - redirect to shop
return RedirectResponse(url=f"/vendors/{vendor_code}/shop/", status_code=302) return RedirectResponse(url=f"/vendors/{vendor_code}/shop/", status_code=302)
# ============================================================================
# PLATFORM PUBLIC PAGES (Platform Homepage, About, FAQ, etc.)
# ============================================================================
logger.info("Registering platform public page routes:")
logger.info(" - / (platform homepage)")
logger.info(" - /{slug} (platform content pages: /about, /faq, /terms, /contact)")
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def platform_homepage(request: Request, db: Session = Depends(get_db)):
"""
Platform homepage at localhost:8000 or platform.com
Looks for CMS page with slug='platform_homepage' (vendor_id=NULL)
Falls back to default static template if not found.
"""
from app.services.content_page_service import content_page_service
logger.debug("[PLATFORM] Homepage requested")
# Try to load platform homepage from CMS
homepage = content_page_service.get_page_for_vendor(
db,
slug="platform_homepage",
vendor_id=None, # Platform-level page
include_unpublished=False
)
# Load header and footer navigation
header_pages = content_page_service.list_pages_for_vendor(
db,
vendor_id=None,
header_only=True,
include_unpublished=False
)
footer_pages = content_page_service.list_pages_for_vendor(
db,
vendor_id=None,
footer_only=True,
include_unpublished=False
)
if homepage:
# Use template selection from CMS
template_name = homepage.template or "default"
template_path = f"platform/homepage-{template_name}.html"
logger.info(f"[PLATFORM] Rendering CMS homepage with template: {template_path}")
return templates.TemplateResponse(
template_path,
{
"request": request,
"page": homepage,
"header_pages": header_pages,
"footer_pages": footer_pages,
}
)
else:
# Fallback to default static template
logger.info("[PLATFORM] No CMS homepage found, using default template")
return templates.TemplateResponse(
"platform/homepage-default.html",
{
"request": request,
"header_pages": header_pages,
"footer_pages": footer_pages,
}
)
@app.get("/{slug}", response_class=HTMLResponse, include_in_schema=False)
async def platform_content_page(
request: Request,
slug: str,
db: Session = Depends(get_db)
):
"""
Platform content pages: /about, /faq, /terms, /contact, etc.
Loads content from CMS with slug (vendor_id=NULL for platform pages).
Returns 404 if page not found.
This route MUST be defined LAST to avoid conflicts with other routes.
"""
from app.services.content_page_service import content_page_service
logger.debug(f"[PLATFORM] Content page requested: /{slug}")
# Load page from CMS
page = content_page_service.get_page_for_vendor(
db,
slug=slug,
vendor_id=None, # Platform pages only
include_unpublished=False
)
if not page:
logger.warning(f"[PLATFORM] Content page not found: {slug}")
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
# Load header and footer navigation
header_pages = content_page_service.list_pages_for_vendor(
db,
vendor_id=None,
header_only=True,
include_unpublished=False
)
footer_pages = content_page_service.list_pages_for_vendor(
db,
vendor_id=None,
footer_only=True,
include_unpublished=False
)
logger.info(f"[PLATFORM] Rendering content page: {page.title} (/{slug})")
return templates.TemplateResponse(
"platform/content-page.html",
{
"request": request,
"page": page,
"header_pages": header_pages,
"footer_pages": footer_pages,
}
)
logger.info("=" * 80) logger.info("=" * 80)
# Log all registered routes # Log all registered routes

View File

@@ -21,6 +21,7 @@ nav:
- Complete Setup Guide: getting-started/DATABASE_SETUP_GUIDE.md - Complete Setup Guide: getting-started/DATABASE_SETUP_GUIDE.md
- Quick Reference: getting-started/DATABASE_QUICK_REFERENCE.md - Quick Reference: getting-started/DATABASE_QUICK_REFERENCE.md
- CMS Quick Start: getting-started/cms-quick-start.md - CMS Quick Start: getting-started/cms-quick-start.md
- Platform Homepage Quick Start: getting-started/platform-homepage-quick-start.md
- Configuration: getting-started/configuration.md - Configuration: getting-started/configuration.md
# ============================================ # ============================================
@@ -149,6 +150,7 @@ nav:
- Content Management System: - Content Management System:
- Overview: features/content-management-system.md - Overview: features/content-management-system.md
- Implementation Guide: features/cms-implementation-guide.md - Implementation Guide: features/cms-implementation-guide.md
- Platform Homepage: features/platform-homepage.md
- Vendor Landing Pages: features/vendor-landing-pages.md - Vendor Landing Pages: features/vendor-landing-pages.md
# ============================================ # ============================================

491
scripts/create_platform_pages.py Executable file
View File

@@ -0,0 +1,491 @@
#!/usr/bin/env python3
"""
Create Platform Content Pages
This script creates default platform-level content pages:
- Platform Homepage (slug='platform_homepage')
- About Us (slug='about')
- FAQ (slug='faq')
- Terms of Service (slug='terms')
- Privacy Policy (slug='privacy')
- Contact Us (slug='contact')
All pages are created with vendor_id=NULL (platform-level defaults).
Usage:
python scripts/create_platform_pages.py
"""
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
from app.core.database import SessionLocal
from app.services.content_page_service import content_page_service
def create_platform_pages():
"""Create default platform content pages."""
db = SessionLocal()
try:
print("=" * 80)
print("CREATING PLATFORM CONTENT PAGES")
print("=" * 80)
print()
# Import ContentPage for checking existing pages
from models.database.content_page import ContentPage
# ========================================================================
# 1. PLATFORM HOMEPAGE
# ========================================================================
print("1. Creating Platform Homepage...")
# Check if already exists
existing = db.query(ContentPage).filter_by(vendor_id=None, slug="platform_homepage").first()
if existing:
print(f" ⚠️ Skipped: Platform Homepage - already exists (ID: {existing.id})")
else:
try:
homepage = content_page_service.create_page(
db,
slug="platform_homepage",
title="Welcome to Our Multi-Vendor Marketplace",
content="""
<p class="lead">
Connect vendors with customers worldwide. Build your online store and reach millions of shoppers.
</p>
<p>
Our platform empowers entrepreneurs to launch their own branded e-commerce stores
with minimal effort and maximum impact.
</p>
""",
template="modern", # Uses platform/homepage-modern.html
vendor_id=None, # Platform-level page
is_published=True,
show_in_header=False, # Homepage is not in menu (it's the root)
show_in_footer=False,
display_order=0,
meta_description="Leading multi-vendor marketplace platform. Connect with thousands of vendors and discover millions of products.",
meta_keywords="marketplace, multi-vendor, e-commerce, online shopping, platform"
)
print(f" ✅ Created: {homepage.title} (/{homepage.slug})")
except Exception as e:
print(f" ⚠️ Error: Platform Homepage - {str(e)}")
# ========================================================================
# 2. ABOUT US
# ========================================================================
print("2. Creating About Us page...")
existing = db.query(ContentPage).filter_by(vendor_id=None, slug="about").first()
if existing:
print(f" ⚠️ Skipped: About Us - already exists (ID: {existing.id})")
else:
try:
about = content_page_service.create_page(
db,
slug="about",
title="About Us",
content="""
<h2>Our Mission</h2>
<p>
We're on a mission to democratize e-commerce by providing powerful,
easy-to-use tools for entrepreneurs worldwide.
</p>
<h2>Our Story</h2>
<p>
Founded in 2024, our platform has grown to serve over 10,000 active vendors
and millions of customers around the globe. We believe that everyone should
have the opportunity to build and grow their own online business.
</p>
<h2>Why Choose Us?</h2>
<ul>
<li><strong>Easy to Start:</strong> Launch your store in minutes, not months</li>
<li><strong>Powerful Tools:</strong> Everything you need to succeed in one platform</li>
<li><strong>Scalable:</strong> Grow from startup to enterprise seamlessly</li>
<li><strong>Reliable:</strong> 99.9% uptime guarantee with 24/7 support</li>
</ul>
<h2>Our Values</h2>
<ul>
<li><strong>Innovation:</strong> We constantly improve and evolve our platform</li>
<li><strong>Transparency:</strong> No hidden fees, no surprises</li>
<li><strong>Community:</strong> We succeed when our vendors succeed</li>
<li><strong>Excellence:</strong> We strive for the highest quality in everything we do</li>
</ul>
""",
vendor_id=None,
is_published=True,
show_in_header=True, # Show in header navigation
show_in_footer=True, # Show in footer
display_order=1,
meta_description="Learn about our mission to democratize e-commerce and empower entrepreneurs worldwide.",
meta_keywords="about us, mission, vision, values, company"
)
print(f" ✅ Created: {about.title} (/{about.slug})")
except Exception as e:
print(f" ⚠️ Error: About Us - {str(e)}")
# ========================================================================
# 3. FAQ
# ========================================================================
print("3. Creating FAQ page...")
existing = db.query(ContentPage).filter_by(vendor_id=None, slug="faq").first()
if existing:
print(f" ⚠️ Skipped: FAQ - already exists (ID: {existing.id})")
else:
try:
faq = content_page_service.create_page(
db,
slug="faq",
title="Frequently Asked Questions",
content="""
<h2>Getting Started</h2>
<h3>How do I create a vendor account?</h3>
<p>
Contact our sales team to get started. We'll set up your account and provide
you with everything you need to launch your store.
</p>
<h3>How long does it take to set up my store?</h3>
<p>
Most vendors can launch their store in less than 24 hours. Our team will guide
you through the setup process step by step.
</p>
<h2>Pricing & Payment</h2>
<h3>What are your pricing plans?</h3>
<p>
We offer flexible pricing plans based on your business needs. Contact us for
detailed pricing information and to find the plan that's right for you.
</p>
<h3>When do I get paid?</h3>
<p>
Payments are processed weekly, with funds typically reaching your account
within 2-3 business days.
</p>
<h2>Features & Support</h2>
<h3>Can I customize my store's appearance?</h3>
<p>
Yes! Our platform supports full theme customization including colors, fonts,
logos, and layouts. Make your store truly yours.
</p>
<h3>What kind of support do you provide?</h3>
<p>
We offer 24/7 email support for all vendors, with priority phone support
available for enterprise plans.
</p>
<h2>Technical Questions</h2>
<h3>Do I need technical knowledge to use the platform?</h3>
<p>
No! Our platform is designed to be user-friendly for everyone. However, if you
want to customize advanced features, our documentation and support team are here to help.
</p>
<h3>Can I integrate with other tools?</h3>
<p>
Yes, we support integrations with popular payment gateways, shipping providers,
and marketing tools.
</p>
""",
vendor_id=None,
is_published=True,
show_in_header=True, # Show in header navigation
show_in_footer=True,
display_order=2,
meta_description="Find answers to common questions about our marketplace platform.",
meta_keywords="faq, frequently asked questions, help, support"
)
print(f" ✅ Created: {faq.title} (/{faq.slug})")
except Exception as e:
print(f" ⚠️ Error: FAQ - {str(e)}")
# ========================================================================
# 4. CONTACT US
# ========================================================================
print("4. Creating Contact Us page...")
existing = db.query(ContentPage).filter_by(vendor_id=None, slug="contact").first()
if existing:
print(f" ⚠️ Skipped: Contact Us - already exists (ID: {existing.id})")
else:
try:
contact = content_page_service.create_page(
db,
slug="contact",
title="Contact Us",
content="""
<h2>Get in Touch</h2>
<p>
We'd love to hear from you! Whether you have questions about our platform,
need technical support, or want to discuss partnership opportunities, our
team is here to help.
</p>
<h2>Contact Information</h2>
<ul>
<li><strong>Email:</strong> support@marketplace.com</li>
<li><strong>Phone:</strong> +1 (555) 123-4567</li>
<li><strong>Hours:</strong> Monday - Friday, 9 AM - 6 PM PST</li>
</ul>
<h2>Office Address</h2>
<p>
123 Business Street, Suite 100<br>
San Francisco, CA 94102<br>
United States
</p>
<h2>Sales Inquiries</h2>
<p>
Interested in launching your store on our platform?<br>
Email: <a href="mailto:sales@marketplace.com">sales@marketplace.com</a>
</p>
<h2>Technical Support</h2>
<p>
Need help with your store?<br>
Email: <a href="mailto:support@marketplace.com">support@marketplace.com</a><br>
24/7 email support for all vendors
</p>
<h2>Media & Press</h2>
<p>
For media inquiries and press releases:<br>
Email: <a href="mailto:press@marketplace.com">press@marketplace.com</a>
</p>
""",
vendor_id=None,
is_published=True,
show_in_header=True, # Show in header navigation
show_in_footer=True,
display_order=3,
meta_description="Get in touch with our team. We're here to help you succeed.",
meta_keywords="contact, support, email, phone, address"
)
print(f" ✅ Created: {contact.title} (/{contact.slug})")
except Exception as e:
print(f" ⚠️ Error: Contact Us - {str(e)}")
# ========================================================================
# 5. TERMS OF SERVICE
# ========================================================================
print("5. Creating Terms of Service page...")
existing = db.query(ContentPage).filter_by(vendor_id=None, slug="terms").first()
if existing:
print(f" ⚠️ Skipped: Terms of Service - already exists (ID: {existing.id})")
else:
try:
terms = content_page_service.create_page(
db,
slug="terms",
title="Terms of Service",
content="""
<p><em>Last updated: January 1, 2024</em></p>
<h2>1. Acceptance of Terms</h2>
<p>
By accessing and using this marketplace platform, you accept and agree to be
bound by the terms and provisions of this agreement.
</p>
<h2>2. Use License</h2>
<p>
Permission is granted to temporarily access the materials on our platform for
personal, non-commercial transitory viewing only.
</p>
<h2>3. Account Terms</h2>
<ul>
<li>You must be at least 18 years old to use our platform</li>
<li>You must provide accurate and complete registration information</li>
<li>You are responsible for maintaining the security of your account</li>
<li>You are responsible for all activities under your account</li>
</ul>
<h2>4. Vendor Responsibilities</h2>
<ul>
<li>Provide accurate product information and pricing</li>
<li>Honor all orders and commitments made through the platform</li>
<li>Comply with all applicable laws and regulations</li>
<li>Maintain appropriate customer service standards</li>
</ul>
<h2>5. Prohibited Activities</h2>
<p>You may not use our platform to:</p>
<ul>
<li>Engage in any fraudulent or illegal activities</li>
<li>Violate any intellectual property rights</li>
<li>Transmit harmful code or malware</li>
<li>Interfere with platform operations</li>
</ul>
<h2>6. Termination</h2>
<p>
We reserve the right to terminate or suspend your account at any time for
violation of these terms.
</p>
<h2>7. Limitation of Liability</h2>
<p>
In no event shall our company be liable for any damages arising out of the
use or inability to use our platform.
</p>
<h2>8. Changes to Terms</h2>
<p>
We reserve the right to modify these terms at any time. We will notify users
of any changes via email.
</p>
<h2>9. Contact</h2>
<p>
If you have any questions about these Terms, please contact us at
<a href="mailto:legal@marketplace.com">legal@marketplace.com</a>.
</p>
""",
vendor_id=None,
is_published=True,
show_in_header=False, # Too legal for header
show_in_footer=True, # Show in footer
display_order=10,
meta_description="Read our terms of service and platform usage policies.",
meta_keywords="terms of service, terms, legal, policy, agreement"
)
print(f" ✅ Created: {terms.title} (/{terms.slug})")
except Exception as e:
print(f" ⚠️ Error: Terms of Service - {str(e)}")
# ========================================================================
# 6. PRIVACY POLICY
# ========================================================================
print("6. Creating Privacy Policy page...")
existing = db.query(ContentPage).filter_by(vendor_id=None, slug="privacy").first()
if existing:
print(f" ⚠️ Skipped: Privacy Policy - already exists (ID: {existing.id})")
else:
try:
privacy = content_page_service.create_page(
db,
slug="privacy",
title="Privacy Policy",
content="""
<p><em>Last updated: January 1, 2024</em></p>
<h2>1. Information We Collect</h2>
<p>We collect information you provide directly to us, including:</p>
<ul>
<li>Name, email address, and contact information</li>
<li>Payment and billing information</li>
<li>Store and product information</li>
<li>Communications with us</li>
</ul>
<h2>2. How We Use Your Information</h2>
<p>We use the information we collect to:</p>
<ul>
<li>Provide, maintain, and improve our services</li>
<li>Process transactions and send related information</li>
<li>Send technical notices and support messages</li>
<li>Respond to your comments and questions</li>
<li>Monitor and analyze trends and usage</li>
</ul>
<h2>3. Information Sharing</h2>
<p>
We do not sell your personal information. We may share information with:
</p>
<ul>
<li>Service providers who help us operate our platform</li>
<li>Law enforcement when required by law</li>
<li>Other parties with your consent</li>
</ul>
<h2>4. Data Security</h2>
<p>
We implement appropriate security measures to protect your personal information.
However, no method of transmission over the internet is 100% secure.
</p>
<h2>5. Your Rights</h2>
<p>You have the right to:</p>
<ul>
<li>Access your personal information</li>
<li>Correct inaccurate information</li>
<li>Request deletion of your information</li>
<li>Opt-out of marketing communications</li>
</ul>
<h2>6. Cookies</h2>
<p>
We use cookies and similar tracking technologies to track activity on our
platform and hold certain information. You can instruct your browser to
refuse cookies.
</p>
<h2>7. Changes to This Policy</h2>
<p>
We may update this privacy policy from time to time. We will notify you of
any changes by posting the new policy on this page.
</p>
<h2>8. Contact Us</h2>
<p>
If you have questions about this Privacy Policy, please contact us at
<a href="mailto:privacy@marketplace.com">privacy@marketplace.com</a>.
</p>
""",
vendor_id=None,
is_published=True,
show_in_header=False, # Too legal for header
show_in_footer=True, # Show in footer
display_order=11,
meta_description="Learn how we collect, use, and protect your personal information.",
meta_keywords="privacy policy, privacy, data protection, gdpr, personal information"
)
print(f" ✅ Created: {privacy.title} (/{privacy.slug})")
except Exception as e:
print(f" ⚠️ Error: Privacy Policy - {str(e)}")
print()
print("=" * 80)
print("✅ Platform pages creation completed successfully!")
print("=" * 80)
print()
print("Created pages:")
print(" - Platform Homepage: http://localhost:8000/")
print(" - About Us: http://localhost:8000/about")
print(" - FAQ: http://localhost:8000/faq")
print(" - Contact Us: http://localhost:8000/contact")
print(" - Terms of Service: http://localhost:8000/terms")
print(" - Privacy Policy: http://localhost:8000/privacy")
print()
except Exception as e:
print(f"\n❌ Error: {e}")
db.rollback()
raise
finally:
db.close()
if __name__ == "__main__":
create_platform_pages()

View File

@@ -0,0 +1,171 @@
// static/admin/js/content-page-edit.js
// Use centralized logger
const contentPageEditLog = window.LogConfig.loggers.contentPageEdit || window.LogConfig.createLogger('contentPageEdit');
// ============================================
// CONTENT PAGE EDITOR FUNCTION
// ============================================
function contentPageEditor(pageId) {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page identifier for sidebar active state
currentPage: 'content-pages',
// Editor state
pageId: pageId,
form: {
slug: '',
title: '',
content: '',
content_format: 'html',
template: 'default',
meta_description: '',
meta_keywords: '',
is_published: false,
show_in_header: false,
show_in_footer: true,
display_order: 0,
vendor_id: null
},
loading: false,
saving: false,
error: null,
successMessage: null,
// Initialize
async init() {
contentPageEditLog.info('=== CONTENT PAGE EDITOR INITIALIZING ===');
contentPageEditLog.info('Page ID:', this.pageId);
// Prevent multiple initializations
if (window._contentPageEditInitialized) {
contentPageEditLog.warn('Content page editor already initialized, skipping...');
return;
}
window._contentPageEditInitialized = true;
if (this.pageId) {
// Edit mode - load existing page
contentPageEditLog.group('Loading page for editing');
await this.loadPage();
contentPageEditLog.groupEnd();
} else {
// Create mode - use default values
contentPageEditLog.info('Create mode - using default form values');
}
contentPageEditLog.info('=== CONTENT PAGE EDITOR INITIALIZATION COMPLETE ===');
},
// Load existing page
async loadPage() {
this.loading = true;
this.error = null;
try {
contentPageEditLog.info(`Fetching page ${this.pageId}...`);
const response = await apiClient.get(`/admin/content-pages/${this.pageId}`);
contentPageEditLog.debug('API Response:', response);
if (!response) {
throw new Error('Invalid API response');
}
// Handle response - API returns object directly
const page = response.data || response;
this.form = {
slug: page.slug || '',
title: page.title || '',
content: page.content || '',
content_format: page.content_format || 'html',
template: page.template || 'default',
meta_description: page.meta_description || '',
meta_keywords: page.meta_keywords || '',
is_published: page.is_published || false,
show_in_header: page.show_in_header || false,
show_in_footer: page.show_in_footer !== undefined ? page.show_in_footer : true,
display_order: page.display_order || 0,
vendor_id: page.vendor_id
};
contentPageEditLog.info('Page loaded successfully');
} catch (err) {
contentPageEditLog.error('Error loading page:', err);
this.error = err.message || 'Failed to load page';
} finally {
this.loading = false;
}
},
// Save page (create or update)
async savePage() {
if (this.saving) return;
this.saving = true;
this.error = null;
this.successMessage = null;
try {
contentPageEditLog.info(this.pageId ? 'Updating page...' : 'Creating page...');
const payload = {
slug: this.form.slug,
title: this.form.title,
content: this.form.content,
content_format: this.form.content_format,
template: this.form.template,
meta_description: this.form.meta_description,
meta_keywords: this.form.meta_keywords,
is_published: this.form.is_published,
show_in_header: this.form.show_in_header,
show_in_footer: this.form.show_in_footer,
display_order: this.form.display_order,
vendor_id: this.form.vendor_id
};
contentPageEditLog.debug('Payload:', payload);
let response;
if (this.pageId) {
// Update existing page
response = await apiClient.put(`/admin/content-pages/${this.pageId}`, payload);
this.successMessage = 'Page updated successfully!';
contentPageEditLog.info('Page updated');
} else {
// Create new page
response = await apiClient.post('/admin/content-pages/platform', payload);
this.successMessage = 'Page created successfully!';
contentPageEditLog.info('Page created');
// Redirect to edit page after creation
const pageData = response.data || response;
if (pageData && pageData.id) {
setTimeout(() => {
window.location.href = `/admin/content-pages/${pageData.id}/edit`;
}, 1500);
}
}
// Clear success message after 3 seconds
setTimeout(() => {
this.successMessage = null;
}, 3000);
} catch (err) {
contentPageEditLog.error('Error saving page:', err);
this.error = err.message || 'Failed to save page';
// Scroll to top to show error
window.scrollTo({ top: 0, behavior: 'smooth' });
} finally {
this.saving = false;
}
}
};
}

View File

@@ -0,0 +1,161 @@
// static/admin/js/content-pages.js
// Use centralized logger
const contentPagesLog = window.LogConfig.loggers.contentPages || window.LogConfig.createLogger('contentPages');
// ============================================
// CONTENT PAGES MANAGER FUNCTION
// ============================================
function contentPagesManager() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page identifier for sidebar active state
currentPage: 'content-pages',
// Content pages specific state
allPages: [],
loading: false,
error: null,
// Tabs and filters
activeTab: 'all', // all, platform, vendor
searchQuery: '',
// Initialize
async init() {
contentPagesLog.info('=== CONTENT PAGES MANAGER INITIALIZING ===');
// Prevent multiple initializations
if (window._contentPagesInitialized) {
contentPagesLog.warn('Content pages manager already initialized, skipping...');
return;
}
window._contentPagesInitialized = true;
contentPagesLog.group('Loading content pages');
await this.loadPages();
contentPagesLog.groupEnd();
contentPagesLog.info('=== CONTENT PAGES MANAGER INITIALIZATION COMPLETE ===');
},
// Computed: Platform pages
get platformPages() {
return this.allPages.filter(page => page.is_platform_default);
},
// Computed: Vendor pages
get vendorPages() {
return this.allPages.filter(page => page.is_vendor_override);
},
// Computed: Filtered pages based on active tab and search
get filteredPages() {
let pages = [];
// Filter by tab
if (this.activeTab === 'platform') {
pages = this.platformPages;
} else if (this.activeTab === 'vendor') {
pages = this.vendorPages;
} else {
pages = this.allPages;
}
// Filter by search query
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase();
pages = pages.filter(page =>
page.title.toLowerCase().includes(query) ||
page.slug.toLowerCase().includes(query) ||
(page.vendor_name && page.vendor_name.toLowerCase().includes(query))
);
}
// Sort by display_order, then title
return pages.sort((a, b) => {
if (a.display_order !== b.display_order) {
return a.display_order - b.display_order;
}
return a.title.localeCompare(b.title);
});
},
// Load all content pages
async loadPages() {
this.loading = true;
this.error = null;
try {
contentPagesLog.info('Fetching all content pages...');
// Fetch all pages (platform + vendor, published + unpublished)
const response = await apiClient.get('/admin/content-pages/?include_unpublished=true');
contentPagesLog.debug('API Response:', response);
if (!response) {
throw new Error('Invalid API response');
}
// Handle response - API returns array directly
this.allPages = Array.isArray(response) ? response : (response.data || response.items || []);
contentPagesLog.info(`Loaded ${this.allPages.length} pages`);
} catch (err) {
contentPagesLog.error('Error loading content pages:', err);
this.error = err.message || 'Failed to load content pages';
} finally {
this.loading = false;
}
},
// Delete a page
async deletePage(page) {
if (!confirm(`Are you sure you want to delete "${page.title}"?`)) {
return;
}
try {
contentPagesLog.info(`Deleting page: ${page.id}`);
await apiClient.delete(`/admin/content-pages/${page.id}`);
// Remove from local array
this.allPages = this.allPages.filter(p => p.id !== page.id);
contentPagesLog.info('Page deleted successfully');
} catch (err) {
contentPagesLog.error('Error deleting page:', err);
alert(`Failed to delete page: ${err.message}`);
}
},
// Format date helper
formatDate(dateString) {
if (!dateString) return '—';
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return 'Today';
} else if (diffDays === 1) {
return 'Yesterday';
} else if (diffDays < 7) {
return `${diffDays} days ago`;
} else {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
}
};
}

View File

@@ -0,0 +1,154 @@
// static/admin/js/platform-homepage.js
// Use centralized logger
const platformHomepageLog = window.LogConfig.loggers.platformHomepage || window.LogConfig.createLogger('platformHomepage');
// ============================================
// PLATFORM HOMEPAGE MANAGER FUNCTION
// ============================================
function platformHomepageManager() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page identifier for sidebar active state
currentPage: 'platform-homepage',
// Platform homepage specific state
page: null,
loading: false,
saving: false,
error: null,
successMessage: null,
// Initialize
async init() {
platformHomepageLog.info('=== PLATFORM HOMEPAGE MANAGER INITIALIZING ===');
// Prevent multiple initializations
if (window._platformHomepageInitialized) {
platformHomepageLog.warn('Platform homepage manager already initialized, skipping...');
return;
}
window._platformHomepageInitialized = true;
platformHomepageLog.group('Loading platform homepage');
await this.loadPlatformHomepage();
platformHomepageLog.groupEnd();
platformHomepageLog.info('=== PLATFORM HOMEPAGE MANAGER INITIALIZATION COMPLETE ===');
},
// Load platform homepage from API
async loadPlatformHomepage() {
this.loading = true;
this.error = null;
try {
platformHomepageLog.info('Fetching platform homepage...');
// Fetch all platform pages
const response = await apiClient.get('/admin/content-pages/platform?include_unpublished=true');
platformHomepageLog.debug('API Response:', response);
if (!response) {
throw new Error('Invalid API response');
}
// Handle response - API returns array directly
const pages = Array.isArray(response) ? response : (response.data || response.items || []);
// Find the platform_homepage page
const homepage = pages.find(page => page.slug === 'platform_homepage');
if (!homepage) {
platformHomepageLog.warn('Platform homepage not found, creating default...');
// Initialize with default values
this.page = {
id: null,
slug: 'platform_homepage',
title: 'Welcome to Our Multi-Vendor Marketplace',
content: '<p>Connect vendors with customers worldwide. Build your online store and reach millions of shoppers.</p>',
template: 'default',
content_format: 'html',
meta_description: 'Leading multi-vendor marketplace platform. Connect with thousands of vendors and discover millions of products.',
meta_keywords: 'marketplace, multi-vendor, e-commerce, online shopping',
is_published: false,
show_in_header: false,
show_in_footer: false,
display_order: 0
};
} else {
this.page = { ...homepage };
platformHomepageLog.info('Platform homepage loaded:', this.page);
}
} catch (err) {
platformHomepageLog.error('Error loading platform homepage:', err);
this.error = err.message || 'Failed to load platform homepage';
} finally {
this.loading = false;
}
},
// Save platform homepage
async savePage() {
if (this.saving) return;
this.saving = true;
this.error = null;
this.successMessage = null;
try {
platformHomepageLog.info('Saving platform homepage...');
const payload = {
slug: 'platform_homepage',
title: this.page.title,
content: this.page.content,
content_format: this.page.content_format || 'html',
template: this.page.template,
meta_description: this.page.meta_description,
meta_keywords: this.page.meta_keywords,
is_published: this.page.is_published,
show_in_header: false, // Homepage never in header
show_in_footer: false, // Homepage never in footer
display_order: 0,
vendor_id: null // Platform default
};
platformHomepageLog.debug('Payload:', payload);
let response;
if (this.page.id) {
// Update existing page
response = await apiClient.put(`/admin/content-pages/${this.page.id}`, payload);
platformHomepageLog.info('Platform homepage updated');
} else {
// Create new page
response = await apiClient.post('/admin/content-pages/platform', payload);
platformHomepageLog.info('Platform homepage created');
}
if (response) {
// Handle response - API returns object directly
const pageData = response.data || response;
this.page = { ...pageData };
this.successMessage = 'Platform homepage saved successfully!';
// Clear success message after 3 seconds
setTimeout(() => {
this.successMessage = null;
}, 3000);
}
} catch (err) {
platformHomepageLog.error('Error saving platform homepage:', err);
this.error = err.message || 'Failed to save platform homepage';
} finally {
this.saving = false;
}
}
};
}