refactor(cms): migrate store theme UI from tenancy to CMS module
Move store theme admin pages, templates, and JS from tenancy module
to CMS module where the data layer (model, service, API, schemas)
already lives. Eliminates split ownership.
Moved:
- Route handlers: GET /store-themes, GET /stores/{code}/theme
- Templates: store-theme.html, store-themes.html
- JS: store-theme.js, store-themes.js
- Updated static references: tenancy_static → cms_static
Deleted old tenancy files (no remaining references).
Menu item in CMS definition already pointed to correct route.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
463
app/modules/cms/templates/cms/admin/store-theme.html
Normal file
463
app/modules/cms/templates/cms/admin/store-theme.html
Normal file
@@ -0,0 +1,463 @@
|
||||
{# app/templates/admin/store-theme.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex %}
|
||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
||||
|
||||
{% block title %}Theme Editor - {{ store_code }}{% endblock %}
|
||||
|
||||
{# ✅ CRITICAL: Binds to adminStoreTheme() function in store-theme.js #}
|
||||
{% block alpine_data %}adminStoreTheme(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call page_header_flex(title='Theme Editor', subtitle_var="'Customize appearance for ' + (store?.name || '...')") %}
|
||||
<a :href="`/admin/stores/${storeCode}`"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:shadow-outline-gray">
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
Back to Store
|
||||
</a>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading theme...') }}
|
||||
|
||||
{{ error_state('Error', show_condition='error && !loading') }}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-3">
|
||||
|
||||
<!-- Theme Configuration Form (2 columns) -->
|
||||
<div class="md:col-span-2 space-y-6">
|
||||
|
||||
<!-- Theme Presets -->
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('palette', 'inline w-5 h-5 mr-2')"></span>
|
||||
Choose a Preset
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Start with a pre-designed theme, then customize it to match your brand.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<!-- Default Preset -->
|
||||
<button @click="applyPreset('default')"
|
||||
:disabled="saving"
|
||||
:class="themeData.theme_name === 'default' ? 'ring-2 ring-purple-500' : ''"
|
||||
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-purple-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div class="flex items-center justify-center space-x-2 mb-2">
|
||||
<div class="w-4 h-4 rounded bg-indigo-500"></div>
|
||||
<div class="w-4 h-4 rounded bg-purple-500"></div>
|
||||
<div class="w-4 h-4 rounded bg-pink-500"></div>
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-gray-300">Default</p>
|
||||
</button>
|
||||
|
||||
<!-- Modern Preset -->
|
||||
<button @click="applyPreset('modern')"
|
||||
:disabled="saving"
|
||||
:class="themeData.theme_name === 'modern' ? 'ring-2 ring-purple-500' : ''"
|
||||
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-purple-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div class="flex items-center justify-center space-x-2 mb-2">
|
||||
<div class="w-4 h-4 rounded bg-indigo-500"></div>
|
||||
<div class="w-4 h-4 rounded bg-purple-600"></div>
|
||||
<div class="w-4 h-4 rounded bg-pink-500"></div>
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-gray-300">Modern</p>
|
||||
</button>
|
||||
|
||||
<!-- Classic Preset -->
|
||||
<button @click="applyPreset('classic')"
|
||||
:disabled="saving"
|
||||
:class="themeData.theme_name === 'classic' ? 'ring-2 ring-purple-500' : ''"
|
||||
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-blue-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div class="flex items-center justify-center space-x-2 mb-2">
|
||||
<div class="w-4 h-4 rounded bg-blue-800"></div>
|
||||
<div class="w-4 h-4 rounded bg-purple-700"></div>
|
||||
<div class="w-4 h-4 rounded bg-red-600"></div>
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-gray-300">Classic</p>
|
||||
</button>
|
||||
|
||||
<!-- Minimal Preset -->
|
||||
<button @click="applyPreset('minimal')"
|
||||
:disabled="saving"
|
||||
:class="themeData.theme_name === 'minimal' ? 'ring-2 ring-purple-500' : ''"
|
||||
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-gray-400 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div class="flex items-center justify-center space-x-2 mb-2">
|
||||
<div class="w-4 h-4 rounded bg-black"></div>
|
||||
<div class="w-4 h-4 rounded bg-gray-600"></div>
|
||||
<div class="w-4 h-4 rounded bg-gray-400"></div>
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-gray-300">Minimal</p>
|
||||
</button>
|
||||
|
||||
<!-- Vibrant Preset -->
|
||||
<button @click="applyPreset('vibrant')"
|
||||
:disabled="saving"
|
||||
:class="themeData.theme_name === 'vibrant' ? 'ring-2 ring-purple-500' : ''"
|
||||
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-orange-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div class="flex items-center justify-center space-x-2 mb-2">
|
||||
<div class="w-4 h-4 rounded bg-orange-500"></div>
|
||||
<div class="w-4 h-4 rounded bg-red-500"></div>
|
||||
<div class="w-4 h-4 rounded bg-purple-600"></div>
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-gray-300">Vibrant</p>
|
||||
</button>
|
||||
|
||||
<!-- Elegant Preset -->
|
||||
<button @click="applyPreset('elegant')"
|
||||
:disabled="saving"
|
||||
:class="themeData.theme_name === 'elegant' ? 'ring-2 ring-purple-500' : ''"
|
||||
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-gray-400 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div class="flex items-center justify-center space-x-2 mb-2">
|
||||
<div class="w-4 h-4 rounded bg-gray-500"></div>
|
||||
<div class="w-4 h-4 rounded bg-gray-700"></div>
|
||||
<div class="w-4 h-4 rounded bg-amber-600"></div>
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-gray-300">Elegant</p>
|
||||
</button>
|
||||
|
||||
<!-- Nature Preset -->
|
||||
<button @click="applyPreset('nature')"
|
||||
:disabled="saving"
|
||||
:class="themeData.theme_name === 'nature' ? 'ring-2 ring-purple-500' : ''"
|
||||
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-green-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div class="flex items-center justify-center space-x-2 mb-2">
|
||||
<div class="w-4 h-4 rounded bg-green-600"></div>
|
||||
<div class="w-4 h-4 rounded bg-emerald-500"></div>
|
||||
<div class="w-4 h-4 rounded bg-amber-500"></div>
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-gray-300">Nature</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colors Section -->
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('color-swatch', 'inline w-5 h-5 mr-2')"></span>
|
||||
Colors
|
||||
</h3>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<!-- Primary Color -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Primary Color</span>
|
||||
<p class="text-xs text-gray-500 mb-2">Main brand color for buttons and links</p>
|
||||
<div class="flex items-center mt-1 space-x-2">
|
||||
<input type="color"
|
||||
x-model="themeData.colors.primary"
|
||||
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
|
||||
<input type="text"
|
||||
x-model="themeData.colors.primary"
|
||||
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- Secondary Color -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Secondary Color</span>
|
||||
<p class="text-xs text-gray-500 mb-2">Supporting color for accents</p>
|
||||
<div class="flex items-center mt-1 space-x-2">
|
||||
<input type="color"
|
||||
x-model="themeData.colors.secondary"
|
||||
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
|
||||
<input type="text"
|
||||
x-model="themeData.colors.secondary"
|
||||
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- Accent Color -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Accent Color</span>
|
||||
<p class="text-xs text-gray-500 mb-2">Call-to-action and highlights</p>
|
||||
<div class="flex items-center mt-1 space-x-2">
|
||||
<input type="color"
|
||||
x-model="themeData.colors.accent"
|
||||
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
|
||||
<input type="text"
|
||||
x-model="themeData.colors.accent"
|
||||
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- Background Color -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Background Color</span>
|
||||
<p class="text-xs text-gray-500 mb-2">Page background</p>
|
||||
<div class="flex items-center mt-1 space-x-2">
|
||||
<input type="color"
|
||||
x-model="themeData.colors.background"
|
||||
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
|
||||
<input type="text"
|
||||
x-model="themeData.colors.background"
|
||||
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- Text Color -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Text Color</span>
|
||||
<p class="text-xs text-gray-500 mb-2">Primary text color</p>
|
||||
<div class="flex items-center mt-1 space-x-2">
|
||||
<input type="color"
|
||||
x-model="themeData.colors.text"
|
||||
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
|
||||
<input type="text"
|
||||
x-model="themeData.colors.text"
|
||||
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- Border Color -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Border Color</span>
|
||||
<p class="text-xs text-gray-500 mb-2">Borders and dividers</p>
|
||||
<div class="flex items-center mt-1 space-x-2">
|
||||
<input type="color"
|
||||
x-model="themeData.colors.border"
|
||||
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
|
||||
<input type="text"
|
||||
x-model="themeData.colors.border"
|
||||
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typography Section -->
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('document', 'inline w-5 h-5 mr-2')"></span>
|
||||
Typography
|
||||
</h3>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<!-- Heading Font -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Heading Font</span>
|
||||
<p class="text-xs text-gray-500 mb-2">For titles and headings</p>
|
||||
<select x-model="themeData.fonts.heading"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
|
||||
<option value="Inter, sans-serif">Inter (Modern)</option>
|
||||
<option value="Roboto, sans-serif">Roboto (Clean)</option>
|
||||
<option value="Poppins, sans-serif">Poppins (Friendly)</option>
|
||||
<option value="Playfair Display, serif">Playfair Display (Elegant)</option>
|
||||
<option value="Merriweather, serif">Merriweather (Classic)</option>
|
||||
<option value="Georgia, serif">Georgia (Traditional)</option>
|
||||
<option value="Helvetica, sans-serif">Helvetica (Minimal)</option>
|
||||
<option value="Montserrat, sans-serif">Montserrat (Bold)</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<!-- Body Font -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Body Font</span>
|
||||
<p class="text-xs text-gray-500 mb-2">For body text and content</p>
|
||||
<select x-model="themeData.fonts.body"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
|
||||
<option value="Inter, sans-serif">Inter (Modern)</option>
|
||||
<option value="Roboto, sans-serif">Roboto (Clean)</option>
|
||||
<option value="Open Sans, sans-serif">Open Sans (Readable)</option>
|
||||
<option value="Lato, sans-serif">Lato (Friendly)</option>
|
||||
<option value="Arial, sans-serif">Arial (Universal)</option>
|
||||
<option value="Georgia, serif">Georgia (Traditional)</option>
|
||||
<option value="Helvetica, sans-serif">Helvetica (Minimal)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layout Section -->
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('view-grid', 'inline w-5 h-5 mr-2')"></span>
|
||||
Layout
|
||||
</h3>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<!-- Product Layout Style -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Product Layout</span>
|
||||
<select x-model="themeData.layout.style"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
|
||||
<option value="grid">Grid</option>
|
||||
<option value="list">List</option>
|
||||
<option value="masonry">Masonry</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<!-- Header Style -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Header Style</span>
|
||||
<select x-model="themeData.layout.header"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
|
||||
<option value="fixed">Fixed</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="transparent">Transparent</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<!-- Product Card Style -->
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400 font-medium">Product Card</span>
|
||||
<select x-model="themeData.layout.product_card"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
|
||||
<option value="modern">Modern</option>
|
||||
<option value="classic">Classic</option>
|
||||
<option value="minimal">Minimal</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom CSS Section -->
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('code', 'inline w-5 h-5 mr-2')"></span>
|
||||
Advanced: Custom CSS
|
||||
</h3>
|
||||
<label class="block text-sm">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
Add custom CSS rules for advanced styling (use with caution)
|
||||
</p>
|
||||
<textarea x-model="themeData.custom_css"
|
||||
rows="6"
|
||||
placeholder=".my-custom-class {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}"
|
||||
class="block w-full mt-1 text-sm font-mono dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-textarea rounded"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center">
|
||||
<button @click="showResetThemeModal = true"
|
||||
:disabled="saving"
|
||||
class="px-4 py-2 text-sm font-medium leading-5 text-red-700 transition-colors duration-150 bg-white border border-red-300 rounded-lg hover:bg-red-50 focus:outline-none disabled:opacity-50 dark:bg-gray-800 dark:text-red-400 dark:border-red-600">
|
||||
<span x-html="$icon('refresh', 'inline w-4 h-4 mr-2')"></span>
|
||||
Reset to Default
|
||||
</button>
|
||||
<button @click="saveTheme()"
|
||||
:disabled="saving"
|
||||
class="flex items-center 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 disabled:opacity-50">
|
||||
<span x-show="!saving">
|
||||
<span x-html="$icon('save', 'inline w-4 h-4 mr-2')"></span>
|
||||
Save Theme
|
||||
</span>
|
||||
<span x-show="saving" class="flex items-center">
|
||||
<span x-html="$icon('spinner', 'inline w-4 h-4 mr-2 animate-spin')"></span>
|
||||
Saving...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Panel (1 column) -->
|
||||
<div class="md:col-span-1">
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 sticky top-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-html="$icon('eye', 'inline w-5 h-5 mr-2')"></span>
|
||||
Preview
|
||||
</h3>
|
||||
|
||||
<!-- Theme Preview -->
|
||||
<div class="space-y-4">
|
||||
<!-- Current Theme Name -->
|
||||
<div class="p-3 bg-purple-50 dark:bg-purple-900 dark:bg-opacity-20 rounded-lg">
|
||||
<p class="text-xs font-semibold text-purple-800 dark:text-purple-200 mb-1">ACTIVE THEME</p>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 capitalize" x-text="themeData.theme_name"></p>
|
||||
</div>
|
||||
|
||||
<!-- Colors Preview -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2">COLORS</p>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="text-center">
|
||||
<div class="h-12 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
|
||||
:style="`background-color: ${themeData.colors.primary}`"></div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">Primary</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="h-12 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
|
||||
:style="`background-color: ${themeData.colors.secondary}`"></div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">Secondary</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="h-12 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
|
||||
:style="`background-color: ${themeData.colors.accent}`"></div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">Accent</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typography Preview -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2">TYPOGRAPHY</p>
|
||||
<div class="space-y-2 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||
<p class="text-lg font-bold" :style="`font-family: ${themeData.fonts.heading}`">
|
||||
Heading Font
|
||||
</p>
|
||||
<p class="text-sm" :style="`font-family: ${themeData.fonts.body}`">
|
||||
This is body text font example. It will be used for paragraphs and descriptions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button Preview -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2">BUTTONS</p>
|
||||
<button class="px-4 py-2 text-sm font-medium text-white rounded-lg w-full shadow-sm"
|
||||
:style="`background-color: ${themeData.colors.primary}`">
|
||||
Primary Button
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Layout Preview -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2">LAYOUT</p>
|
||||
<div class="text-xs space-y-1 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
<span class="font-semibold">Product Layout:</span>
|
||||
<span class="capitalize" x-text="themeData.layout.style"></span>
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
<span class="font-semibold">Header:</span>
|
||||
<span class="capitalize" x-text="themeData.layout.header"></span>
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
<span class="font-semibold">Product Card:</span>
|
||||
<span class="capitalize" x-text="themeData.layout.product_card"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Link -->
|
||||
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<a :href="`http://${store?.subdomain}.localhost:8000`"
|
||||
target="_blank"
|
||||
class="flex items-center justify-center px-4 py-2 text-sm font-medium text-purple-700 bg-purple-50 border border-purple-300 rounded-lg hover:bg-purple-100 dark:bg-purple-900 dark:bg-opacity-20 dark:text-purple-300 dark:border-purple-700 transition-colors">
|
||||
<span x-html="$icon('external-link', 'w-4 h-4 mr-2')"></span>
|
||||
View Live Shop
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Theme Confirmation Modal -->
|
||||
{{ confirm_modal(
|
||||
'resetThemeModal',
|
||||
'Reset Theme',
|
||||
'Are you sure you want to reset the theme to default? All customizations will be lost.',
|
||||
'resetTheme()',
|
||||
'showResetThemeModal',
|
||||
'Reset',
|
||||
'Cancel',
|
||||
'warning'
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer src="{{ url_for('cms_static', path='admin/js/store-theme.js') }}"></script>
|
||||
{% endblock %}
|
||||
129
app/modules/cms/templates/cms/admin/store-themes.html
Normal file
129
app/modules/cms/templates/cms/admin/store-themes.html
Normal file
@@ -0,0 +1,129 @@
|
||||
{# app/templates/admin/store-themes.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
|
||||
{% block title %}Store Themes{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css" rel="stylesheet">
|
||||
<style>
|
||||
.ts-wrapper { width: 100%; }
|
||||
.ts-control {
|
||||
background-color: rgb(249 250 251) !important;
|
||||
border-color: rgb(209 213 219) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
}
|
||||
.dark .ts-control {
|
||||
background-color: rgb(55 65 81) !important;
|
||||
border-color: rgb(75 85 99) !important;
|
||||
color: rgb(229 231 235) !important;
|
||||
}
|
||||
.ts-dropdown {
|
||||
border-radius: 0.5rem !important;
|
||||
border-color: rgb(209 213 219) !important;
|
||||
}
|
||||
.dark .ts-dropdown {
|
||||
background-color: rgb(55 65 81) !important;
|
||||
border-color: rgb(75 85 99) !important;
|
||||
}
|
||||
.dark .ts-dropdown .option {
|
||||
color: rgb(229 231 235) !important;
|
||||
}
|
||||
.dark .ts-dropdown .option.active {
|
||||
background-color: rgb(75 85 99) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminStoreThemes(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Store Themes', subtitle='Customize store theme colors and branding') }}
|
||||
|
||||
<!-- Selected Store Display (when filtered) -->
|
||||
<div x-show="selectedStore" x-cloak class="mb-6">
|
||||
<div class="px-4 py-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span x-html="$icon('color-swatch', 'w-6 h-6 text-purple-600')"></span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Filtered by Store</p>
|
||||
<p class="text-lg font-semibold text-purple-900 dark:text-purple-100" x-text="selectedStore?.name"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="clearStoreFilter()"
|
||||
class="flex items-center gap-1 px-3 py-1.5 text-sm text-purple-700 dark:text-purple-300 bg-purple-100 dark:bg-purple-800 rounded-md hover:bg-purple-200 dark:hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<span x-html="$icon('x-mark', 'w-4 h-4')"></span>
|
||||
Clear Filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Store Search/Filter -->
|
||||
<div class="px-4 py-6 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Search Store
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Search for a store to customize their theme
|
||||
</p>
|
||||
|
||||
<div class="max-w-md">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Store
|
||||
</label>
|
||||
<select x-ref="storeSelect" placeholder="Search store by name or code..."></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ loading_state('Loading stores...') }}
|
||||
|
||||
{{ error_state('Error loading stores') }}
|
||||
|
||||
<!-- Stores List -->
|
||||
<div x-show="!loading && filteredStores.length > 0">
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
<span x-text="selectedStore ? 'Selected Store' : 'All Stores'"></span>
|
||||
<span class="text-sm font-normal text-gray-500 dark:text-gray-400" x-text="`(${filteredStores.length})`"></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<template x-for="store in filteredStores" :key="store.store_code">
|
||||
<a
|
||||
:href="`/admin/stores/${store.store_code}/theme`"
|
||||
class="block p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 dark:hover:border-purple-500 hover:shadow-md transition-all"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-semibold text-gray-700 dark:text-gray-200" x-text="store.name"></h4>
|
||||
<span x-html="$icon('color-swatch', 'w-5 h-5 text-purple-600')"></span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="store.store_code"></p>
|
||||
<div class="mt-3 flex items-center text-xs text-purple-600 dark:text-purple-400">
|
||||
<span>Customize theme</span>
|
||||
<span x-html="$icon('chevron-right', 'w-4 h-4 ml-1')"></span>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div x-show="!loading && filteredStores.length === 0" class="text-center py-12">
|
||||
<span x-html="$icon('shopping-bag', 'inline w-12 h-12 text-gray-400 mb-4')"></span>
|
||||
<p class="text-gray-600 dark:text-gray-400">No stores found</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
|
||||
<script defer src="{{ url_for('cms_static', path='admin/js/store-themes.js') }}"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user