Files
orion/app/templates/vendor/content-pages.html
Samir Boulahtit 968002630e feat: complete multi-platform CMS phases 2-5
Phase 2 - OMS Migration & Integration:
- Fix platform_pages.py to use get_platform_page for marketing pages
- Fix shop_pages.py to pass platform_id to content page service calls

Phase 3 - Admin Interface:
- Add platform management API (app/api/v1/admin/platforms.py)
- Add platforms admin page with stats cards
- Add Platforms menu item to admin sidebar
- Update content pages admin with platform filter and four-tab tier system

Phase 4 - Documentation:
- Add comprehensive architecture docs (docs/architecture/multi-platform-cms.md)
- Update implementation plan with completion status

Phase 5 - Vendor Dashboard:
- Add CMS usage API endpoint with tier limits
- Add usage progress bar to vendor content pages
- Add platform-default/{slug} API for preview
- Add View Default button and modal in page editor

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 16:30:31 +01:00

328 lines
19 KiB
HTML

{# app/templates/vendor/content-pages.html #}
{% extends "vendor/base.html" %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/tabs.html' import tabs_inline, tab_button %}
{% block title %}Content Pages{% endblock %}
{% block alpine_data %}vendorContentPagesManager(){% endblock %}
{% block content %}
{{ page_header('Content Pages', subtitle='Customize your shop pages or create new ones', action_label='Create Page', action_url='/vendor/' + vendor_code + '/content-pages/create') }}
{{ loading_state('Loading pages...') }}
{{ error_state('Error loading pages') }}
<!-- CMS Usage Indicator -->
<div x-show="!loading && cmsUsage" 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">
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">CMS Usage</span>
<span class="text-sm text-gray-500 dark:text-gray-400">
<span x-text="cmsUsage.total_pages"></span>
<span x-show="cmsUsage.pages_limit"> / <span x-text="cmsUsage.pages_limit"></span></span>
<span x-show="!cmsUsage.pages_limit"> (unlimited)</span>
pages
</span>
</div>
<!-- Progress Bar -->
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div
class="h-2.5 rounded-full transition-all duration-300"
:class="{
'bg-green-500': cmsUsage.usage_percent < 70,
'bg-yellow-500': cmsUsage.usage_percent >= 70 && cmsUsage.usage_percent < 90,
'bg-red-500': cmsUsage.usage_percent >= 90
}"
:style="`width: ${cmsUsage.pages_limit ? cmsUsage.usage_percent : 0}%`"
></div>
</div>
<div class="flex justify-between mt-1 text-xs text-gray-500 dark:text-gray-400">
<span><span x-text="cmsUsage.override_pages"></span> overrides</span>
<span><span x-text="cmsUsage.custom_pages"></span> custom pages</span>
</div>
</div>
<!-- Upgrade Prompt (show when approaching limit) -->
<div x-show="cmsUsage.pages_limit && cmsUsage.usage_percent >= 80" class="flex-shrink-0">
<a
href="/vendor/{{ vendor_code }}/settings/billing"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-amber-700 bg-amber-100 dark:bg-amber-900/30 dark:text-amber-300 rounded-lg hover:bg-amber-200 dark:hover:bg-amber-900/50 transition-colors"
>
<span x-html="$icon('arrow-trending-up', 'w-4 h-4 mr-1')"></span>
Upgrade for more pages
</a>
</div>
</div>
</div>
<!-- Tabs and Info -->
<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 -->
{% call tabs_inline() %}
{{ tab_button('platform', 'Platform Defaults', count_var='platformPages.length') }}
{{ tab_button('custom', 'My Pages', count_var='customPages.length') }}
{% endcall %}
<!-- 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>
<!-- Platform Defaults Tab -->
<div x-show="!loading && activeTab === 'platform'" class="space-y-4">
<!-- Info Banner -->
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div class="flex">
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-500 mr-3 flex-shrink-0 mt-0.5')"></span>
<div>
<h4 class="text-sm font-medium text-blue-800 dark:text-blue-200">Platform Default Pages</h4>
<p class="text-sm text-blue-700 dark:text-blue-300 mt-1">
These pages are provided by the platform. You can override any of them with your own custom content.
Your overridden version will be shown to your customers instead of the default.
</p>
</div>
</div>
</div>
<!-- Platform Pages Table -->
<div x-show="filteredPlatformPages.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">URL</th>
<th class="px-4 py-3">Navigation</th>
<th class="px-4 py-3">Status</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 filteredPlatformPages" :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-500 dark:text-gray-400">Platform Default</p>
</div>
</td>
<!-- URL/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>
<!-- Navigation -->
<td class="px-4 py-3 text-xs">
<div class="flex gap-1 flex-wrap">
<span x-show="page.show_in_header" class="px-2 py-0.5 font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900/50 dark:text-indigo-300 rounded-full">Header</span>
<span x-show="page.show_in_footer" class="px-2 py-0.5 font-medium bg-teal-100 text-teal-800 dark:bg-teal-900/50 dark:text-teal-300 rounded-full">Footer</span>
<span x-show="page.show_in_legal" class="px-2 py-0.5 font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300 rounded-full">Legal</span>
<span x-show="!page.show_in_header && !page.show_in_footer && !page.show_in_legal" class="text-gray-400"></span>
</div>
</td>
<!-- Status -->
<td class="px-4 py-3 text-sm">
<span x-show="hasOverride(page.slug)" class="px-2 py-1 text-xs font-semibold bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 rounded-full">
Overridden
</span>
<span x-show="!hasOverride(page.slug)" class="px-2 py-1 text-xs font-semibold bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 rounded-full">
Using Default
</span>
</td>
<!-- Actions -->
<td class="px-4 py-3">
<div class="flex items-center space-x-2 text-sm">
<!-- Override / Edit Override button -->
<template x-if="hasOverride(page.slug)">
<a
:href="`/vendor/${vendorCode}/content-pages/${getOverrideId(page.slug)}/edit`"
class="flex items-center justify-center px-3 py-1.5 text-sm font-medium text-purple-600 bg-purple-50 rounded-lg hover:bg-purple-100 dark:text-purple-400 dark:bg-purple-900/30 dark:hover:bg-purple-900/50 transition-colors"
>
<span x-html="$icon('edit', 'w-4 h-4 mr-1')"></span>
Edit Override
</a>
</template>
<template x-if="!hasOverride(page.slug)">
<button
@click="createOverride(page)"
class="flex items-center justify-center px-3 py-1.5 text-sm font-medium text-blue-600 bg-blue-50 rounded-lg hover:bg-blue-100 dark:text-blue-400 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 transition-colors"
>
<span x-html="$icon('plus', 'w-4 h-4 mr-1')"></span>
Override
</button>
</template>
<!-- Preview button -->
<a
:href="`/vendors/${vendorCode}/shop/${page.slug}`"
target="_blank"
class="flex items-center justify-center p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 transition-colors"
title="Preview"
>
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</a>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Empty State -->
<div x-show="filteredPlatformPages.length === 0 && searchQuery" class="text-center py-12 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<span x-html="$icon('search', 'inline w-12 h-12 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">
No platform pages match "<span x-text="searchQuery"></span>"
</p>
</div>
</div>
<!-- Custom Pages Tab -->
<div x-show="!loading && activeTab === 'custom'" class="space-y-4">
<!-- Info Banner -->
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
<div class="flex">
<span x-html="$icon('plus-circle', 'w-5 h-5 text-green-500 mr-3 flex-shrink-0 mt-0.5')"></span>
<div>
<h4 class="text-sm font-medium text-green-800 dark:text-green-200">Your Custom Pages</h4>
<p class="text-sm text-green-700 dark:text-green-300 mt-1">
Create unique pages for your shop like promotions, brand story, or special information.
These pages are exclusive to your store.
</p>
</div>
</div>
</div>
<!-- Custom Pages Table -->
<div x-show="filteredCustomPages.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">URL</th>
<th class="px-4 py-3">Navigation</th>
<th class="px-4 py-3">Status</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 filteredCustomPages" :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 x-show="page.is_vendor_override" class="text-xs text-purple-600 dark:text-purple-400">Override of platform default</p>
<p x-show="!page.is_vendor_override" class="text-xs text-green-600 dark:text-green-400">Custom page</p>
</div>
</td>
<!-- URL/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>
<!-- Navigation -->
<td class="px-4 py-3 text-xs">
<div class="flex gap-1 flex-wrap">
<span x-show="page.show_in_header" class="px-2 py-0.5 font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900/50 dark:text-indigo-300 rounded-full">Header</span>
<span x-show="page.show_in_footer" class="px-2 py-0.5 font-medium bg-teal-100 text-teal-800 dark:bg-teal-900/50 dark:text-teal-300 rounded-full">Footer</span>
<span x-show="page.show_in_legal" class="px-2 py-0.5 font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300 rounded-full">Legal</span>
<span x-show="!page.show_in_header && !page.show_in_footer && !page.show_in_legal" class="text-gray-400"></span>
</div>
</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>
<!-- 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="`/vendor/${vendorCode}/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('edit', 'w-5 h-5')"></span>
</a>
<a
:href="`/vendors/${vendorCode}/shop/${page.slug}`"
target="_blank"
class="flex items-center justify-center p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 transition-colors"
title="Preview"
>
<span x-html="$icon('eye', '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('delete', 'w-5 h-5')"></span>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Empty State -->
<div x-show="filteredCustomPages.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" x-text="searchQuery ? 'No pages found' : 'No custom pages yet'"></h3>
<p class="text-gray-500 dark:text-gray-400 mb-4" x-show="searchQuery">
No custom pages match "<span x-text="searchQuery"></span>"
</p>
<p class="text-gray-500 dark:text-gray-400 mb-4" x-show="!searchQuery">
Create your first custom page or override a platform default.
</p>
<a
:href="`/vendor/${vendorCode}/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 Page
</a>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='vendor/js/content-pages.js') }}"></script>
{% endblock %}