feat: complete dynamic menu system across all frontends
All checks were successful
All checks were successful
- Add "Merchant Frontend" tab to admin menu-config page - Merchant render endpoint now respects AdminMenuConfig visibility via get_merchant_primary_platform_id() platform resolution - New store menu render endpoint (GET /store/core/menu/render/store) with platform-scoped visibility and store_code interpolation - Store sidebar migrated from hardcoded Jinja2 macros to dynamic Alpine.js x-for rendering with loading skeleton and fallback - Store init-alpine.js: add loadMenuConfig(), expandSectionForCurrentPage() - Include store page route fixes, login template updates, and tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,60 +1,5 @@
|
||||
{# app/templates/store/partials/sidebar.html #}
|
||||
{# Collapsible sidebar sections with localStorage persistence - matching admin pattern #}
|
||||
|
||||
{# ============================================================================
|
||||
REUSABLE MACROS FOR SIDEBAR ITEMS
|
||||
============================================================================ #}
|
||||
|
||||
{# Macro for collapsible section header #}
|
||||
{% macro section_header(title, section_key, icon=none) %}
|
||||
<div class="px-6 my-4">
|
||||
<hr class="border-gray-200 dark:border-gray-700" />
|
||||
</div>
|
||||
<button
|
||||
@click="toggleSection('{{ section_key }}')"
|
||||
class="flex items-center justify-between w-full px-6 py-2 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
{% if icon %}
|
||||
<span x-html="$icon('{{ icon }}', 'w-4 h-4 mr-2 text-gray-400')"></span>
|
||||
{% endif %}
|
||||
{{ title }}
|
||||
</span>
|
||||
<span
|
||||
x-html="$icon('chevron-down', 'w-4 h-4 transition-transform duration-200')"
|
||||
:class="{ 'rotate-180': openSections.{{ section_key }} }"
|
||||
></span>
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
{# Macro for collapsible section content wrapper #}
|
||||
{% macro section_content(section_key) %}
|
||||
<ul
|
||||
x-show="openSections.{{ section_key }}"
|
||||
x-transition:enter="transition-all duration-200 ease-out"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition-all duration-150 ease-in"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2"
|
||||
class="mt-1 overflow-hidden"
|
||||
>
|
||||
{{ caller() }}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
{# Macro for menu item - uses storeCode for dynamic URLs #}
|
||||
{% macro menu_item(page_id, path, icon, label) %}
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === '{{ page_id }}'" 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 === '{{ page_id }}' ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
:href="`/store/${storeCode}/{{ path }}`">
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5')"></span>
|
||||
<span class="ml-4">{{ label }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
{# Dynamic sidebar driven by menu discovery API - matching merchant/admin pattern #}
|
||||
|
||||
{# ============================================================================
|
||||
SIDEBAR CONTENT (shared between desktop and mobile)
|
||||
@@ -69,57 +14,94 @@
|
||||
<span x-text="store?.name || 'Store Portal'"></span>
|
||||
</a>
|
||||
|
||||
<!-- Dashboard (always visible) -->
|
||||
<ul class="mt-6">
|
||||
{{ menu_item('dashboard', 'dashboard', 'home', 'Dashboard') }}
|
||||
{{ menu_item('analytics', 'analytics', 'chart-bar', 'Analytics') }}
|
||||
</ul>
|
||||
<!-- Loading skeleton -->
|
||||
<div x-show="menuLoading" class="mt-6 px-6 space-y-4 animate-pulse">
|
||||
<div class="h-8 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||
<div class="h-8 bg-gray-200 dark:bg-gray-700 rounded w-full"></div>
|
||||
<div class="h-8 bg-gray-200 dark:bg-gray-700 rounded w-5/6"></div>
|
||||
</div>
|
||||
|
||||
<!-- Products & Inventory Section -->
|
||||
{{ section_header('Products & Inventory', 'products', 'cube') }}
|
||||
{% call section_content('products') %}
|
||||
{{ menu_item('products', 'products', 'shopping-bag', 'All Products') }}
|
||||
{{ menu_item('inventory', 'inventory', 'clipboard-list', 'Inventory') }}
|
||||
{{ menu_item('marketplace', 'marketplace', 'download', 'Marketplace Import') }}
|
||||
{% endcall %}
|
||||
<!-- Dynamic menu from API -->
|
||||
<div x-show="!menuLoading && menuData" x-cloak>
|
||||
<template x-for="section in (menuData?.sections || [])" :key="section.id">
|
||||
<div>
|
||||
{# Unlabeled sections (e.g. "main" with dashboard) render items directly #}
|
||||
<template x-if="!section.label">
|
||||
<ul class="mt-6">
|
||||
<template x-for="item in section.items" :key="item.id">
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === item.id" 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 === item.id ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
:href="item.url">
|
||||
<span x-html="$icon(item.icon, 'w-5 h-5')"></span>
|
||||
<span class="ml-4" x-text="item.label"></span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<!-- Sales & Orders Section -->
|
||||
{{ section_header('Sales & Orders', 'sales', 'shopping-cart') }}
|
||||
{% call section_content('sales') %}
|
||||
{{ menu_item('orders', 'orders', 'document-text', 'Orders') }}
|
||||
{{ menu_item('letzshop', 'letzshop', 'external-link', 'Letzshop Orders') }}
|
||||
{{ menu_item('invoices', 'invoices', 'currency-euro', 'Invoices') }}
|
||||
{% endcall %}
|
||||
{# Labeled sections are collapsible #}
|
||||
<template x-if="section.label">
|
||||
<div>
|
||||
<div class="px-6 my-4">
|
||||
<hr class="border-gray-200 dark:border-gray-700" />
|
||||
</div>
|
||||
<button
|
||||
@click="toggleSection(section.id)"
|
||||
class="flex items-center justify-between w-full px-6 py-2 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<span class="flex items-center" x-text="section.label"></span>
|
||||
<span
|
||||
x-html="$icon('chevron-down', 'w-4 h-4 transition-transform duration-200')"
|
||||
:class="{ 'rotate-180': openSections[section.id] }"
|
||||
></span>
|
||||
</button>
|
||||
<ul
|
||||
x-show="openSections[section.id]"
|
||||
x-transition:enter="transition-all duration-200 ease-out"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition-all duration-150 ease-in"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2"
|
||||
class="mt-1 overflow-hidden"
|
||||
>
|
||||
<template x-for="item in section.items" :key="item.id">
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === item.id" 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 === item.id ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
:href="item.url">
|
||||
<span x-html="$icon(item.icon, 'w-5 h-5')"></span>
|
||||
<span class="ml-4" x-text="item.label"></span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Customers & Communication Section -->
|
||||
{{ section_header('Customers', 'customers', 'users') }}
|
||||
{% call section_content('customers') %}
|
||||
{{ menu_item('customers', 'customers', 'user-group', 'All Customers') }}
|
||||
{{ menu_item('messages', 'messages', 'chat-bubble-left-right', 'Messages') }}
|
||||
{{ menu_item('notifications', 'notifications', 'bell', 'Notifications') }}
|
||||
{% endcall %}
|
||||
<!-- Fallback: static dashboard link (if menu API fails) -->
|
||||
<div x-show="!menuLoading && !menuData" x-cloak>
|
||||
<ul class="mt-6">
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === 'dashboard'" 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 === 'dashboard' ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
:href="`/store/${storeCode}/dashboard`">
|
||||
<span x-html="$icon('home', 'w-5 h-5')"></span>
|
||||
<span class="ml-4">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Shop & Content Section -->
|
||||
{{ section_header('Shop & Content', 'shop', 'color-swatch') }}
|
||||
{% call section_content('shop') %}
|
||||
{{ menu_item('content-pages', 'content-pages', 'document-text', 'Content Pages') }}
|
||||
{{ menu_item('media', 'media', 'photograph', 'Media Library') }}
|
||||
{# Future: Theme customization, if enabled for store tier
|
||||
{{ menu_item('theme', 'theme', 'paint-brush', 'Theme') }}
|
||||
#}
|
||||
{% endcall %}
|
||||
|
||||
<!-- Account & Settings Section -->
|
||||
{{ section_header('Account & Settings', 'account', 'cog') }}
|
||||
{% call section_content('account') %}
|
||||
{{ menu_item('team', 'team', 'user-group', 'Team') }}
|
||||
{{ menu_item('profile', 'profile', 'user', 'Profile') }}
|
||||
{{ menu_item('billing', 'billing', 'credit-card', 'Billing') }}
|
||||
{{ menu_item('email-templates', 'email-templates', 'mail', 'Email Templates') }}
|
||||
{{ menu_item('settings', 'settings', 'adjustments', 'Settings') }}
|
||||
{% endcall %}
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<!-- Quick Actions (static, outside dynamic menu) -->
|
||||
<div class="px-6 my-6">
|
||||
<button class="flex items-center justify-between w-full px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple"
|
||||
@click="$dispatch('open-add-product-modal')">
|
||||
|
||||
Reference in New Issue
Block a user