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>
This commit is contained in:
@@ -17,25 +17,45 @@
|
||||
|
||||
<!-- 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">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||
<!-- Tabs -->
|
||||
{% call tabs_inline() %}
|
||||
{{ tab_button('all', 'All Pages', count_var='allPages.length') }}
|
||||
{{ tab_button('platform', 'Platform Defaults', count_var='platformPages.length') }}
|
||||
{{ tab_button('vendor', 'Vendor Overrides', count_var='vendorPages.length') }}
|
||||
{{ tab_button('platform_marketing', 'Platform Marketing', count_var='platformMarketingPages.length') }}
|
||||
{{ tab_button('vendor_defaults', 'Vendor Defaults', count_var='vendorDefaultPages.length') }}
|
||||
{{ tab_button('vendor_overrides', 'Vendor Overrides', count_var='vendorOverridePages.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"
|
||||
>
|
||||
<!-- Filters Row -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Platform Filter -->
|
||||
<div class="relative">
|
||||
<select
|
||||
x-model="selectedPlatform"
|
||||
class="pl-3 pr-8 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 appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="">All Platforms</option>
|
||||
<template x-for="platform in platforms" :key="platform.id">
|
||||
<option :value="platform.code" x-text="platform.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<span x-html="$icon('chevron-down', 'w-4 h-4 text-gray-400')"></span>
|
||||
</span>
|
||||
</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>
|
||||
</div>
|
||||
@@ -73,12 +93,18 @@
|
||||
<code class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="'/' + page.slug"></code>
|
||||
</td>
|
||||
|
||||
<!-- Type -->
|
||||
<!-- Type (Three-Tier System) -->
|
||||
<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'"
|
||||
:class="getPageTierClass(page)"
|
||||
x-text="getPageTierLabel(page)"
|
||||
></span>
|
||||
<!-- Platform badge -->
|
||||
<span
|
||||
x-show="page.platform_name"
|
||||
class="ml-1 px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400 rounded"
|
||||
x-text="page.platform_name"
|
||||
></span>
|
||||
</td>
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<!-- Content Management Section -->
|
||||
{{ section_header('Content Management', 'contentMgmt') }}
|
||||
{% call section_content('contentMgmt') %}
|
||||
{{ menu_item('platforms', '/admin/platforms', 'globe-alt', 'Platforms') }}
|
||||
{{ menu_item('platform-homepage', '/admin/platform-homepage', 'home', 'Platform Homepage') }}
|
||||
{{ menu_item('content-pages', '/admin/content-pages', 'document-text', 'Content Pages') }}
|
||||
{{ menu_item('vendor-theme', '/admin/vendor-themes', 'color-swatch', 'Vendor Themes') }}
|
||||
|
||||
154
app/templates/admin/platforms.html
Normal file
154
app/templates/admin/platforms.html
Normal file
@@ -0,0 +1,154 @@
|
||||
{# app/templates/admin/platforms.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 %}Platforms{% endblock %}
|
||||
|
||||
{% block alpine_data %}platformsManager(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Platforms', subtitle='Manage platform configurations for OMS, Loyalty, and other business offerings') }}
|
||||
|
||||
{{ loading_state('Loading platforms...') }}
|
||||
|
||||
{{ error_state('Error loading platforms') }}
|
||||
|
||||
<!-- Platforms Grid -->
|
||||
<div x-show="!loading && platforms.length > 0" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-3">
|
||||
<template x-for="platform in platforms" :key="platform.id">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<!-- Platform Header -->
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<!-- Platform Icon -->
|
||||
<div class="flex items-center justify-center w-12 h-12 rounded-lg bg-purple-100 dark:bg-purple-900/50">
|
||||
<span x-html="$icon(getPlatformIcon(platform.code), 'w-6 h-6 text-purple-600 dark:text-purple-400')"></span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white" x-text="platform.name"></h3>
|
||||
<code class="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded" x-text="platform.code"></code>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status Badge -->
|
||||
<span
|
||||
class="px-3 py-1 text-xs font-semibold rounded-full"
|
||||
:class="platform.is_active ? '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="platform.is_active ? 'Active' : 'Inactive'"
|
||||
></span>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-gray-600 dark:text-gray-400" x-text="platform.description || 'No description'"></p>
|
||||
</div>
|
||||
|
||||
<!-- Platform Stats -->
|
||||
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-800/50">
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-purple-600 dark:text-purple-400" x-text="platform.vendor_count"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Vendors</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400" x-text="platform.platform_pages_count"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Marketing Pages</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-teal-600 dark:text-teal-400" x-text="platform.vendor_defaults_count"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Vendor Defaults</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Platform Info -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between" x-show="platform.domain">
|
||||
<span class="text-gray-500 dark:text-gray-400">Domain:</span>
|
||||
<span class="text-gray-900 dark:text-white" x-text="platform.domain"></span>
|
||||
</div>
|
||||
<div class="flex justify-between" x-show="platform.path_prefix">
|
||||
<span class="text-gray-500 dark:text-gray-400">Path Prefix:</span>
|
||||
<code class="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded" x-text="platform.path_prefix"></code>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Language:</span>
|
||||
<span class="text-gray-900 dark:text-white" x-text="platform.default_language.toUpperCase()"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Platform Actions -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
||||
<div class="flex justify-between items-center">
|
||||
<a
|
||||
:href="`/admin/platforms/${platform.code}`"
|
||||
class="inline-flex items-center text-sm font-medium text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-4 h-4 mr-1')"></span>
|
||||
View Details
|
||||
</a>
|
||||
<div class="flex space-x-2">
|
||||
<a
|
||||
:href="`/admin/content-pages?platform=${platform.code}`"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
|
||||
title="View content pages for this platform"
|
||||
>
|
||||
<span x-html="$icon('document-text', 'w-4 h-4 mr-1')"></span>
|
||||
Pages
|
||||
</a>
|
||||
<a
|
||||
:href="`/admin/platforms/${platform.code}/edit`"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
|
||||
title="Edit platform settings"
|
||||
>
|
||||
<span x-html="$icon('edit', 'w-4 h-4 mr-1')"></span>
|
||||
Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div x-show="!loading && platforms.length === 0" class="text-center py-12 bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<span x-html="$icon('globe-alt', '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 platforms found</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">
|
||||
No platforms have been configured yet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Page Tier Legend -->
|
||||
<div x-show="!loading && platforms.length > 0" class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-4">Content Page Tiers</h4>
|
||||
<div class="grid md:grid-cols-3 gap-6">
|
||||
<div class="flex items-start">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-blue-500 mt-1.5 mr-3"></span>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">Platform Marketing Pages</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Public pages like homepage, pricing, features. Not inherited by vendors.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-teal-500 mt-1.5 mr-3"></span>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">Vendor Defaults</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Default pages inherited by all vendors (about, terms, privacy).</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-purple-500 mt-1.5 mr-3"></span>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">Vendor Overrides</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Custom pages created by individual vendors.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='admin/js/platforms.js') }}"></script>
|
||||
{% endblock %}
|
||||
75
app/templates/vendor/content-page-edit.html
vendored
75
app/templates/vendor/content-page-edit.html
vendored
@@ -41,14 +41,73 @@
|
||||
|
||||
<!-- Override Info Banner -->
|
||||
<div x-show="!loading && isOverride" class="mb-6 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-4">
|
||||
<div class="flex">
|
||||
<span x-html="$icon('information-circle', 'w-5 h-5 text-purple-500 mr-3 flex-shrink-0 mt-0.5')"></span>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-purple-800 dark:text-purple-200">Overriding Platform Default</h4>
|
||||
<p class="text-sm text-purple-700 dark:text-purple-300 mt-1">
|
||||
You're customizing the "<span x-text="form.title"></span>" page. Your version will be shown to customers instead of the platform default.
|
||||
<button @click="deletePage()" class="underline hover:no-underline ml-1">Revert to default</button>
|
||||
</p>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex">
|
||||
<span x-html="$icon('information-circle', 'w-5 h-5 text-purple-500 mr-3 flex-shrink-0 mt-0.5')"></span>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-purple-800 dark:text-purple-200">Overriding Platform Default</h4>
|
||||
<p class="text-sm text-purple-700 dark:text-purple-300 mt-1">
|
||||
You're customizing the "<span x-text="form.title"></span>" page. Your version will be shown to customers instead of the platform default.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-4 flex-shrink-0">
|
||||
<button
|
||||
@click="showDefaultPreview()"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-purple-700 bg-white dark:bg-purple-900/50 dark:text-purple-300 border border-purple-300 dark:border-purple-700 rounded-lg hover:bg-purple-50 dark:hover:bg-purple-900 transition-colors"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-4 h-4 mr-1')"></span>
|
||||
View Default
|
||||
</button>
|
||||
<button
|
||||
@click="deletePage()"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-red-700 bg-white dark:bg-red-900/50 dark:text-red-300 border border-red-300 dark:border-red-700 rounded-lg hover:bg-red-50 dark:hover:bg-red-900 transition-colors"
|
||||
>
|
||||
<span x-html="$icon('arrow-uturn-left', 'w-4 h-4 mr-1')"></span>
|
||||
Revert to Default
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default Content Preview Modal -->
|
||||
<div
|
||||
x-show="showingDefaultPreview"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
||||
@click.self="showingDefaultPreview = false"
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full mx-4 max-h-[80vh] overflow-hidden">
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Platform Default Content</h3>
|
||||
<button @click="showingDefaultPreview = false" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
|
||||
<span x-html="$icon('x-mark', 'w-6 h-6')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto max-h-[60vh]">
|
||||
<div x-show="loadingDefault" class="text-center py-8">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-sm text-gray-500">Loading default content...</p>
|
||||
</div>
|
||||
<div x-show="!loadingDefault && defaultContent">
|
||||
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Title</h4>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white mb-4" x-text="defaultContent?.title"></p>
|
||||
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Content</h4>
|
||||
<div class="prose dark:prose-invert max-w-none bg-gray-50 dark:bg-gray-700 rounded-lg p-4" x-html="defaultContent?.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<button
|
||||
@click="showingDefaultPreview = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 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"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
44
app/templates/vendor/content-pages.html
vendored
44
app/templates/vendor/content-pages.html
vendored
@@ -15,6 +15,50 @@
|
||||
|
||||
{{ 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">
|
||||
|
||||
Reference in New Issue
Block a user