frontend migration to jinja, alpine.js
This commit is contained in:
@@ -32,8 +32,9 @@ function adminComponents() {
|
|||||||
{ id: 'cards', name: 'Cards', icon: 'collection' },
|
{ id: 'cards', name: 'Cards', icon: 'collection' },
|
||||||
{ id: 'badges', name: 'Badges', icon: 'tag' },
|
{ id: 'badges', name: 'Badges', icon: 'tag' },
|
||||||
{ id: 'tables', name: 'Tables', icon: 'table' },
|
{ id: 'tables', name: 'Tables', icon: 'table' },
|
||||||
{ id: 'modals', name: 'Modals', icon: 'window' },
|
{ id: 'modals', name: 'Modals', icon: 'view-grid-add' },
|
||||||
{ id: 'alerts', name: 'Alerts', icon: 'exclamation' }
|
{ id: 'alerts', name: 'Alerts', icon: 'exclamation' },
|
||||||
|
{ id: 'charts', name: 'Charts', icon: 'chart-pie' }
|
||||||
],
|
],
|
||||||
|
|
||||||
// Sample form data for examples
|
// Sample form data for examples
|
||||||
@@ -53,6 +54,10 @@ function adminComponents() {
|
|||||||
required: 'This field is required'
|
required: 'This field is required'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Modal state variables for examples
|
||||||
|
showExampleModal: false,
|
||||||
|
showFormModal: false,
|
||||||
|
|
||||||
// ✅ CRITICAL: Proper initialization with guard
|
// ✅ CRITICAL: Proper initialization with guard
|
||||||
async init() {
|
async init() {
|
||||||
componentsLog.info('=== COMPONENTS PAGE INITIALIZING ===');
|
componentsLog.info('=== COMPONENTS PAGE INITIALIZING ===');
|
||||||
@@ -72,6 +77,11 @@ function adminComponents() {
|
|||||||
this.setActiveSectionFromHash();
|
this.setActiveSectionFromHash();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize charts after DOM is ready
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.initializeCharts();
|
||||||
|
});
|
||||||
|
|
||||||
componentsLog.info('=== COMPONENTS PAGE INITIALIZATION COMPLETE ===');
|
componentsLog.info('=== COMPONENTS PAGE INITIALIZATION COMPLETE ===');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -114,11 +124,18 @@ function adminComponents() {
|
|||||||
async copyCode(code) {
|
async copyCode(code) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(code);
|
await navigator.clipboard.writeText(code);
|
||||||
Utils.showToast('Code copied to clipboard!', 'success');
|
// Use the global Utils.showToast function
|
||||||
|
if (typeof Utils !== 'undefined' && Utils.showToast) {
|
||||||
|
Utils.showToast('Code copied to clipboard!', 'success');
|
||||||
|
} else {
|
||||||
|
componentsLog.warn('Utils.showToast not available');
|
||||||
|
}
|
||||||
componentsLog.debug('Code copied to clipboard');
|
componentsLog.debug('Code copied to clipboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
componentsLog.error('Failed to copy code:', error);
|
componentsLog.error('Failed to copy code:', error);
|
||||||
Utils.showToast('Failed to copy code', 'error');
|
if (typeof Utils !== 'undefined' && Utils.showToast) {
|
||||||
|
Utils.showToast('Failed to copy code', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -132,7 +149,145 @@ function adminComponents() {
|
|||||||
warning: 'Please review your input.',
|
warning: 'Please review your input.',
|
||||||
info: 'Here is some information.'
|
info: 'Here is some information.'
|
||||||
};
|
};
|
||||||
Utils.showToast(messages[type] || messages.info, type);
|
|
||||||
|
if (typeof Utils !== 'undefined' && Utils.showToast) {
|
||||||
|
Utils.showToast(messages[type] || messages.info, type);
|
||||||
|
} else {
|
||||||
|
componentsLog.error('Utils.showToast not available');
|
||||||
|
alert(messages[type] || messages.info); // Fallback to alert
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Chart.js charts
|
||||||
|
*/
|
||||||
|
initializeCharts() {
|
||||||
|
try {
|
||||||
|
// Check if Chart.js is loaded
|
||||||
|
if (typeof Chart === 'undefined') {
|
||||||
|
componentsLog.warn('Chart.js not loaded, skipping chart initialization');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentsLog.info('Initializing charts...');
|
||||||
|
|
||||||
|
// Pie Chart
|
||||||
|
const pieCanvas = document.getElementById('examplePieChart');
|
||||||
|
if (pieCanvas) {
|
||||||
|
const pieConfig = {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
datasets: [{
|
||||||
|
data: [33, 33, 33],
|
||||||
|
backgroundColor: ['#0694a2', '#7e3af2', '#1c64f2'],
|
||||||
|
label: 'Dataset 1',
|
||||||
|
}],
|
||||||
|
labels: ['Shoes', 'Shirts', 'Bags'],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
cutoutPercentage: 80,
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
new Chart(pieCanvas, pieConfig);
|
||||||
|
componentsLog.debug('Pie chart initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line Chart
|
||||||
|
const lineCanvas = document.getElementById('exampleLineChart');
|
||||||
|
if (lineCanvas) {
|
||||||
|
const lineConfig = {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Organic',
|
||||||
|
backgroundColor: '#0694a2',
|
||||||
|
borderColor: '#0694a2',
|
||||||
|
data: [43, 48, 40, 54, 67, 73, 70],
|
||||||
|
fill: false,
|
||||||
|
}, {
|
||||||
|
label: 'Paid',
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: '#7e3af2',
|
||||||
|
borderColor: '#7e3af2',
|
||||||
|
data: [24, 50, 64, 74, 52, 51, 65],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'nearest',
|
||||||
|
intersect: true,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: true,
|
||||||
|
labelString: 'Month',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: true,
|
||||||
|
labelString: 'Value',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
new Chart(lineCanvas, lineConfig);
|
||||||
|
componentsLog.debug('Line chart initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bar Chart
|
||||||
|
const barCanvas = document.getElementById('exampleBarChart');
|
||||||
|
if (barCanvas) {
|
||||||
|
const barConfig = {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Shoes',
|
||||||
|
backgroundColor: '#0694a2',
|
||||||
|
borderColor: '#0694a2',
|
||||||
|
borderWidth: 1,
|
||||||
|
data: [43, 48, 40, 54, 67, 73, 70],
|
||||||
|
}, {
|
||||||
|
label: 'Bags',
|
||||||
|
backgroundColor: '#7e3af2',
|
||||||
|
borderColor: '#7e3af2',
|
||||||
|
borderWidth: 1,
|
||||||
|
data: [24, 50, 64, 74, 52, 51, 65],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
new Chart(barCanvas, barConfig);
|
||||||
|
componentsLog.debug('Bar chart initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
componentsLog.info('All charts initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
componentsLog.error('Error initializing charts:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
278
static/admin/js/vendor-theme.js
Normal file
278
static/admin/js/vendor-theme.js
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
// static/admin/js/vendor-theme.js
|
||||||
|
/**
|
||||||
|
* Vendor Theme Management Component
|
||||||
|
* Follows the established Alpine.js pattern from FRONTEND_ALPINE_PAGE_TEMPLATE.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
const THEME_LOG_LEVEL = 3;
|
||||||
|
|
||||||
|
const themeLog = {
|
||||||
|
error: (...args) => THEME_LOG_LEVEL >= 1 && console.error('❌ [THEME ERROR]', ...args),
|
||||||
|
warn: (...args) => THEME_LOG_LEVEL >= 2 && console.warn('⚠️ [THEME WARN]', ...args),
|
||||||
|
info: (...args) => THEME_LOG_LEVEL >= 3 && console.info('ℹ️ [THEME INFO]', ...args),
|
||||||
|
debug: (...args) => THEME_LOG_LEVEL >= 4 && console.log('🔍 [THEME DEBUG]', ...args)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Theme presets
|
||||||
|
const THEME_PRESETS = {
|
||||||
|
modern: {
|
||||||
|
colors: {
|
||||||
|
primary: "#6366f1",
|
||||||
|
secondary: "#8b5cf6",
|
||||||
|
accent: "#ec4899"
|
||||||
|
},
|
||||||
|
fonts: {
|
||||||
|
heading: "Inter, sans-serif",
|
||||||
|
body: "Inter, sans-serif"
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
style: "grid",
|
||||||
|
header: "fixed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
classic: {
|
||||||
|
colors: {
|
||||||
|
primary: "#1e40af",
|
||||||
|
secondary: "#7c3aed",
|
||||||
|
accent: "#dc2626"
|
||||||
|
},
|
||||||
|
fonts: {
|
||||||
|
heading: "Georgia, serif",
|
||||||
|
body: "Arial, sans-serif"
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
style: "list",
|
||||||
|
header: "static"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
minimal: {
|
||||||
|
colors: {
|
||||||
|
primary: "#000000",
|
||||||
|
secondary: "#404040",
|
||||||
|
accent: "#666666"
|
||||||
|
},
|
||||||
|
fonts: {
|
||||||
|
heading: "Helvetica, sans-serif",
|
||||||
|
body: "Helvetica, sans-serif"
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
style: "grid",
|
||||||
|
header: "transparent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
vibrant: {
|
||||||
|
colors: {
|
||||||
|
primary: "#f59e0b",
|
||||||
|
secondary: "#ef4444",
|
||||||
|
accent: "#8b5cf6"
|
||||||
|
},
|
||||||
|
fonts: {
|
||||||
|
heading: "Poppins, sans-serif",
|
||||||
|
body: "Open Sans, sans-serif"
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
style: "masonry",
|
||||||
|
header: "fixed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function vendorThemeData() {
|
||||||
|
return {
|
||||||
|
// ✅ CRITICAL: Inherit base layout functionality
|
||||||
|
...data(),
|
||||||
|
|
||||||
|
// ✅ CRITICAL: Set page identifier
|
||||||
|
currentPage: 'vendor-theme',
|
||||||
|
|
||||||
|
// Page state
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
vendor: null,
|
||||||
|
vendorCode: window.location.pathname.split('/')[3], // Extract from /admin/vendors/{code}/theme
|
||||||
|
|
||||||
|
// Theme data
|
||||||
|
themeData: {
|
||||||
|
colors: {
|
||||||
|
primary: "#6366f1",
|
||||||
|
secondary: "#8b5cf6",
|
||||||
|
accent: "#ec4899"
|
||||||
|
},
|
||||||
|
fonts: {
|
||||||
|
heading: "Inter, sans-serif",
|
||||||
|
body: "Inter, sans-serif"
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
style: "grid",
|
||||||
|
header: "fixed"
|
||||||
|
},
|
||||||
|
custom_css: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
originalTheme: null, // For detecting changes
|
||||||
|
|
||||||
|
// ✅ CRITICAL: Proper initialization with guard
|
||||||
|
async init() {
|
||||||
|
themeLog.info('=== VENDOR THEME PAGE INITIALIZING ===');
|
||||||
|
|
||||||
|
// Prevent multiple initializations
|
||||||
|
if (window._vendorThemeInitialized) {
|
||||||
|
themeLog.warn('Page already initialized, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window._vendorThemeInitialized = true;
|
||||||
|
|
||||||
|
// Load data
|
||||||
|
await this.loadVendor();
|
||||||
|
await this.loadTheme();
|
||||||
|
|
||||||
|
themeLog.info('=== VENDOR THEME PAGE INITIALIZATION COMPLETE ===');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load vendor info
|
||||||
|
async loadVendor() {
|
||||||
|
themeLog.info('Loading vendor:', this.vendorCode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ✅ CRITICAL: Use lowercase apiClient
|
||||||
|
const response = await apiClient.get(`/api/v1/admin/vendors/${this.vendorCode}`);
|
||||||
|
this.vendor = response;
|
||||||
|
themeLog.info('Vendor loaded:', this.vendor.name);
|
||||||
|
} catch (error) {
|
||||||
|
themeLog.error('Failed to load vendor:', error);
|
||||||
|
Utils.showToast('Failed to load vendor', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load theme configuration
|
||||||
|
async loadTheme() {
|
||||||
|
themeLog.info('Loading theme...');
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Get vendor's theme config from vendor object
|
||||||
|
if (this.vendor && this.vendor.theme_config) {
|
||||||
|
this.themeData = {
|
||||||
|
colors: this.vendor.theme_config.colors || this.themeData.colors,
|
||||||
|
fonts: this.vendor.theme_config.fonts || this.themeData.fonts,
|
||||||
|
layout: this.vendor.theme_config.layout || this.themeData.layout,
|
||||||
|
custom_css: this.vendor.theme_config.custom_css || ""
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
themeLog.info('No theme config found, using defaults');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store original for change detection
|
||||||
|
this.originalTheme = JSON.parse(JSON.stringify(this.themeData));
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
themeLog.info(`Theme loaded in ${duration}ms`, this.themeData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
themeLog.error('Failed to load theme:', error);
|
||||||
|
Utils.showToast('Failed to load theme', 'error');
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save theme configuration
|
||||||
|
async saveTheme() {
|
||||||
|
themeLog.info('Saving theme...');
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Update vendor with new theme_config
|
||||||
|
const updateData = {
|
||||||
|
theme_config: this.themeData
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await apiClient.put(
|
||||||
|
`/api/v1/admin/vendors/${this.vendorCode}`,
|
||||||
|
updateData
|
||||||
|
);
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
themeLog.info(`Theme saved in ${duration}ms`);
|
||||||
|
|
||||||
|
// Update vendor data
|
||||||
|
this.vendor = response;
|
||||||
|
this.originalTheme = JSON.parse(JSON.stringify(this.themeData));
|
||||||
|
|
||||||
|
Utils.showToast('Theme saved successfully', 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
themeLog.error('Failed to save theme:', error);
|
||||||
|
Utils.showToast('Failed to save theme', 'error');
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Apply preset theme
|
||||||
|
applyPreset(presetName) {
|
||||||
|
themeLog.info('Applying preset:', presetName);
|
||||||
|
|
||||||
|
if (!THEME_PRESETS[presetName]) {
|
||||||
|
themeLog.error('Unknown preset:', presetName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preset = THEME_PRESETS[presetName];
|
||||||
|
|
||||||
|
// Apply preset values
|
||||||
|
this.themeData.colors = { ...preset.colors };
|
||||||
|
this.themeData.fonts = { ...preset.fonts };
|
||||||
|
this.themeData.layout = { ...preset.layout };
|
||||||
|
|
||||||
|
Utils.showToast(`Applied ${presetName} theme preset`, 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reset to default theme
|
||||||
|
resetToDefault() {
|
||||||
|
themeLog.info('Resetting to default theme');
|
||||||
|
|
||||||
|
// Confirm with user
|
||||||
|
if (!confirm('Are you sure you want to reset to the default theme? This will discard all customizations.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.themeData = {
|
||||||
|
colors: {
|
||||||
|
primary: "#6366f1",
|
||||||
|
secondary: "#8b5cf6",
|
||||||
|
accent: "#ec4899"
|
||||||
|
},
|
||||||
|
fonts: {
|
||||||
|
heading: "Inter, sans-serif",
|
||||||
|
body: "Inter, sans-serif"
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
style: "grid",
|
||||||
|
header: "fixed"
|
||||||
|
},
|
||||||
|
custom_css: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
Utils.showToast('Theme reset to default', 'info');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if theme has unsaved changes
|
||||||
|
hasChanges() {
|
||||||
|
if (!this.originalTheme) return false;
|
||||||
|
return JSON.stringify(this.themeData) !== JSON.stringify(this.originalTheme);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Format date helper
|
||||||
|
formatDate(dateString) {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
return Utils.formatDate(dateString);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
themeLog.info('Vendor theme module loaded');
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<!-- static/admin/partials/base-layout.html -->
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html :class="{ 'theme-dark': dark }" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title data-page-title>Admin Panel - Multi-Tenant Platform</title>
|
|
||||||
|
|
||||||
<!-- Fonts -->
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
|
||||||
|
|
||||||
<!-- Tailwind CSS -->
|
|
||||||
<link rel="stylesheet" href="/static/admin/css/tailwind.output.css" />
|
|
||||||
|
|
||||||
<!-- Alpine Cloak -->
|
|
||||||
<style>
|
|
||||||
[x-cloak] { display: none !important; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- Page-specific styles slot -->
|
|
||||||
<slot name="head"></slot>
|
|
||||||
</head>
|
|
||||||
<body x-cloak>
|
|
||||||
<div class="flex h-screen bg-gray-50 dark:bg-gray-900" :class="{ 'overflow-hidden': isSideMenuOpen }">
|
|
||||||
<!-- Sidebar Container -->
|
|
||||||
<div id="sidebar-container"></div>
|
|
||||||
|
|
||||||
<div class="flex flex-col flex-1 w-full">
|
|
||||||
<!-- Header Container -->
|
|
||||||
<div id="header-container"></div>
|
|
||||||
|
|
||||||
<!-- Main Content Area (Child pages inject content here) -->
|
|
||||||
<main class="h-full overflow-y-auto">
|
|
||||||
<div class="container px-6 mx-auto grid">
|
|
||||||
<!-- Page content slot -->
|
|
||||||
<slot name="content"></slot>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Core Scripts (loaded for all pages) -->
|
|
||||||
<!-- 1. Partial Loader -->
|
|
||||||
<script src="/static/shared/js/partial-loader.js"></script>
|
|
||||||
|
|
||||||
<!-- 2. Icons Helper -->
|
|
||||||
<script src="/static/shared/js/icons.js"></script>
|
|
||||||
|
|
||||||
<!-- 3. Load Header & Sidebar -->
|
|
||||||
<script>
|
|
||||||
(async () => {
|
|
||||||
await window.partialLoader.loadAll({
|
|
||||||
'header-container': 'header.html',
|
|
||||||
'sidebar-container': 'sidebar.html'
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- 4. Base Alpine Data -->
|
|
||||||
<script src="/static/admin/js/init-alpine.js"></script>
|
|
||||||
|
|
||||||
<!-- 5. API Client & Utils -->
|
|
||||||
<script src="/static/shared/js/api-client.js"></script>
|
|
||||||
<script src="/static/shared/js/utils.js"></script>
|
|
||||||
|
|
||||||
<!-- 6. Alpine.js v3 -->
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Page-specific scripts slot -->
|
|
||||||
<slot name="scripts"></slot>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>User management</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<-- User management -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,498 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Edit Vendor - Admin Portal</title>
|
|
||||||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/admin/admin.css">
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body x-data="vendorEdit()" x-init="init()" x-cloak>
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="admin-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<h1>🔐 Admin Dashboard</h1>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<span class="user-info">Welcome, <strong x-text="currentUser.username"></strong></span>
|
|
||||||
<button class="btn-logout" @click="handleLogout">Logout</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Main Container -->
|
|
||||||
<div class="admin-container">
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<aside class="admin-sidebar">
|
|
||||||
<nav>
|
|
||||||
<ul class="nav-menu">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="/static/admin/dashboard.html" class="nav-link">
|
|
||||||
📊 Dashboard
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="/static/admin/dashboard.html#vendors" class="nav-link active">
|
|
||||||
🏪 Vendors
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="/static/admin/dashboard.html#users" class="nav-link">
|
|
||||||
👥 Users
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="/static/admin/dashboard.html#imports" class="nav-link">
|
|
||||||
📦 Import Jobs
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main class="admin-content">
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div x-show="loadingVendor" class="loading">
|
|
||||||
<span class="loading-spinner loading-spinner-lg"></span>
|
|
||||||
<p class="loading-text">Loading vendor details...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Edit Form -->
|
|
||||||
<div x-show="!loadingVendor && vendor">
|
|
||||||
<div class="content-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2 class="section-title">
|
|
||||||
Edit Vendor: <span x-text="vendor?.name"></span>
|
|
||||||
</h2>
|
|
||||||
<div>
|
|
||||||
<a href="/static/admin/dashboard.html#vendors" class="btn btn-secondary">
|
|
||||||
← Back to Vendor Management
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
|
||||||
<div class="quick-actions mb-3">
|
|
||||||
<button
|
|
||||||
@click="showVerificationModal()"
|
|
||||||
class="btn"
|
|
||||||
:class="vendor?.is_verified ? 'btn-warning' : 'btn-success'"
|
|
||||||
:disabled="saving">
|
|
||||||
<span x-text="vendor?.is_verified ? '❌ Unverify Vendor' : '✅ Verify Vendor'"></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="showStatusModal()"
|
|
||||||
class="btn"
|
|
||||||
:class="vendor?.is_active ? 'btn-danger' : 'btn-success'"
|
|
||||||
:disabled="saving">
|
|
||||||
<span x-text="vendor?.is_active ? '🔒 Deactivate Vendor' : '🔓 Activate Vendor'"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="handleSubmit">
|
|
||||||
<div class="form-grid">
|
|
||||||
<!-- Left Column -->
|
|
||||||
<div class="form-column">
|
|
||||||
<h3 class="form-section-title">Basic Information</h3>
|
|
||||||
|
|
||||||
<!-- Vendor Code (read-only) -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="vendorCode">Vendor Code</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="vendorCode"
|
|
||||||
name="vendor_code"
|
|
||||||
x-model="vendor.vendor_code"
|
|
||||||
disabled
|
|
||||||
class="form-control-disabled"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
<div class="form-help">Cannot be changed after creation</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Vendor Name -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="name">
|
|
||||||
Vendor Name <span class="required">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="name"
|
|
||||||
name="vendor_name"
|
|
||||||
x-model="formData.name"
|
|
||||||
:class="{ 'error': errors.name }"
|
|
||||||
required
|
|
||||||
maxlength="255"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="organization"
|
|
||||||
>
|
|
||||||
<div x-show="errors.name"
|
|
||||||
x-text="errors.name"
|
|
||||||
class="error-message show"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Subdomain -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="subdomain">
|
|
||||||
Subdomain <span class="required">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="subdomain"
|
|
||||||
name="subdomain"
|
|
||||||
x-model="formData.subdomain"
|
|
||||||
@input="formatSubdomain"
|
|
||||||
:class="{ 'error': errors.subdomain }"
|
|
||||||
required
|
|
||||||
maxlength="100"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
<div class="form-help">Lowercase letters, numbers, and hyphens only</div>
|
|
||||||
<div x-show="errors.subdomain"
|
|
||||||
x-text="errors.subdomain"
|
|
||||||
class="error-message show"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="description">Description</label>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
x-model="formData.description"
|
|
||||||
rows="3"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="off"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column -->
|
|
||||||
<div class="form-column">
|
|
||||||
<h3 class="form-section-title">Contact & Business Information</h3>
|
|
||||||
|
|
||||||
<!-- Owner Email (read-only with warning) -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="ownerEmail">Owner Email (Login)</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="ownerEmail"
|
|
||||||
name="owner_email"
|
|
||||||
x-model="vendor.owner_email"
|
|
||||||
disabled
|
|
||||||
class="form-control-disabled"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
<div class="form-help">
|
|
||||||
⚠️ Owner email cannot be changed here. Use "Transfer Ownership" below to change the owner.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Email (editable) -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="contactEmail">Business Contact Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="contactEmail"
|
|
||||||
name="contact_email"
|
|
||||||
x-model="formData.contact_email"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="email"
|
|
||||||
>
|
|
||||||
<div class="form-help">
|
|
||||||
Public business contact email (can be different from owner email)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Phone -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="contactPhone">Contact Phone</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
id="contactPhone"
|
|
||||||
name="contact_phone"
|
|
||||||
x-model="formData.contact_phone"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="tel"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Website -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="website">Website</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
id="website"
|
|
||||||
name="website"
|
|
||||||
x-model="formData.website"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="url"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Business Address -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="businessAddress">Business Address</label>
|
|
||||||
<textarea
|
|
||||||
id="businessAddress"
|
|
||||||
name="business_address"
|
|
||||||
x-model="formData.business_address"
|
|
||||||
rows="3"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="street-address"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tax Number -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="taxNumber">Tax Number</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="taxNumber"
|
|
||||||
name="tax_number"
|
|
||||||
x-model="formData.tax_number"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Transfer Ownership Section -->
|
|
||||||
<div class="form-section-divider"></div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<h3 class="form-section-title">⚠️ Change Vendor Owner</h3>
|
|
||||||
<p class="text-muted mb-2">
|
|
||||||
To change the owner to a different user account, use the Transfer Ownership feature.
|
|
||||||
This will assign ownership to another user and demote the current owner to Manager.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="showTransferOwnership = true"
|
|
||||||
class="btn btn-warning"
|
|
||||||
:disabled="saving">
|
|
||||||
🔄 Transfer Ownership to Different User
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
@click="window.location.href='/static/admin/dashboard.html#vendors'"
|
|
||||||
:disabled="saving">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit"
|
|
||||||
class="btn btn-primary"
|
|
||||||
:disabled="saving">
|
|
||||||
<span x-show="!saving">💾 Save Changes</span>
|
|
||||||
<span x-show="saving">
|
|
||||||
<span class="loading-spinner"></span>
|
|
||||||
Saving...
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Confirmation Modal (for verify/status changes) -->
|
|
||||||
<div x-show="confirmModal.show"
|
|
||||||
class="modal-overlay"
|
|
||||||
@click.self="confirmModal.onCancel ? confirmModal.onCancel() : (confirmModal.show = false)"
|
|
||||||
x-transition>
|
|
||||||
<div class="modal-content modal-sm">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3 x-text="confirmModal.title"></h3>
|
|
||||||
<button @click="confirmModal.onCancel ? confirmModal.onCancel() : (confirmModal.show = false)" class="btn-close">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<p x-text="confirmModal.message"></p>
|
|
||||||
|
|
||||||
<div x-show="confirmModal.warning" class="alert alert-warning mt-3" style="white-space: pre-line;">
|
|
||||||
<strong>⚠️ Warning:</strong><br>
|
|
||||||
<span x-text="confirmModal.warning"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button
|
|
||||||
@click="confirmModal.onCancel ? confirmModal.onCancel() : (confirmModal.show = false)"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
:disabled="saving">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="confirmModal.onConfirm(); confirmModal.show = false"
|
|
||||||
class="btn"
|
|
||||||
:class="confirmModal.buttonClass"
|
|
||||||
:disabled="saving">
|
|
||||||
<span x-show="!saving" x-text="confirmModal.buttonText"></span>
|
|
||||||
<span x-show="saving">
|
|
||||||
<span class="loading-spinner"></span>
|
|
||||||
Processing...
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Success Modal (for transfer ownership success) -->
|
|
||||||
<div x-show="successModal.show"
|
|
||||||
class="modal-overlay"
|
|
||||||
@click.self="successModal.show = false"
|
|
||||||
x-transition>
|
|
||||||
<div class="modal-content modal-md">
|
|
||||||
<div class="modal-header modal-header-success">
|
|
||||||
<h3>✅ <span x-text="successModal.title"></span></h3>
|
|
||||||
<button @click="successModal.show = false" class="btn-close">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="success-icon-wrapper">
|
|
||||||
<div class="success-icon">✓</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-center mb-4" x-text="successModal.message"></p>
|
|
||||||
|
|
||||||
<!-- Transfer Details -->
|
|
||||||
<div x-show="successModal.details" class="transfer-details">
|
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Previous Owner:</div>
|
|
||||||
<div class="detail-value">
|
|
||||||
<strong x-text="successModal.details?.oldOwner?.username"></strong>
|
|
||||||
<br>
|
|
||||||
<span class="text-muted" x-text="successModal.details?.oldOwner?.email"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="detail-arrow">→</div>
|
|
||||||
|
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">New Owner:</div>
|
|
||||||
<div class="detail-value">
|
|
||||||
<strong x-text="successModal.details?.newOwner?.username"></strong>
|
|
||||||
<br>
|
|
||||||
<span class="text-muted" x-text="successModal.details?.newOwner?.email"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-show="successModal.note" class="alert alert-info mt-3">
|
|
||||||
<strong>ℹ️ Note:</strong>
|
|
||||||
<span x-text="successModal.note"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button
|
|
||||||
@click="successModal.show = false"
|
|
||||||
class="btn btn-primary btn-block">
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Confirmation Modal (for verify/status changes) -->
|
|
||||||
|
|
||||||
<!-- Transfer Ownership Modal -->
|
|
||||||
<div x-show="showTransferOwnership"
|
|
||||||
class="modal-overlay"
|
|
||||||
@click.self="showTransferOwnership = false"
|
|
||||||
x-transition>
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>🔄 Transfer Vendor Ownership</h3>
|
|
||||||
<button @click="showTransferOwnership = false" class="btn-close">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="alert alert-warning mb-3">
|
|
||||||
<strong>⚠️ Warning:</strong> This will transfer complete ownership to another user.
|
|
||||||
The current owner will be demoted to Manager role.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-box mb-3">
|
|
||||||
<strong>Current Owner:</strong><br>
|
|
||||||
<span x-text="vendor?.owner_username"></span> (<span x-text="vendor?.owner_email"></span>)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="newOwnerId">
|
|
||||||
New Owner User ID <span class="required">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="newOwnerId"
|
|
||||||
name="new_owner_user_id"
|
|
||||||
x-model.number="transferData.new_owner_user_id"
|
|
||||||
required
|
|
||||||
placeholder="Enter user ID"
|
|
||||||
min="1"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
<div class="form-help">
|
|
||||||
Enter the ID of the user who will become the new owner
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="transferReason">Reason for Transfer</label>
|
|
||||||
<textarea
|
|
||||||
id="transferReason"
|
|
||||||
name="transfer_reason"
|
|
||||||
x-model="transferData.transfer_reason"
|
|
||||||
rows="3"
|
|
||||||
placeholder="Optional: Why is ownership being transferred?"
|
|
||||||
autocomplete="off"
|
|
||||||
></textarea>
|
|
||||||
<div class="form-help">
|
|
||||||
This will be logged for audit purposes
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="confirmTransfer" class="checkbox-label">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="confirmTransfer"
|
|
||||||
name="confirm_transfer"
|
|
||||||
x-model="transferData.confirm_transfer"
|
|
||||||
>
|
|
||||||
I confirm this ownership transfer
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button
|
|
||||||
@click="showTransferOwnership = false"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
:disabled="transferring">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="handleTransferOwnership()"
|
|
||||||
class="btn btn-danger"
|
|
||||||
:disabled="!transferData.confirm_transfer || !transferData.new_owner_user_id || transferring">
|
|
||||||
<span x-show="!transferring">🔄 Transfer Ownership</span>
|
|
||||||
<span x-show="transferring">
|
|
||||||
<span class="loading-spinner"></span>
|
|
||||||
Transferring...
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/shared/js/api-client.js"></script>
|
|
||||||
<script src="/static/admin/js/vendor-edit.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html :class="{ 'theme-dark': dark }" x-data="vendorsData()" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Vendors - Admin Portal</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="/static/admin/css/tailwind.output.css" />
|
|
||||||
<style>
|
|
||||||
[x-cloak] { display: none !important; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body x-cloak>
|
|
||||||
<div class="flex h-screen bg-gray-50 dark:bg-gray-900" :class="{ 'overflow-hidden': isSideMenuOpen }">
|
|
||||||
<!-- Sidebar Container -->
|
|
||||||
<div id="sidebar-container"></div>
|
|
||||||
|
|
||||||
<div class="flex flex-col flex-1 w-full">
|
|
||||||
<!-- Header Container -->
|
|
||||||
<div id="header-container"></div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main class="h-full overflow-y-auto">
|
|
||||||
<div class="container px-6 mx-auto grid">
|
|
||||||
<div class="flex items-center justify-between my-6">
|
|
||||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
|
||||||
Vendor Management
|
|
||||||
</h2>
|
|
||||||
<button class="flex items-center justify-between 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">
|
|
||||||
<!-- Heroicon: plus -->
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
|
||||||
</svg>
|
|
||||||
Create Vendor
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Placeholder Content -->
|
|
||||||
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
||||||
<div class="text-center py-12">
|
|
||||||
<div class="mb-4">
|
|
||||||
<!-- Heroicon: shopping-bag (outline, large) -->
|
|
||||||
<svg class="mx-auto h-24 w-24 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="mb-2 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
|
||||||
Vendor Management
|
|
||||||
</h3>
|
|
||||||
<p class="mb-6 text-gray-600 dark:text-gray-400">
|
|
||||||
This page will display vendor list and management tools.
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Features to be implemented:
|
|
||||||
</p>
|
|
||||||
<ul class="mt-4 text-sm text-gray-600 dark:text-gray-400 space-y-2">
|
|
||||||
<li>• View all vendors with filtering and search</li>
|
|
||||||
<li>• Create new vendors with detailed forms</li>
|
|
||||||
<li>• Edit vendor information</li>
|
|
||||||
<li>• Verify/unverify vendors</li>
|
|
||||||
<li>• View vendor statistics</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Example vendors table structure (for reference) -->
|
|
||||||
<div class="w-full overflow-hidden rounded-lg shadow-xs">
|
|
||||||
<div class="w-full overflow-x-auto">
|
|
||||||
<table class="w-full whitespace-no-wrap">
|
|
||||||
<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:text-gray-400 dark:bg-gray-800">
|
|
||||||
<th class="px-4 py-3">Vendor Code</th>
|
|
||||||
<th class="px-4 py-3">Name</th>
|
|
||||||
<th class="px-4 py-3">Subdomain</th>
|
|
||||||
<th class="px-4 py-3">Status</th>
|
|
||||||
<th class="px-4 py-3">Created</th>
|
|
||||||
<th class="px-4 py-3">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
|
||||||
<template x-if="vendors.length === 0">
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="px-4 py-8 text-sm text-center text-gray-600 dark:text-gray-400">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<svg class="w-16 h-16 mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
|
|
||||||
</svg>
|
|
||||||
<p class="font-medium">No vendors found</p>
|
|
||||||
<p class="text-xs mt-1">Create your first vendor to get started</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-for="vendor in vendors" :key="vendor.id">
|
|
||||||
<tr class="text-gray-700 dark:text-gray-400">
|
|
||||||
<td class="px-4 py-3">
|
|
||||||
<p class="font-semibold text-sm" x-text="vendor.vendor_code"></p>
|
|
||||||
</td>
|
|
||||||
<td class="px-4 py-3 text-sm" x-text="vendor.name"></td>
|
|
||||||
<td class="px-4 py-3 text-sm" x-text="vendor.subdomain"></td>
|
|
||||||
<td class="px-4 py-3 text-xs">
|
|
||||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
|
|
||||||
:class="vendor.is_verified ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:text-white dark:bg-orange-600'"
|
|
||||||
x-text="vendor.is_verified ? 'Verified' : 'Pending'">
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-4 py-3 text-sm" x-text="formatDate(vendor.created_at)"></td>
|
|
||||||
<td class="px-4 py-3">
|
|
||||||
<div class="flex items-center space-x-4 text-sm">
|
|
||||||
<button class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray"
|
|
||||||
aria-label="Edit">
|
|
||||||
<svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-red-600 rounded-lg dark:text-red-400 focus:outline-none focus:shadow-outline-gray"
|
|
||||||
aria-label="Delete">
|
|
||||||
<svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Load partials BEFORE Alpine -->
|
|
||||||
<script src="/static/shared/js/partial-loader.js"></script>
|
|
||||||
<script src="/static/shared/js/icons.js"></script>
|
|
||||||
<script>
|
|
||||||
(async () => {
|
|
||||||
await window.partialLoader.loadAll({
|
|
||||||
'header-container': 'header.html',
|
|
||||||
'sidebar-container': 'sidebar.html'
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Alpine.js v3 -->
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Initialize Alpine data -->
|
|
||||||
<script src="/static/admin/js/init-alpine.js"></script>
|
|
||||||
|
|
||||||
<!-- Vendors-specific logic -->
|
|
||||||
<script>
|
|
||||||
function vendorsData() {
|
|
||||||
return {
|
|
||||||
...data(), // Spread base data from init-alpine.js
|
|
||||||
currentPage: 'vendors',
|
|
||||||
vendors: [],
|
|
||||||
loading: false,
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
await this.loadVendors();
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadVendors() {
|
|
||||||
this.loading = true;
|
|
||||||
try {
|
|
||||||
// Replace with your actual API endpoint
|
|
||||||
const response = await fetch('/api/v1/admin/vendors', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
this.vendors = data.vendors || [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading vendors:', error);
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDate(dateString) {
|
|
||||||
if (!dateString) return '-';
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,894 +0,0 @@
|
|||||||
/* static/css/admin/admin.css */
|
|
||||||
/* Admin-specific styles */
|
|
||||||
|
|
||||||
/* Admin Header */
|
|
||||||
.admin-header {
|
|
||||||
background: white;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
padding: 16px 24px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-header h1 {
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
font-size: var(--font-base);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info strong {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-logout {
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: var(--danger-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-logout:hover {
|
|
||||||
background: #c0392b;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Admin Container */
|
|
||||||
.admin-container {
|
|
||||||
display: flex;
|
|
||||||
min-height: calc(100vh - 64px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Admin Sidebar */
|
|
||||||
.admin-sidebar {
|
|
||||||
width: 250px;
|
|
||||||
background: white;
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
padding: var(--spacing-lg) 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 24px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
border-right: 3px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover {
|
|
||||||
background: var(--gray-50);
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link.active {
|
|
||||||
background: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
border-right-color: var(--primary-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
margin-right: var(--spacing-sm);
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Admin Content */
|
|
||||||
.admin-content {
|
|
||||||
flex: 1;
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
overflow-y: auto;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stats Grid */
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: var(--spacing-lg);
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-title {
|
|
||||||
font-size: var(--font-base);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-weight: 500;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-subtitle {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-change {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-left: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-change.positive {
|
|
||||||
color: var(--success-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-change.negative {
|
|
||||||
color: var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content Sections */
|
|
||||||
.content-section {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
padding-bottom: var(--spacing-md);
|
|
||||||
border-bottom: 2px solid var(--gray-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Data Tables */
|
|
||||||
.data-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table thead {
|
|
||||||
background: var(--gray-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table th {
|
|
||||||
text-align: left;
|
|
||||||
padding: 12px;
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table td {
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table tbody tr {
|
|
||||||
transition: background var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table tbody tr:hover {
|
|
||||||
background: var(--gray-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table tbody tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-actions .btn {
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty State */
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-2xl) var(--spacing-lg);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h3 {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
font-size: var(--font-base);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading State */
|
|
||||||
.loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-2xl);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
margin-top: var(--spacing-md);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search and Filters */
|
|
||||||
.filter-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
position: relative;
|
|
||||||
flex: 2;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
padding-left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action Buttons */
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal/Dialog */
|
|
||||||
/* Modal Styles */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
max-width: 600px;
|
|
||||||
width: 90%;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
padding: 20px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 28px;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-close:hover {
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-box {
|
|
||||||
background-color: #eff6ff;
|
|
||||||
border: 1px solid #3b82f6;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px;
|
|
||||||
color: #1e40af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section-divider {
|
|
||||||
border-top: 2px solid #e5e7eb;
|
|
||||||
margin: 32px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Small modal variant for confirmations */
|
|
||||||
.modal-sm {
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Info box styling */
|
|
||||||
.info-box {
|
|
||||||
background: #e3f2fd;
|
|
||||||
border-left: 4px solid #2196f3;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quick actions spacing */
|
|
||||||
.quick-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Success Modal Styles */
|
|
||||||
.modal-header-success {
|
|
||||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
|
||||||
color: white;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header-success .btn-close {
|
|
||||||
color: white;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header-success .btn-close:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-icon-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-icon {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 48px;
|
|
||||||
font-weight: bold;
|
|
||||||
animation: successPulse 0.6s ease-out;
|
|
||||||
box-shadow: 0 4px 20px rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes successPulse {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.5);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.transfer-details {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #666;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-value {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-value strong {
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-arrow {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 24px;
|
|
||||||
color: #4caf50;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-md {
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-block {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alert variants in modals */
|
|
||||||
.modal-body .alert {
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body .alert-info {
|
|
||||||
background: #e3f2fd;
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body .alert-info strong {
|
|
||||||
color: #0d47a1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Text utilities */
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-muted {
|
|
||||||
color: #6c757d;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-3 {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-4 {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-3 {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pagination */
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-btn {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-btn:hover:not(:disabled) {
|
|
||||||
background: var(--gray-50);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-btn.active {
|
|
||||||
background: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-info {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.admin-sidebar {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.admin-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar {
|
|
||||||
width: 100%;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-content {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-bar {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group,
|
|
||||||
.search-box {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table th,
|
|
||||||
.data-table td {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.admin-header {
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-header h1 {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: var(--font-3xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make table scrollable on small screens */
|
|
||||||
.table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table {
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print Styles */
|
|
||||||
@media print {
|
|
||||||
.admin-sidebar,
|
|
||||||
.admin-header .header-right,
|
|
||||||
.section-actions,
|
|
||||||
.table-actions {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-content {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section {
|
|
||||||
box-shadow: none;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Value with copy button */
|
|
||||||
.value-with-copy {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-item.highlight {
|
|
||||||
background: #fff3cd;
|
|
||||||
border: 2px solid var(--warning-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-item.highlight .value {
|
|
||||||
background: white;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-copy {
|
|
||||||
background: var(--gray-100);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--font-xs);
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-copy:hover {
|
|
||||||
background: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Text color utilities */
|
|
||||||
.text-danger {
|
|
||||||
color: var(--danger-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-warning {
|
|
||||||
color: var(--warning-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-success {
|
|
||||||
color: var(--success-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disabled button styles */
|
|
||||||
.btn-icon:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon:disabled:hover {
|
|
||||||
background: white;
|
|
||||||
border-color: var(--border-color);
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alert improvements */
|
|
||||||
.alert {
|
|
||||||
animation: slideDown 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideDown {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Section Styles */
|
|
||||||
.form-section-title {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1f2937;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 2px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control-disabled {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-help {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #6b7280;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-muted {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-3 {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-3 {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alert Styles */
|
|
||||||
.alert {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-warning {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
border: 1px solid #f59e0b;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-warning strong {
|
|
||||||
color: #78350f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quick Actions */
|
|
||||||
.quick-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-actions .btn {
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button Sizes */
|
|
||||||
.btn-sm {
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Required Field Indicator */
|
|
||||||
.required {
|
|
||||||
color: #ef4444;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
@@ -1,622 +0,0 @@
|
|||||||
/* static/css/shared/auth.css */
|
|
||||||
/* Authentication pages (login, register) styles */
|
|
||||||
|
|
||||||
/* Auth Page Layout */
|
|
||||||
.auth-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-page::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 20% 80%, rgba(255,255,255,0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 20%, rgba(255,255,255,0.1) 0%, transparent 50%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login Container */
|
|
||||||
.login-container,
|
|
||||||
.auth-container {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
box-shadow: var(--shadow-xl);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 420px;
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
animation: slideUp 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login Header */
|
|
||||||
.login-header,
|
|
||||||
.auth-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-logo {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin: 0 auto var(--spacing-md);
|
|
||||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 40px;
|
|
||||||
color: white;
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header h1,
|
|
||||||
.auth-header h1 {
|
|
||||||
font-size: var(--font-3xl);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header p,
|
|
||||||
.auth-header p {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vendor Info Display */
|
|
||||||
.vendor-info {
|
|
||||||
background: var(--gray-50);
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-info strong {
|
|
||||||
color: var(--primary-color);
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* No Vendor Message */
|
|
||||||
.no-vendor-message {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-2xl) var(--spacing-lg);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-vendor-message h2 {
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-vendor-message p {
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Auth Form */
|
|
||||||
.auth-form,
|
|
||||||
.login-form {
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
font-size: var(--font-md);
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input.error {
|
|
||||||
border-color: var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input::placeholder {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Password Toggle */
|
|
||||||
.password-group {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-toggle {
|
|
||||||
position: absolute;
|
|
||||||
right: 12px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-muted);
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-toggle:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remember Me & Forgot Password */
|
|
||||||
.form-options {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.remember-me {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.remember-me input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forgot-password {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forgot-password:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Submit Button */
|
|
||||||
.btn-login,
|
|
||||||
.btn-auth {
|
|
||||||
width: 100%;
|
|
||||||
padding: 14px 24px;
|
|
||||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login:hover:not(:disabled),
|
|
||||||
.btn-auth:hover:not(:disabled) {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login:active:not(:disabled),
|
|
||||||
.btn-auth:active:not(:disabled) {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login:disabled,
|
|
||||||
.btn-auth:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Social Login Buttons */
|
|
||||||
.social-login {
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
padding-top: var(--spacing-lg);
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-login-text {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-social {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-social:hover {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
background: var(--gray-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login Footer */
|
|
||||||
.login-footer,
|
|
||||||
.auth-footer {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
padding-top: var(--spacing-lg);
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-footer a,
|
|
||||||
.auth-footer a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-footer a:hover,
|
|
||||||
.auth-footer a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-footer-text {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Back Button */
|
|
||||||
.btn-back {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: var(--secondary-color);
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-back:hover {
|
|
||||||
background: #5a6268;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Error and Success Messages */
|
|
||||||
.alert {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
display: none;
|
|
||||||
animation: slideDown 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideDown {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-success {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-error {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-warning {
|
|
||||||
background: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
border: 1px solid #ffeaa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-info {
|
|
||||||
background: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
border: 1px solid #bee5eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Field Errors */
|
|
||||||
.error-message {
|
|
||||||
color: var(--danger-color);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
margin-top: var(--spacing-xs);
|
|
||||||
display: none;
|
|
||||||
animation: fadeIn 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading State */
|
|
||||||
.loading-spinner {
|
|
||||||
display: inline-block;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid currentColor;
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: transparent;
|
|
||||||
animation: spinner 0.6s linear infinite;
|
|
||||||
margin-right: var(--spacing-sm);
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spinner {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Divider */
|
|
||||||
.divider {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
margin: var(--spacing-lg) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider::before,
|
|
||||||
.divider::after {
|
|
||||||
content: '';
|
|
||||||
flex: 1;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider span {
|
|
||||||
padding: 0 var(--spacing-md);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Credentials Display (for vendor creation success) */
|
|
||||||
.credentials-card {
|
|
||||||
background: #fff3cd;
|
|
||||||
border: 2px solid var(--warning-color);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credentials-card h3 {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
color: #856404;
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-item {
|
|
||||||
background: white;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-item label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-item .value {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
background: var(--gray-50);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning-text {
|
|
||||||
color: #856404;
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
margin-top: var(--spacing-md);
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning-text::before {
|
|
||||||
content: '⚠️';
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Copy Button */
|
|
||||||
.btn-copy {
|
|
||||||
background: none;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--font-xs);
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-copy:hover {
|
|
||||||
background: var(--gray-50);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.login-container,
|
|
||||||
.auth-container {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-logo {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header h1,
|
|
||||||
.auth-header h1 {
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login,
|
|
||||||
.btn-auth {
|
|
||||||
padding: 12px 20px;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-buttons {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-item .value {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark Mode Support (optional) */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.auth-page {
|
|
||||||
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container,
|
|
||||||
.auth-container {
|
|
||||||
background: #2d3748;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header h1,
|
|
||||||
.auth-header h1,
|
|
||||||
.form-group label {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header p,
|
|
||||||
.auth-header p {
|
|
||||||
color: #a0aec0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input {
|
|
||||||
background: #1a202c;
|
|
||||||
border-color: #4a5568;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input::placeholder {
|
|
||||||
color: #718096;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-info {
|
|
||||||
background: #1a202c;
|
|
||||||
border-color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-item {
|
|
||||||
background: #1a202c;
|
|
||||||
border-color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-item .value {
|
|
||||||
background: #2d3748;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print Styles */
|
|
||||||
@media print {
|
|
||||||
.auth-page::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container,
|
|
||||||
.auth-container {
|
|
||||||
box-shadow: none;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login,
|
|
||||||
.btn-auth,
|
|
||||||
.social-login,
|
|
||||||
.login-footer,
|
|
||||||
.auth-footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,514 +0,0 @@
|
|||||||
/* static/css/shared/base.css */
|
|
||||||
/* Base styles shared across all pages */
|
|
||||||
|
|
||||||
/* Import responsive utilities */
|
|
||||||
@import url('responsive-utilities.css');
|
|
||||||
|
|
||||||
/* Rest of your base.css... */
|
|
||||||
:root {
|
|
||||||
/* Color Palette */
|
|
||||||
--primary-color: #667eea;
|
|
||||||
--primary-dark: #764ba2;
|
|
||||||
--secondary-color: #6c757d;
|
|
||||||
--success-color: #28a745;
|
|
||||||
--danger-color: #e74c3c;
|
|
||||||
--warning-color: #ffc107;
|
|
||||||
--info-color: #17a2b8;
|
|
||||||
|
|
||||||
/* Grays */
|
|
||||||
--gray-50: #f9fafb;
|
|
||||||
--gray-100: #f5f7fa;
|
|
||||||
--gray-200: #e1e8ed;
|
|
||||||
--gray-300: #d1d9e0;
|
|
||||||
--gray-400: #b0bac5;
|
|
||||||
--gray-500: #8796a5;
|
|
||||||
--gray-600: #687785;
|
|
||||||
--gray-700: #4a5568;
|
|
||||||
--gray-800: #2d3748;
|
|
||||||
--gray-900: #1a202c;
|
|
||||||
|
|
||||||
/* Text Colors */
|
|
||||||
--text-primary: #333333;
|
|
||||||
--text-secondary: #666666;
|
|
||||||
--text-muted: #999999;
|
|
||||||
|
|
||||||
/* Background Colors */
|
|
||||||
--bg-primary: #ffffff;
|
|
||||||
--bg-secondary: #f5f7fa;
|
|
||||||
--bg-overlay: rgba(0, 0, 0, 0.5);
|
|
||||||
|
|
||||||
/* Border Colors */
|
|
||||||
--border-color: #e1e8ed;
|
|
||||||
--border-focus: #667eea;
|
|
||||||
|
|
||||||
/* Shadows */
|
|
||||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.15);
|
|
||||||
--shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
||||||
|
|
||||||
/* Transitions */
|
|
||||||
--transition-fast: 0.15s ease;
|
|
||||||
--transition-base: 0.2s ease;
|
|
||||||
--transition-slow: 0.3s ease;
|
|
||||||
|
|
||||||
/* Border Radius */
|
|
||||||
--radius-sm: 4px;
|
|
||||||
--radius-md: 6px;
|
|
||||||
--radius-lg: 8px;
|
|
||||||
--radius-xl: 12px;
|
|
||||||
--radius-full: 9999px;
|
|
||||||
|
|
||||||
/* Spacing */
|
|
||||||
--spacing-xs: 4px;
|
|
||||||
--spacing-sm: 8px;
|
|
||||||
--spacing-md: 16px;
|
|
||||||
--spacing-lg: 24px;
|
|
||||||
--spacing-xl: 32px;
|
|
||||||
--spacing-2xl: 48px;
|
|
||||||
|
|
||||||
/* Font Sizes */
|
|
||||||
--font-xs: 12px;
|
|
||||||
--font-sm: 13px;
|
|
||||||
--font-base: 14px;
|
|
||||||
--font-md: 15px;
|
|
||||||
--font-lg: 16px;
|
|
||||||
--font-xl: 18px;
|
|
||||||
--font-2xl: 20px;
|
|
||||||
--font-3xl: 24px;
|
|
||||||
--font-4xl: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset and Base Styles */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 16px;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Typography */
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.2;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 { font-size: var(--font-4xl); }
|
|
||||||
h2 { font-size: var(--font-3xl); }
|
|
||||||
h3 { font-size: var(--font-2xl); }
|
|
||||||
h4 { font-size: var(--font-xl); }
|
|
||||||
h5 { font-size: var(--font-lg); }
|
|
||||||
h6 { font-size: var(--font-base); }
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: var(--primary-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
text-decoration: none;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: var(--secondary-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover:not(:disabled) {
|
|
||||||
background: #5a6268;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background: var(--success-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: var(--danger-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning {
|
|
||||||
background: var(--warning-color);
|
|
||||||
color: var(--gray-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
background: transparent;
|
|
||||||
border: 2px solid var(--primary-color);
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline:hover:not(:disabled) {
|
|
||||||
background: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-lg {
|
|
||||||
padding: 14px 28px;
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Elements */
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control,
|
|
||||||
.form-input,
|
|
||||||
.form-select,
|
|
||||||
.form-textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: white;
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
transition: border-color var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus,
|
|
||||||
.form-input:focus,
|
|
||||||
.form-select:focus,
|
|
||||||
.form-textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--border-focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control.error,
|
|
||||||
.form-input.error {
|
|
||||||
border-color: var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-help {
|
|
||||||
display: block;
|
|
||||||
margin-top: var(--spacing-xs);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
display: none;
|
|
||||||
margin-top: var(--spacing-xs);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cards */
|
|
||||||
.card {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
background: var(--gray-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Badges */
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 10px;
|
|
||||||
font-size: var(--font-xs);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-primary {
|
|
||||||
background: rgba(102, 126, 234, 0.1);
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-success {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-danger {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-warning {
|
|
||||||
background: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-info {
|
|
||||||
background: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-secondary {
|
|
||||||
background: var(--gray-200);
|
|
||||||
color: var(--gray-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alerts */
|
|
||||||
.alert {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-success {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
border-color: #c3e6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-danger,
|
|
||||||
.alert-error {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
border-color: #f5c6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-warning {
|
|
||||||
background: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
border-color: #ffeaa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-info {
|
|
||||||
background: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
border-color: #bee5eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
.table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th {
|
|
||||||
text-align: left;
|
|
||||||
padding: 12px;
|
|
||||||
background: var(--gray-100);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table td {
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tr:hover {
|
|
||||||
background: var(--gray-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-striped tbody tr:nth-child(odd) {
|
|
||||||
background: var(--gray-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Utilities */
|
|
||||||
.text-center { text-align: center; }
|
|
||||||
.text-left { text-align: left; }
|
|
||||||
.text-right { text-align: right; }
|
|
||||||
|
|
||||||
.text-muted { color: var(--text-muted); }
|
|
||||||
.text-primary { color: var(--primary-color); }
|
|
||||||
.text-success { color: var(--success-color); }
|
|
||||||
.text-danger { color: var(--danger-color); }
|
|
||||||
.text-warning { color: var(--warning-color); }
|
|
||||||
|
|
||||||
.font-bold { font-weight: 700; }
|
|
||||||
.font-semibold { font-weight: 600; }
|
|
||||||
.font-normal { font-weight: 400; }
|
|
||||||
|
|
||||||
.mt-0 { margin-top: 0; }
|
|
||||||
.mt-1 { margin-top: var(--spacing-sm); }
|
|
||||||
.mt-2 { margin-top: var(--spacing-md); }
|
|
||||||
.mt-3 { margin-top: var(--spacing-lg); }
|
|
||||||
.mt-4 { margin-top: var(--spacing-xl); }
|
|
||||||
|
|
||||||
.mb-0 { margin-bottom: 0; }
|
|
||||||
.mb-1 { margin-bottom: var(--spacing-sm); }
|
|
||||||
.mb-2 { margin-bottom: var(--spacing-md); }
|
|
||||||
.mb-3 { margin-bottom: var(--spacing-lg); }
|
|
||||||
.mb-4 { margin-bottom: var(--spacing-xl); }
|
|
||||||
|
|
||||||
.p-0 { padding: 0; }
|
|
||||||
.p-1 { padding: var(--spacing-sm); }
|
|
||||||
.p-2 { padding: var(--spacing-md); }
|
|
||||||
.p-3 { padding: var(--spacing-lg); }
|
|
||||||
.p-4 { padding: var(--spacing-xl); }
|
|
||||||
|
|
||||||
.d-none { display: none; }
|
|
||||||
.d-block { display: block; }
|
|
||||||
.d-inline { display: inline; }
|
|
||||||
.d-inline-block { display: inline-block; }
|
|
||||||
.d-flex { display: flex; }
|
|
||||||
|
|
||||||
.flex-column { flex-direction: column; }
|
|
||||||
.flex-row { flex-direction: row; }
|
|
||||||
.justify-start { justify-content: flex-start; }
|
|
||||||
.justify-end { justify-content: flex-end; }
|
|
||||||
.justify-center { justify-content: center; }
|
|
||||||
.justify-between { justify-content: space-between; }
|
|
||||||
.align-start { align-items: flex-start; }
|
|
||||||
.align-end { align-items: flex-end; }
|
|
||||||
.align-center { align-items: center; }
|
|
||||||
|
|
||||||
.gap-1 { gap: var(--spacing-sm); }
|
|
||||||
.gap-2 { gap: var(--spacing-md); }
|
|
||||||
.gap-3 { gap: var(--spacing-lg); }
|
|
||||||
|
|
||||||
/* Loading Spinner */
|
|
||||||
.loading-spinner {
|
|
||||||
display: inline-block;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid currentColor;
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: transparent;
|
|
||||||
animation: spinner 0.6s linear infinite;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spinner {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: var(--bg-overlay);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner-lg {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
:root {
|
|
||||||
--font-base: 14px;
|
|
||||||
--font-lg: 15px;
|
|
||||||
--font-xl: 16px;
|
|
||||||
--font-2xl: 18px;
|
|
||||||
--font-3xl: 20px;
|
|
||||||
--font-4xl: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,728 +0,0 @@
|
|||||||
/**
|
|
||||||
* Universal Components Styles
|
|
||||||
* Shared component styles for Admin, Vendor, and Shop sections
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
MODAL SYSTEM STYLES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
/* Modal Backdrop */
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9999;
|
|
||||||
animation: fadeIn 0.2s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Container */
|
|
||||||
.modal-container {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
max-width: 500px;
|
|
||||||
width: 90%;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
animation: slideUp 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
transform: translateY(20px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Header */
|
|
||||||
.modal-header {
|
|
||||||
padding: 20px 24px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #111827;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #6b7280;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close:hover {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Body */
|
|
||||||
.modal-body {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-message {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: #374151;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-warning {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
border-left: 4px solid #f59e0b;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-warning p {
|
|
||||||
margin: 0;
|
|
||||||
color: #92400e;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-details {
|
|
||||||
background-color: #f9fafb;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-top: 12px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #374151;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Footer */
|
|
||||||
.modal-footer {
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Icon Styles */
|
|
||||||
.modal-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 auto 16px;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-icon.success {
|
|
||||||
background-color: #d1fae5;
|
|
||||||
color: #059669;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-icon.error {
|
|
||||||
background-color: #fee2e2;
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-icon.warning {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Overlay */
|
|
||||||
.loading-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 10000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 4px solid #e5e7eb;
|
|
||||||
border-top-color: #3b82f6;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
BUTTON STYLES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #3b82f6;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
background-color: #2563eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: #6b7280;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover:not(:disabled) {
|
|
||||||
background-color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background-color: #10b981;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success:hover:not(:disabled) {
|
|
||||||
background-color: #059669;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background-color: #ef4444;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover:not(:disabled) {
|
|
||||||
background-color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning {
|
|
||||||
background-color: #f59e0b;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning:hover:not(:disabled) {
|
|
||||||
background-color: #d97706;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline:hover:not(:disabled) {
|
|
||||||
background-color: #f9fafb;
|
|
||||||
border-color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
background-color: transparent;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover:not(:disabled) {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-lg {
|
|
||||||
padding: 12px 24px;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
ADMIN LAYOUT STYLES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.admin-header {
|
|
||||||
background-color: #1f2937;
|
|
||||||
color: white;
|
|
||||||
padding: 0 24px;
|
|
||||||
height: 64px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-toggle {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-logout {
|
|
||||||
background-color: #374151;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-logout:hover {
|
|
||||||
background-color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar {
|
|
||||||
width: 260px;
|
|
||||||
background-color: #f9fafb;
|
|
||||||
border-right: 1px solid #e5e7eb;
|
|
||||||
height: calc(100vh - 64px);
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 64px;
|
|
||||||
overflow-y: auto;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-nav {
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 24px;
|
|
||||||
color: #374151;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.2s;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:hover {
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active {
|
|
||||||
background-color: #3b82f6;
|
|
||||||
color: white;
|
|
||||||
border-left: 4px solid #2563eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item i {
|
|
||||||
width: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-content {
|
|
||||||
margin-left: 260px;
|
|
||||||
padding: 24px;
|
|
||||||
min-height: calc(100vh - 64px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
VENDOR LAYOUT STYLES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.vendor-header {
|
|
||||||
background-color: #059669;
|
|
||||||
color: white;
|
|
||||||
padding: 0 24px;
|
|
||||||
height: 64px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-header .btn-logout {
|
|
||||||
background-color: #047857;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-header .btn-logout:hover {
|
|
||||||
background-color: #065f46;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-header .menu-toggle {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-sidebar {
|
|
||||||
width: 260px;
|
|
||||||
background-color: #f9fafb;
|
|
||||||
border-right: 1px solid #e5e7eb;
|
|
||||||
height: calc(100vh - 64px);
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 64px;
|
|
||||||
overflow-y: auto;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-content {
|
|
||||||
margin-left: 260px;
|
|
||||||
padding: 24px;
|
|
||||||
min-height: calc(100vh - 64px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
SHOP LAYOUT STYLES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.shop-header {
|
|
||||||
background-color: white;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
padding: 16px 24px;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-header-top {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-logo {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #111827;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-search {
|
|
||||||
flex: 1;
|
|
||||||
max-width: 500px;
|
|
||||||
margin: 0 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-button {
|
|
||||||
position: relative;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-button:hover {
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-count {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: #ef4444;
|
|
||||||
color: white;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 10px;
|
|
||||||
min-width: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 16px auto 0;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-nav-item {
|
|
||||||
color: #6b7280;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
padding: 8px 0;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-nav-item:hover {
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-nav-item.active {
|
|
||||||
color: #3b82f6;
|
|
||||||
font-weight: 500;
|
|
||||||
border-bottom: 2px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-content {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 32px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
SHOP ACCOUNT LAYOUT STYLES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.account-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 260px 1fr;
|
|
||||||
gap: 32px;
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 32px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-sidebar {
|
|
||||||
background-color: #f9fafb;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-nav-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
color: #374151;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-nav-item:hover {
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-nav-item.active {
|
|
||||||
background-color: #3b82f6;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-content {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
RESPONSIVE STYLES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.menu-toggle {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-header .menu-toggle {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar,
|
|
||||||
.vendor-sidebar {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar.open,
|
|
||||||
.vendor-sidebar.open {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-content,
|
|
||||||
.vendor-content {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-search {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-sidebar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
width: 95%;
|
|
||||||
margin: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer .btn {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
UTILITY CLASSES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-0 { margin-bottom: 0; }
|
|
||||||
.mb-1 { margin-bottom: 8px; }
|
|
||||||
.mb-2 { margin-bottom: 16px; }
|
|
||||||
.mb-3 { margin-bottom: 24px; }
|
|
||||||
.mb-4 { margin-bottom: 32px; }
|
|
||||||
|
|
||||||
.mt-0 { margin-top: 0; }
|
|
||||||
.mt-1 { margin-top: 8px; }
|
|
||||||
.mt-2 { margin-top: 16px; }
|
|
||||||
.mt-3 { margin-top: 24px; }
|
|
||||||
.mt-4 { margin-top: 32px; }
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pointer {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
@@ -1,428 +0,0 @@
|
|||||||
/**
|
|
||||||
* Enhanced Modal Styles
|
|
||||||
* Additional styling and animations for modal system
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
MODAL ANIMATIONS
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
/* Fade In Animation */
|
|
||||||
@keyframes modalFadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Slide Down Animation */
|
|
||||||
@keyframes modalSlideDown {
|
|
||||||
from {
|
|
||||||
transform: translateY(-50px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Slide Up Animation */
|
|
||||||
@keyframes modalSlideUp {
|
|
||||||
from {
|
|
||||||
transform: translateY(50px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scale Animation */
|
|
||||||
@keyframes modalScale {
|
|
||||||
from {
|
|
||||||
transform: scale(0.9);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Shake Animation for errors */
|
|
||||||
@keyframes modalShake {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
10%, 30%, 50%, 70%, 90% {
|
|
||||||
transform: translateX(-10px);
|
|
||||||
}
|
|
||||||
20%, 40%, 60%, 80% {
|
|
||||||
transform: translateX(10px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
MODAL VARIANTS
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
/* Modal with slide down animation */
|
|
||||||
.modal-container.slide-down {
|
|
||||||
animation: modalSlideDown 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal with scale animation */
|
|
||||||
.modal-container.scale {
|
|
||||||
animation: modalScale 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal with shake animation (for errors) */
|
|
||||||
.modal-container.shake {
|
|
||||||
animation: modalShake 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
MODAL SIZES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.modal-container.modal-sm {
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container.modal-md {
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container.modal-lg {
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container.modal-xl {
|
|
||||||
max-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container.modal-full {
|
|
||||||
max-width: 95%;
|
|
||||||
max-height: 95vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
MODAL THEMES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
/* Success Modal Theme */
|
|
||||||
.modal-container.modal-success .modal-header {
|
|
||||||
background-color: #d1fae5;
|
|
||||||
border-bottom-color: #a7f3d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container.modal-success .modal-title {
|
|
||||||
color: #065f46;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Error Modal Theme */
|
|
||||||
.modal-container.modal-error .modal-header {
|
|
||||||
background-color: #fee2e2;
|
|
||||||
border-bottom-color: #fecaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container.modal-error .modal-title {
|
|
||||||
color: #991b1b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Warning Modal Theme */
|
|
||||||
.modal-container.modal-warning .modal-header {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
border-bottom-color: #fde68a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container.modal-warning .modal-title {
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Info Modal Theme */
|
|
||||||
.modal-container.modal-info .modal-header {
|
|
||||||
background-color: #dbeafe;
|
|
||||||
border-bottom-color: #bfdbfe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container.modal-info .modal-title {
|
|
||||||
color: #1e40af;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
MODAL CONTENT STYLES
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
/* Modal List */
|
|
||||||
.modal-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-list-item {
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-list-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-list-item i {
|
|
||||||
width: 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Form Elements */
|
|
||||||
.modal-form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #374151;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-input,
|
|
||||||
.modal-textarea,
|
|
||||||
.modal-select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-input:focus,
|
|
||||||
.modal-textarea:focus,
|
|
||||||
.modal-select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-textarea {
|
|
||||||
min-height: 100px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-input.error,
|
|
||||||
.modal-textarea.error,
|
|
||||||
.modal-select.error {
|
|
||||||
border-color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-error-text {
|
|
||||||
color: #ef4444;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-help-text {
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
MODAL ALERTS
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.modal-alert {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: start;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-alert i {
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-alert.alert-success {
|
|
||||||
background-color: #d1fae5;
|
|
||||||
color: #065f46;
|
|
||||||
border: 1px solid #a7f3d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-alert.alert-error {
|
|
||||||
background-color: #fee2e2;
|
|
||||||
color: #991b1b;
|
|
||||||
border: 1px solid #fecaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-alert.alert-warning {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
border: 1px solid #fde68a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-alert.alert-info {
|
|
||||||
background-color: #dbeafe;
|
|
||||||
color: #1e40af;
|
|
||||||
border: 1px solid #bfdbfe;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
MODAL PROGRESS
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.modal-progress {
|
|
||||||
width: 100%;
|
|
||||||
height: 8px;
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-progress-bar {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #3b82f6;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-progress-bar.success {
|
|
||||||
background-color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-progress-bar.error {
|
|
||||||
background-color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-progress-bar.warning {
|
|
||||||
background-color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
LOADING SPINNER VARIANTS
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.loading-spinner.spinner-sm {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-width: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner.spinner-lg {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
border-width: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner.spinner-success {
|
|
||||||
border-top-color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner.spinner-error {
|
|
||||||
border-top-color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner.spinner-warning {
|
|
||||||
border-top-color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
MODAL BACKDROP VARIANTS
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.modal-backdrop.backdrop-dark {
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-backdrop.backdrop-light {
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-backdrop.backdrop-blur {
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
RESPONSIVE ENHANCEMENTS
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.modal-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100vh;
|
|
||||||
border-radius: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
padding: 12px 20px;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer .btn {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-alert {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
ACCESSIBILITY
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.modal-backdrop:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container:focus {
|
|
||||||
outline: 2px solid #3b82f6;
|
|
||||||
outline-offset: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reduce motion for users who prefer it */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.modal-backdrop,
|
|
||||||
.modal-container,
|
|
||||||
.loading-spinner {
|
|
||||||
animation: none !important;
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* High contrast mode support */
|
|
||||||
@media (prefers-contrast: high) {
|
|
||||||
.modal-container {
|
|
||||||
border: 2px solid currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
border-bottom-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
border-top-width: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
/* static/css/shared/responsive-utilities.css */
|
|
||||||
/* Responsive utility classes - Framework-like responsiveness without the framework */
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
BREAKPOINTS (Mobile-first)
|
|
||||||
================================ */
|
|
||||||
:root {
|
|
||||||
--breakpoint-sm: 640px;
|
|
||||||
--breakpoint-md: 768px;
|
|
||||||
--breakpoint-lg: 1024px;
|
|
||||||
--breakpoint-xl: 1280px;
|
|
||||||
--breakpoint-2xl: 1536px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
CONTAINER
|
|
||||||
================================ */
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
padding-left: var(--spacing-md);
|
|
||||||
padding-right: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.container { max-width: 640px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.container { max-width: 768px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.container { max-width: 1024px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.container { max-width: 1280px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
DISPLAY UTILITIES
|
|
||||||
================================ */
|
|
||||||
.hidden { display: none !important; }
|
|
||||||
.block { display: block !important; }
|
|
||||||
.inline-block { display: inline-block !important; }
|
|
||||||
.inline { display: inline !important; }
|
|
||||||
.flex { display: flex !important; }
|
|
||||||
.inline-flex { display: inline-flex !important; }
|
|
||||||
.grid { display: grid !important; }
|
|
||||||
|
|
||||||
/* Responsive Display */
|
|
||||||
@media (max-width: 639px) {
|
|
||||||
.sm\:hidden { display: none !important; }
|
|
||||||
.sm\:block { display: block !important; }
|
|
||||||
.sm\:flex { display: flex !important; }
|
|
||||||
.sm\:grid { display: grid !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) and (max-width: 767px) {
|
|
||||||
.md\:hidden { display: none !important; }
|
|
||||||
.md\:block { display: block !important; }
|
|
||||||
.md\:flex { display: flex !important; }
|
|
||||||
.md\:grid { display: grid !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) and (max-width: 1023px) {
|
|
||||||
.lg\:hidden { display: none !important; }
|
|
||||||
.lg\:block { display: block !important; }
|
|
||||||
.lg\:flex { display: flex !important; }
|
|
||||||
.lg\:grid { display: grid !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.xl\:hidden { display: none !important; }
|
|
||||||
.xl\:block { display: block !important; }
|
|
||||||
.xl\:flex { display: flex !important; }
|
|
||||||
.xl\:grid { display: grid !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
FLEXBOX UTILITIES
|
|
||||||
================================ */
|
|
||||||
.flex-row { flex-direction: row; }
|
|
||||||
.flex-col { flex-direction: column; }
|
|
||||||
.flex-row-reverse { flex-direction: row-reverse; }
|
|
||||||
.flex-col-reverse { flex-direction: column-reverse; }
|
|
||||||
|
|
||||||
.flex-wrap { flex-wrap: wrap; }
|
|
||||||
.flex-nowrap { flex-wrap: nowrap; }
|
|
||||||
|
|
||||||
.items-start { align-items: flex-start; }
|
|
||||||
.items-center { align-items: center; }
|
|
||||||
.items-end { align-items: flex-end; }
|
|
||||||
.items-stretch { align-items: stretch; }
|
|
||||||
|
|
||||||
.justify-start { justify-content: flex-start; }
|
|
||||||
.justify-center { justify-content: center; }
|
|
||||||
.justify-end { justify-content: flex-end; }
|
|
||||||
.justify-between { justify-content: space-between; }
|
|
||||||
.justify-around { justify-content: space-around; }
|
|
||||||
.justify-evenly { justify-content: space-evenly; }
|
|
||||||
|
|
||||||
.flex-1 { flex: 1 1 0%; }
|
|
||||||
.flex-auto { flex: 1 1 auto; }
|
|
||||||
.flex-none { flex: none; }
|
|
||||||
|
|
||||||
.gap-0 { gap: 0; }
|
|
||||||
.gap-1 { gap: var(--spacing-xs); }
|
|
||||||
.gap-2 { gap: var(--spacing-sm); }
|
|
||||||
.gap-3 { gap: var(--spacing-md); }
|
|
||||||
.gap-4 { gap: var(--spacing-lg); }
|
|
||||||
.gap-5 { gap: var(--spacing-xl); }
|
|
||||||
.gap-6 { gap: var(--spacing-2xl); }
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
GRID UTILITIES
|
|
||||||
================================ */
|
|
||||||
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
||||||
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
||||||
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
||||||
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
||||||
.grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
|
||||||
.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
|
|
||||||
|
|
||||||
/* Responsive Grid */
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.sm\:grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
||||||
.sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
||||||
.sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
||||||
.sm\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.md\:grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
||||||
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
||||||
.md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
||||||
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.lg\:grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
||||||
.lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
||||||
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
||||||
.lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
||||||
.lg\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
SPACING UTILITIES
|
|
||||||
================================ */
|
|
||||||
/* Margin */
|
|
||||||
.m-0 { margin: 0; }
|
|
||||||
.m-1 { margin: var(--spacing-xs); }
|
|
||||||
.m-2 { margin: var(--spacing-sm); }
|
|
||||||
.m-3 { margin: var(--spacing-md); }
|
|
||||||
.m-4 { margin: var(--spacing-lg); }
|
|
||||||
.m-5 { margin: var(--spacing-xl); }
|
|
||||||
.m-auto { margin: auto; }
|
|
||||||
|
|
||||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
|
||||||
.my-auto { margin-top: auto; margin-bottom: auto; }
|
|
||||||
|
|
||||||
.mt-0 { margin-top: 0; }
|
|
||||||
.mt-1 { margin-top: var(--spacing-xs); }
|
|
||||||
.mt-2 { margin-top: var(--spacing-sm); }
|
|
||||||
.mt-3 { margin-top: var(--spacing-md); }
|
|
||||||
.mt-4 { margin-top: var(--spacing-lg); }
|
|
||||||
.mt-5 { margin-top: var(--spacing-xl); }
|
|
||||||
|
|
||||||
.mb-0 { margin-bottom: 0; }
|
|
||||||
.mb-1 { margin-bottom: var(--spacing-xs); }
|
|
||||||
.mb-2 { margin-bottom: var(--spacing-sm); }
|
|
||||||
.mb-3 { margin-bottom: var(--spacing-md); }
|
|
||||||
.mb-4 { margin-bottom: var(--spacing-lg); }
|
|
||||||
.mb-5 { margin-bottom: var(--spacing-xl); }
|
|
||||||
|
|
||||||
.ml-0 { margin-left: 0; }
|
|
||||||
.ml-1 { margin-left: var(--spacing-xs); }
|
|
||||||
.ml-2 { margin-left: var(--spacing-sm); }
|
|
||||||
.ml-3 { margin-left: var(--spacing-md); }
|
|
||||||
.ml-4 { margin-left: var(--spacing-lg); }
|
|
||||||
.ml-auto { margin-left: auto; }
|
|
||||||
|
|
||||||
.mr-0 { margin-right: 0; }
|
|
||||||
.mr-1 { margin-right: var(--spacing-xs); }
|
|
||||||
.mr-2 { margin-right: var(--spacing-sm); }
|
|
||||||
.mr-3 { margin-right: var(--spacing-md); }
|
|
||||||
.mr-4 { margin-right: var(--spacing-lg); }
|
|
||||||
.mr-auto { margin-right: auto; }
|
|
||||||
|
|
||||||
/* Padding */
|
|
||||||
.p-0 { padding: 0; }
|
|
||||||
.p-1 { padding: var(--spacing-xs); }
|
|
||||||
.p-2 { padding: var(--spacing-sm); }
|
|
||||||
.p-3 { padding: var(--spacing-md); }
|
|
||||||
.p-4 { padding: var(--spacing-lg); }
|
|
||||||
.p-5 { padding: var(--spacing-xl); }
|
|
||||||
|
|
||||||
.px-0 { padding-left: 0; padding-right: 0; }
|
|
||||||
.px-1 { padding-left: var(--spacing-xs); padding-right: var(--spacing-xs); }
|
|
||||||
.px-2 { padding-left: var(--spacing-sm); padding-right: var(--spacing-sm); }
|
|
||||||
.px-3 { padding-left: var(--spacing-md); padding-right: var(--spacing-md); }
|
|
||||||
.px-4 { padding-left: var(--spacing-lg); padding-right: var(--spacing-lg); }
|
|
||||||
|
|
||||||
.py-0 { padding-top: 0; padding-bottom: 0; }
|
|
||||||
.py-1 { padding-top: var(--spacing-xs); padding-bottom: var(--spacing-xs); }
|
|
||||||
.py-2 { padding-top: var(--spacing-sm); padding-bottom: var(--spacing-sm); }
|
|
||||||
.py-3 { padding-top: var(--spacing-md); padding-bottom: var(--spacing-md); }
|
|
||||||
.py-4 { padding-top: var(--spacing-lg); padding-bottom: var(--spacing-lg); }
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
WIDTH & HEIGHT UTILITIES
|
|
||||||
================================ */
|
|
||||||
.w-full { width: 100%; }
|
|
||||||
.w-auto { width: auto; }
|
|
||||||
.w-1\/2 { width: 50%; }
|
|
||||||
.w-1\/3 { width: 33.333333%; }
|
|
||||||
.w-2\/3 { width: 66.666667%; }
|
|
||||||
.w-1\/4 { width: 25%; }
|
|
||||||
.w-3\/4 { width: 75%; }
|
|
||||||
|
|
||||||
.h-full { height: 100%; }
|
|
||||||
.h-auto { height: auto; }
|
|
||||||
.h-screen { height: 100vh; }
|
|
||||||
|
|
||||||
.min-h-screen { min-height: 100vh; }
|
|
||||||
.max-w-full { max-width: 100%; }
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
TEXT UTILITIES
|
|
||||||
================================ */
|
|
||||||
.text-left { text-align: left; }
|
|
||||||
.text-center { text-align: center; }
|
|
||||||
.text-right { text-align: right; }
|
|
||||||
|
|
||||||
.text-xs { font-size: var(--font-xs); }
|
|
||||||
.text-sm { font-size: var(--font-sm); }
|
|
||||||
.text-base { font-size: var(--font-base); }
|
|
||||||
.text-lg { font-size: var(--font-lg); }
|
|
||||||
.text-xl { font-size: var(--font-xl); }
|
|
||||||
.text-2xl { font-size: var(--font-2xl); }
|
|
||||||
.text-3xl { font-size: var(--font-3xl); }
|
|
||||||
|
|
||||||
.font-normal { font-weight: 400; }
|
|
||||||
.font-medium { font-weight: 500; }
|
|
||||||
.font-semibold { font-weight: 600; }
|
|
||||||
.font-bold { font-weight: 700; }
|
|
||||||
|
|
||||||
.uppercase { text-transform: uppercase; }
|
|
||||||
.lowercase { text-transform: lowercase; }
|
|
||||||
.capitalize { text-transform: capitalize; }
|
|
||||||
|
|
||||||
.truncate {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
POSITION UTILITIES
|
|
||||||
================================ */
|
|
||||||
.relative { position: relative; }
|
|
||||||
.absolute { position: absolute; }
|
|
||||||
.fixed { position: fixed; }
|
|
||||||
.sticky { position: sticky; }
|
|
||||||
|
|
||||||
.top-0 { top: 0; }
|
|
||||||
.right-0 { right: 0; }
|
|
||||||
.bottom-0 { bottom: 0; }
|
|
||||||
.left-0 { left: 0; }
|
|
||||||
|
|
||||||
.z-0 { z-index: 0; }
|
|
||||||
.z-10 { z-index: 10; }
|
|
||||||
.z-20 { z-index: 20; }
|
|
||||||
.z-30 { z-index: 30; }
|
|
||||||
.z-40 { z-index: 40; }
|
|
||||||
.z-50 { z-index: 50; }
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
OVERFLOW UTILITIES
|
|
||||||
================================ */
|
|
||||||
.overflow-auto { overflow: auto; }
|
|
||||||
.overflow-hidden { overflow: hidden; }
|
|
||||||
.overflow-visible { overflow: visible; }
|
|
||||||
.overflow-scroll { overflow: scroll; }
|
|
||||||
|
|
||||||
.overflow-x-auto { overflow-x: auto; }
|
|
||||||
.overflow-y-auto { overflow-y: auto; }
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
BORDER UTILITIES
|
|
||||||
================================ */
|
|
||||||
.rounded-none { border-radius: 0; }
|
|
||||||
.rounded-sm { border-radius: var(--radius-sm); }
|
|
||||||
.rounded { border-radius: var(--radius-md); }
|
|
||||||
.rounded-lg { border-radius: var(--radius-lg); }
|
|
||||||
.rounded-xl { border-radius: var(--radius-xl); }
|
|
||||||
.rounded-full { border-radius: var(--radius-full); }
|
|
||||||
|
|
||||||
.border { border: 1px solid var(--border-color); }
|
|
||||||
.border-0 { border: 0; }
|
|
||||||
.border-2 { border: 2px solid var(--border-color); }
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
SHADOW UTILITIES
|
|
||||||
================================ */
|
|
||||||
.shadow-none { box-shadow: none; }
|
|
||||||
.shadow-sm { box-shadow: var(--shadow-sm); }
|
|
||||||
.shadow { box-shadow: var(--shadow-md); }
|
|
||||||
.shadow-lg { box-shadow: var(--shadow-lg); }
|
|
||||||
.shadow-xl { box-shadow: var(--shadow-xl); }
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
RESPONSIVE HELPERS
|
|
||||||
================================ */
|
|
||||||
/* Hide on mobile, show on desktop */
|
|
||||||
.mobile-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.mobile-hidden {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show on mobile, hide on desktop */
|
|
||||||
.desktop-hidden {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.desktop-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tablet specific */
|
|
||||||
@media (min-width: 640px) and (max-width: 1023px) {
|
|
||||||
.tablet-only {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
CURSOR & POINTER EVENTS
|
|
||||||
================================ */
|
|
||||||
.cursor-pointer { cursor: pointer; }
|
|
||||||
.cursor-default { cursor: default; }
|
|
||||||
.cursor-not-allowed { cursor: not-allowed; }
|
|
||||||
.pointer-events-none { pointer-events: none; }
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
OPACITY UTILITIES
|
|
||||||
================================ */
|
|
||||||
.opacity-0 { opacity: 0; }
|
|
||||||
.opacity-25 { opacity: 0.25; }
|
|
||||||
.opacity-50 { opacity: 0.5; }
|
|
||||||
.opacity-75 { opacity: 0.75; }
|
|
||||||
.opacity-100 { opacity: 1; }
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
TRANSITION UTILITIES
|
|
||||||
================================ */
|
|
||||||
.transition { transition: all var(--transition-base); }
|
|
||||||
.transition-fast { transition: all var(--transition-fast); }
|
|
||||||
.transition-slow { transition: all var(--transition-slow); }
|
|
||||||
.transition-none { transition: none; }
|
|
||||||
601
static/css/vendor/vendor.css
vendored
601
static/css/vendor/vendor.css
vendored
@@ -1,601 +0,0 @@
|
|||||||
/* static/css/vendor/vendor.css */
|
|
||||||
/* Vendor-specific styles */
|
|
||||||
|
|
||||||
/* Vendor Header */
|
|
||||||
.vendor-header {
|
|
||||||
background: white;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
padding: 16px 24px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-header h1 {
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-logo {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-logo-img {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-name {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vendor Container */
|
|
||||||
.vendor-container {
|
|
||||||
display: flex;
|
|
||||||
min-height: calc(100vh - 64px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vendor Sidebar */
|
|
||||||
.vendor-sidebar {
|
|
||||||
width: 260px;
|
|
||||||
background: white;
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
padding: var(--spacing-lg) 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-sidebar-header {
|
|
||||||
padding: 0 var(--spacing-lg) var(--spacing-lg);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-status {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
margin-top: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vendor Content */
|
|
||||||
.vendor-content {
|
|
||||||
flex: 1;
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
overflow-y: auto;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vendor Dashboard Widgets */
|
|
||||||
.dashboard-widgets {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
||||||
gap: var(--spacing-lg);
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-title {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-icon {
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-content {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-footer {
|
|
||||||
padding-top: var(--spacing-md);
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-stat {
|
|
||||||
font-size: var(--font-4xl);
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-label {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Welcome Card */
|
|
||||||
.welcome-card {
|
|
||||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
|
||||||
color: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-icon {
|
|
||||||
font-size: 64px;
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-card h2 {
|
|
||||||
color: white;
|
|
||||||
font-size: var(--font-3xl);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-card p {
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vendor Info Card */
|
|
||||||
.vendor-info-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 0;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Coming Soon Badge */
|
|
||||||
.coming-soon {
|
|
||||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Product Grid */
|
|
||||||
.product-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
gap: var(--spacing-lg);
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
background: var(--gray-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-info {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-title {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-price {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--primary-color);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
line-height: 1.5;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-actions {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Order List */
|
|
||||||
.order-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-card:hover {
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
padding-bottom: var(--spacing-md);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-number {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-date {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-body {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-items {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-total {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tabs */
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
padding: 12px 24px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
margin-bottom: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab:hover {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab.active {
|
|
||||||
color: var(--primary-color);
|
|
||||||
border-bottom-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* File Upload */
|
|
||||||
.upload-area {
|
|
||||||
border: 2px dashed var(--border-color);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-area:hover,
|
|
||||||
.upload-area.dragover {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
background: rgba(102, 126, 234, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-text {
|
|
||||||
font-size: var(--font-base);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-hint {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list {
|
|
||||||
margin-top: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
background: var(--gray-50);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
font-size: var(--font-base);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-size {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-remove {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--danger-color);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Progress Bar */
|
|
||||||
.progress {
|
|
||||||
width: 100%;
|
|
||||||
height: 8px;
|
|
||||||
background: var(--gray-200);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
|
||||||
transition: width var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
text-align: center;
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-top: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Settings Form */
|
|
||||||
.settings-section {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-header {
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
padding-bottom: var(--spacing-md);
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-title {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-description {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: var(--spacing-lg);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.vendor-sidebar {
|
|
||||||
width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-widgets {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-grid {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.vendor-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-sidebar {
|
|
||||||
width: 100%;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
padding: var(--spacing-md) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-content {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-widgets,
|
|
||||||
.product-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-card {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-card h2 {
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
overflow-x: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.vendor-header {
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-header h1 {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-name {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-stat {
|
|
||||||
font-size: var(--font-3xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-section {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print Styles */
|
|
||||||
@media print {
|
|
||||||
.vendor-sidebar,
|
|
||||||
.vendor-header .header-right,
|
|
||||||
.product-actions,
|
|
||||||
.order-footer .btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vendor-content {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-section,
|
|
||||||
.order-card,
|
|
||||||
.product-card {
|
|
||||||
box-shadow: none;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
break-inside: avoid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,581 +0,0 @@
|
|||||||
/**
|
|
||||||
* Alpine.js Components for Multi-Tenant E-commerce Platform
|
|
||||||
* Universal component system for Admin, Vendor, and Shop sections
|
|
||||||
*/
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// BASE MODAL SYSTEM
|
|
||||||
// Universal modal functions used by all sections
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
window.baseModalSystem = function() {
|
|
||||||
return {
|
|
||||||
// Confirmation Modal State
|
|
||||||
confirmModal: {
|
|
||||||
show: false,
|
|
||||||
title: '',
|
|
||||||
message: '',
|
|
||||||
warning: '',
|
|
||||||
buttonText: 'Confirm',
|
|
||||||
buttonClass: 'btn-danger',
|
|
||||||
onConfirm: null,
|
|
||||||
onCancel: null
|
|
||||||
},
|
|
||||||
|
|
||||||
// Success Modal State
|
|
||||||
successModal: {
|
|
||||||
show: false,
|
|
||||||
title: 'Success',
|
|
||||||
message: '',
|
|
||||||
redirectUrl: null,
|
|
||||||
redirectDelay: 2000
|
|
||||||
},
|
|
||||||
|
|
||||||
// Error Modal State
|
|
||||||
errorModal: {
|
|
||||||
show: false,
|
|
||||||
title: 'Error',
|
|
||||||
message: '',
|
|
||||||
details: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
// Loading State
|
|
||||||
loading: false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show confirmation modal
|
|
||||||
* @param {Object} options - Modal configuration
|
|
||||||
*/
|
|
||||||
showConfirmModal(options) {
|
|
||||||
this.confirmModal = {
|
|
||||||
show: true,
|
|
||||||
title: options.title || 'Confirm Action',
|
|
||||||
message: options.message || 'Are you sure?',
|
|
||||||
warning: options.warning || '',
|
|
||||||
buttonText: options.buttonText || 'Confirm',
|
|
||||||
buttonClass: options.buttonClass || 'btn-danger',
|
|
||||||
onConfirm: options.onConfirm || null,
|
|
||||||
onCancel: options.onCancel || null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close confirmation modal
|
|
||||||
*/
|
|
||||||
closeConfirmModal() {
|
|
||||||
if (this.confirmModal.onCancel) {
|
|
||||||
this.confirmModal.onCancel();
|
|
||||||
}
|
|
||||||
this.confirmModal.show = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle confirmation action
|
|
||||||
*/
|
|
||||||
async handleConfirm() {
|
|
||||||
if (this.confirmModal.onConfirm) {
|
|
||||||
this.closeConfirmModal();
|
|
||||||
await this.confirmModal.onConfirm();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show success modal
|
|
||||||
* @param {Object} options - Modal configuration
|
|
||||||
*/
|
|
||||||
showSuccessModal(options) {
|
|
||||||
this.successModal = {
|
|
||||||
show: true,
|
|
||||||
title: options.title || 'Success',
|
|
||||||
message: options.message || 'Operation completed successfully',
|
|
||||||
redirectUrl: options.redirectUrl || null,
|
|
||||||
redirectDelay: options.redirectDelay || 2000
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-redirect if URL provided
|
|
||||||
if (this.successModal.redirectUrl) {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = this.successModal.redirectUrl;
|
|
||||||
}, this.successModal.redirectDelay);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close success modal
|
|
||||||
*/
|
|
||||||
closeSuccessModal() {
|
|
||||||
this.successModal.show = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show error modal
|
|
||||||
* @param {Object} options - Modal configuration
|
|
||||||
*/
|
|
||||||
showErrorModal(options) {
|
|
||||||
this.errorModal = {
|
|
||||||
show: true,
|
|
||||||
title: options.title || 'Error',
|
|
||||||
message: options.message || 'An error occurred',
|
|
||||||
details: options.details || ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close error modal
|
|
||||||
*/
|
|
||||||
closeErrorModal() {
|
|
||||||
this.errorModal.show = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show loading overlay
|
|
||||||
*/
|
|
||||||
showLoading() {
|
|
||||||
this.loading = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide loading overlay
|
|
||||||
*/
|
|
||||||
hideLoading() {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// ADMIN LAYOUT COMPONENT
|
|
||||||
// Header, Sidebar, Navigation, Modals for Admin Section
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
window.adminLayout = function() {
|
|
||||||
return {
|
|
||||||
...window.baseModalSystem(),
|
|
||||||
|
|
||||||
// Admin-specific state
|
|
||||||
user: null,
|
|
||||||
menuOpen: false,
|
|
||||||
currentPage: '',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize admin layout
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
this.currentPage = this.getCurrentPage();
|
|
||||||
await this.loadUserData();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load current admin user data
|
|
||||||
*/
|
|
||||||
async loadUserData() {
|
|
||||||
try {
|
|
||||||
const response = await apiClient.get('/admin/auth/me');
|
|
||||||
this.user = response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load user data:', error);
|
|
||||||
// Redirect to login if not authenticated
|
|
||||||
if (error.status === 401) {
|
|
||||||
window.location.href = '/admin/login.html';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current page name from URL
|
|
||||||
*/
|
|
||||||
getCurrentPage() {
|
|
||||||
const path = window.location.pathname;
|
|
||||||
const page = path.split('/').pop().replace('.html', '');
|
|
||||||
return page || 'dashboard';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if menu item is active
|
|
||||||
*/
|
|
||||||
isActive(page) {
|
|
||||||
return this.currentPage === page;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle mobile menu
|
|
||||||
*/
|
|
||||||
toggleMenu() {
|
|
||||||
this.menuOpen = !this.menuOpen;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show logout confirmation
|
|
||||||
*/
|
|
||||||
confirmLogout() {
|
|
||||||
this.showConfirmModal({
|
|
||||||
title: 'Confirm Logout',
|
|
||||||
message: 'Are you sure you want to logout?',
|
|
||||||
buttonText: 'Logout',
|
|
||||||
buttonClass: 'btn-primary',
|
|
||||||
onConfirm: () => this.logout()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform logout
|
|
||||||
*/
|
|
||||||
async logout() {
|
|
||||||
try {
|
|
||||||
this.showLoading();
|
|
||||||
await apiClient.post('/admin/auth/logout');
|
|
||||||
window.location.href = '/admin/login.html';
|
|
||||||
} catch (error) {
|
|
||||||
this.hideLoading();
|
|
||||||
this.showErrorModal({
|
|
||||||
message: 'Logout failed',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// VENDOR LAYOUT COMPONENT
|
|
||||||
// Header, Sidebar, Navigation, Modals for Vendor Dashboard
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
window.vendorLayout = function() {
|
|
||||||
return {
|
|
||||||
...window.baseModalSystem(),
|
|
||||||
|
|
||||||
// Vendor-specific state
|
|
||||||
user: null,
|
|
||||||
vendor: null,
|
|
||||||
menuOpen: false,
|
|
||||||
currentPage: '',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize vendor layout
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
this.currentPage = this.getCurrentPage();
|
|
||||||
await this.loadUserData();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load current vendor user data
|
|
||||||
*/
|
|
||||||
async loadUserData() {
|
|
||||||
try {
|
|
||||||
const response = await apiClient.get('/vendor/auth/me');
|
|
||||||
this.user = response.user;
|
|
||||||
this.vendor = response.vendor;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load user data:', error);
|
|
||||||
if (error.status === 401) {
|
|
||||||
window.location.href = '/vendor/login.html';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current page name from URL
|
|
||||||
*/
|
|
||||||
getCurrentPage() {
|
|
||||||
const path = window.location.pathname;
|
|
||||||
const page = path.split('/').pop().replace('.html', '');
|
|
||||||
return page || 'dashboard';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if menu item is active
|
|
||||||
*/
|
|
||||||
isActive(page) {
|
|
||||||
return this.currentPage === page;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle mobile menu
|
|
||||||
*/
|
|
||||||
toggleMenu() {
|
|
||||||
this.menuOpen = !this.menuOpen;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show logout confirmation
|
|
||||||
*/
|
|
||||||
confirmLogout() {
|
|
||||||
this.showConfirmModal({
|
|
||||||
title: 'Confirm Logout',
|
|
||||||
message: 'Are you sure you want to logout?',
|
|
||||||
buttonText: 'Logout',
|
|
||||||
buttonClass: 'btn-primary',
|
|
||||||
onConfirm: () => this.logout()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform logout
|
|
||||||
*/
|
|
||||||
async logout() {
|
|
||||||
try {
|
|
||||||
this.showLoading();
|
|
||||||
await apiClient.post('/vendor/auth/logout');
|
|
||||||
window.location.href = '/vendor/login.html';
|
|
||||||
} catch (error) {
|
|
||||||
this.hideLoading();
|
|
||||||
this.showErrorModal({
|
|
||||||
message: 'Logout failed',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// SHOP LAYOUT COMPONENT
|
|
||||||
// Header, Cart, Search, Navigation for Customer-Facing Shop
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
window.shopLayout = function() {
|
|
||||||
return {
|
|
||||||
...window.baseModalSystem(),
|
|
||||||
|
|
||||||
// Shop-specific state
|
|
||||||
vendor: null,
|
|
||||||
cart: null,
|
|
||||||
cartCount: 0,
|
|
||||||
sessionId: null,
|
|
||||||
searchQuery: '',
|
|
||||||
mobileMenuOpen: false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize shop layout
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
this.sessionId = this.getOrCreateSessionId();
|
|
||||||
await this.detectVendor();
|
|
||||||
if (this.vendor) {
|
|
||||||
await this.loadCart();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect vendor from subdomain or vendor code
|
|
||||||
*/
|
|
||||||
async detectVendor() {
|
|
||||||
try {
|
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const subdomain = hostname.split('.')[0];
|
|
||||||
|
|
||||||
// Try to get vendor by subdomain first
|
|
||||||
if (subdomain && subdomain !== 'localhost' && subdomain !== 'www') {
|
|
||||||
this.vendor = await apiClient.get(`/public/vendors/by-subdomain/${subdomain}`);
|
|
||||||
} else {
|
|
||||||
// Fallback: Try to get vendor code from URL or localStorage
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const vendorCode = urlParams.get('vendor') || localStorage.getItem('vendorCode');
|
|
||||||
|
|
||||||
if (vendorCode) {
|
|
||||||
this.vendor = await apiClient.get(`/public/vendors/by-code/${vendorCode}`);
|
|
||||||
localStorage.setItem('vendorCode', vendorCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to detect vendor:', error);
|
|
||||||
this.showErrorModal({
|
|
||||||
message: 'Vendor not found',
|
|
||||||
details: 'Unable to identify the store. Please check the URL.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or create session ID for cart
|
|
||||||
*/
|
|
||||||
getOrCreateSessionId() {
|
|
||||||
let sessionId = localStorage.getItem('cartSessionId');
|
|
||||||
if (!sessionId) {
|
|
||||||
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
||||||
localStorage.setItem('cartSessionId', sessionId);
|
|
||||||
}
|
|
||||||
return sessionId;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load cart from API
|
|
||||||
*/
|
|
||||||
async loadCart() {
|
|
||||||
if (!this.vendor) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.cart = await apiClient.get(
|
|
||||||
`/public/vendors/${this.vendor.id}/cart/${this.sessionId}`
|
|
||||||
);
|
|
||||||
this.updateCartCount();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load cart:', error);
|
|
||||||
this.cart = { items: [] };
|
|
||||||
this.cartCount = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update cart item count
|
|
||||||
*/
|
|
||||||
updateCartCount() {
|
|
||||||
if (this.cart && this.cart.items) {
|
|
||||||
this.cartCount = this.cart.items.reduce((sum, item) => sum + item.quantity, 0);
|
|
||||||
} else {
|
|
||||||
this.cartCount = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add item to cart
|
|
||||||
*/
|
|
||||||
async addToCart(productId, quantity = 1) {
|
|
||||||
if (!this.vendor) {
|
|
||||||
this.showErrorModal({ message: 'Vendor not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.showLoading();
|
|
||||||
await apiClient.post(
|
|
||||||
`/public/vendors/${this.vendor.id}/cart/${this.sessionId}/items`,
|
|
||||||
{ product_id: productId, quantity }
|
|
||||||
);
|
|
||||||
await this.loadCart();
|
|
||||||
this.hideLoading();
|
|
||||||
this.showSuccessModal({
|
|
||||||
title: 'Added to Cart',
|
|
||||||
message: 'Product added successfully'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.hideLoading();
|
|
||||||
this.showErrorModal({
|
|
||||||
message: 'Failed to add to cart',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle mobile menu
|
|
||||||
*/
|
|
||||||
toggleMobileMenu() {
|
|
||||||
this.mobileMenuOpen = !this.mobileMenuOpen;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle search
|
|
||||||
*/
|
|
||||||
handleSearch() {
|
|
||||||
if (this.searchQuery.trim()) {
|
|
||||||
window.location.href = `/shop/products.html?search=${encodeURIComponent(this.searchQuery)}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to cart page
|
|
||||||
*/
|
|
||||||
goToCart() {
|
|
||||||
window.location.href = '/shop/cart.html';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// SHOP ACCOUNT LAYOUT COMPONENT
|
|
||||||
// Layout for customer account area (orders, profile, addresses)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
window.shopAccountLayout = function() {
|
|
||||||
return {
|
|
||||||
...window.shopLayout(),
|
|
||||||
|
|
||||||
// Account-specific state
|
|
||||||
customer: null,
|
|
||||||
currentPage: '',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize shop account layout
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
this.currentPage = this.getCurrentPage();
|
|
||||||
this.sessionId = this.getOrCreateSessionId();
|
|
||||||
await this.detectVendor();
|
|
||||||
await this.loadCustomerData();
|
|
||||||
if (this.vendor) {
|
|
||||||
await this.loadCart();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load customer data
|
|
||||||
*/
|
|
||||||
async loadCustomerData() {
|
|
||||||
if (!this.vendor) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await apiClient.get(
|
|
||||||
`/public/vendors/${this.vendor.id}/customers/me`
|
|
||||||
);
|
|
||||||
this.customer = response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load customer data:', error);
|
|
||||||
// Redirect to login if not authenticated
|
|
||||||
if (error.status === 401) {
|
|
||||||
window.location.href = `/shop/account/login.html?redirect=${encodeURIComponent(window.location.pathname)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current page name from URL
|
|
||||||
*/
|
|
||||||
getCurrentPage() {
|
|
||||||
const path = window.location.pathname;
|
|
||||||
const page = path.split('/').pop().replace('.html', '');
|
|
||||||
return page || 'orders';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if menu item is active
|
|
||||||
*/
|
|
||||||
isActive(page) {
|
|
||||||
return this.currentPage === page;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show logout confirmation
|
|
||||||
*/
|
|
||||||
confirmLogout() {
|
|
||||||
this.showConfirmModal({
|
|
||||||
title: 'Confirm Logout',
|
|
||||||
message: 'Are you sure you want to logout?',
|
|
||||||
buttonText: 'Logout',
|
|
||||||
buttonClass: 'btn-primary',
|
|
||||||
onConfirm: () => this.logoutCustomer()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform customer logout
|
|
||||||
*/
|
|
||||||
async logoutCustomer() {
|
|
||||||
if (!this.vendor) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.showLoading();
|
|
||||||
await apiClient.post(`/public/vendors/${this.vendor.id}/customers/logout`);
|
|
||||||
window.location.href = '/shop/home.html';
|
|
||||||
} catch (error) {
|
|
||||||
this.hideLoading();
|
|
||||||
this.showErrorModal({
|
|
||||||
message: 'Logout failed',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// File upload utilities
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
/**
|
|
||||||
* Modal System Helper Functions
|
|
||||||
* Utility functions for modal operations across all sections
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.modalHelpers = {
|
|
||||||
/**
|
|
||||||
* Show a simple confirmation dialog
|
|
||||||
* Returns a Promise that resolves with true/false
|
|
||||||
*/
|
|
||||||
async confirm(options) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const component = Alpine.$data(document.body);
|
|
||||||
|
|
||||||
component.showConfirmModal({
|
|
||||||
title: options.title || 'Confirm Action',
|
|
||||||
message: options.message || 'Are you sure?',
|
|
||||||
warning: options.warning || '',
|
|
||||||
buttonText: options.buttonText || 'Confirm',
|
|
||||||
buttonClass: options.buttonClass || 'btn-danger',
|
|
||||||
onConfirm: () => resolve(true),
|
|
||||||
onCancel: () => resolve(false)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a success message
|
|
||||||
*/
|
|
||||||
success(message, options = {}) {
|
|
||||||
const component = Alpine.$data(document.body);
|
|
||||||
|
|
||||||
component.showSuccessModal({
|
|
||||||
title: options.title || 'Success',
|
|
||||||
message: message,
|
|
||||||
redirectUrl: options.redirectUrl || null,
|
|
||||||
redirectDelay: options.redirectDelay || 2000
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show an error message
|
|
||||||
*/
|
|
||||||
error(message, details = '') {
|
|
||||||
const component = Alpine.$data(document.body);
|
|
||||||
|
|
||||||
component.showErrorModal({
|
|
||||||
title: 'Error',
|
|
||||||
message: message,
|
|
||||||
details: details
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show API error with proper formatting
|
|
||||||
*/
|
|
||||||
apiError(error) {
|
|
||||||
const component = Alpine.$data(document.body);
|
|
||||||
|
|
||||||
let message = 'An error occurred';
|
|
||||||
let details = '';
|
|
||||||
|
|
||||||
if (error.message) {
|
|
||||||
message = error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.details) {
|
|
||||||
details = typeof error.details === 'string'
|
|
||||||
? error.details
|
|
||||||
: JSON.stringify(error.details, null, 2);
|
|
||||||
} else if (error.error_code) {
|
|
||||||
details = `Error Code: ${error.error_code}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
component.showErrorModal({
|
|
||||||
title: 'Error',
|
|
||||||
message: message,
|
|
||||||
details: details
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show loading overlay
|
|
||||||
*/
|
|
||||||
showLoading() {
|
|
||||||
const component = Alpine.$data(document.body);
|
|
||||||
component.showLoading();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide loading overlay
|
|
||||||
*/
|
|
||||||
hideLoading() {
|
|
||||||
const component = Alpine.$data(document.body);
|
|
||||||
component.hideLoading();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute an async operation with loading state
|
|
||||||
*/
|
|
||||||
async withLoading(asyncFunction) {
|
|
||||||
try {
|
|
||||||
this.showLoading();
|
|
||||||
const result = await asyncFunction();
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
this.hideLoading();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute an async operation with error handling
|
|
||||||
*/
|
|
||||||
async withErrorHandling(asyncFunction, errorMessage = 'Operation failed') {
|
|
||||||
try {
|
|
||||||
return await asyncFunction();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Operation error:', error);
|
|
||||||
this.apiError({
|
|
||||||
message: errorMessage,
|
|
||||||
details: error.message || error.toString()
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute an async operation with both loading and error handling
|
|
||||||
*/
|
|
||||||
async execute(asyncFunction, options = {}) {
|
|
||||||
const {
|
|
||||||
errorMessage = 'Operation failed',
|
|
||||||
successMessage = null,
|
|
||||||
redirectUrl = null
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.showLoading();
|
|
||||||
const result = await asyncFunction();
|
|
||||||
|
|
||||||
if (successMessage) {
|
|
||||||
this.success(successMessage, { redirectUrl });
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Operation error:', error);
|
|
||||||
this.apiError({
|
|
||||||
message: errorMessage,
|
|
||||||
details: error.message || error.toString()
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.hideLoading();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm a destructive action
|
|
||||||
*/
|
|
||||||
async confirmDelete(itemName, itemType = 'item') {
|
|
||||||
return this.confirm({
|
|
||||||
title: `Delete ${itemType}`,
|
|
||||||
message: `Are you sure you want to delete "${itemName}"?`,
|
|
||||||
warning: 'This action cannot be undone.',
|
|
||||||
buttonText: 'Delete',
|
|
||||||
buttonClass: 'btn-danger'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm logout
|
|
||||||
*/
|
|
||||||
async confirmLogout() {
|
|
||||||
return this.confirm({
|
|
||||||
title: 'Confirm Logout',
|
|
||||||
message: 'Are you sure you want to logout?',
|
|
||||||
buttonText: 'Logout',
|
|
||||||
buttonClass: 'btn-primary'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show validation errors
|
|
||||||
*/
|
|
||||||
validationError(errors) {
|
|
||||||
let message = 'Please correct the following errors:';
|
|
||||||
let details = '';
|
|
||||||
|
|
||||||
if (Array.isArray(errors)) {
|
|
||||||
details = errors.join('\n');
|
|
||||||
} else if (typeof errors === 'object') {
|
|
||||||
details = Object.entries(errors)
|
|
||||||
.map(([field, error]) => `${field}: ${error}`)
|
|
||||||
.join('\n');
|
|
||||||
} else {
|
|
||||||
details = errors.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.error(message, details);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Shorthand aliases for convenience
|
|
||||||
window.showConfirm = window.modalHelpers.confirm.bind(window.modalHelpers);
|
|
||||||
window.showSuccess = window.modalHelpers.success.bind(window.modalHelpers);
|
|
||||||
window.showError = window.modalHelpers.error.bind(window.modalHelpers);
|
|
||||||
window.showLoading = window.modalHelpers.showLoading.bind(window.modalHelpers);
|
|
||||||
window.hideLoading = window.modalHelpers.hideLoading.bind(window.modalHelpers);
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
/**
|
|
||||||
* Universal Modal Templates
|
|
||||||
* Shared across all sections: Admin, Vendor, and Shop
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.modalTemplates = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirmation Modal
|
|
||||||
*/
|
|
||||||
confirmModal: () => `
|
|
||||||
<div x-show="confirmModal.show"
|
|
||||||
x-cloak
|
|
||||||
class="modal-backdrop"
|
|
||||||
@click.self="closeConfirmModal()">
|
|
||||||
<div class="modal-container" @click.stop>
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3 class="modal-title" x-text="confirmModal.title"></h3>
|
|
||||||
<button @click="closeConfirmModal()" class="modal-close">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p class="modal-message" x-text="confirmModal.message"></p>
|
|
||||||
<div x-show="confirmModal.warning" class="modal-warning">
|
|
||||||
<p x-text="confirmModal.warning"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button @click="closeConfirmModal()" class="btn btn-outline">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button @click="handleConfirm()"
|
|
||||||
class="btn"
|
|
||||||
:class="confirmModal.buttonClass"
|
|
||||||
x-text="confirmModal.buttonText">
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Success Modal
|
|
||||||
*/
|
|
||||||
successModal: () => `
|
|
||||||
<div x-show="successModal.show"
|
|
||||||
x-cloak
|
|
||||||
class="modal-backdrop"
|
|
||||||
@click.self="closeSuccessModal()">
|
|
||||||
<div class="modal-container" @click.stop>
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3 class="modal-title" x-text="successModal.title"></h3>
|
|
||||||
<button @click="closeSuccessModal()" class="modal-close">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="modal-icon success">
|
|
||||||
<i class="fas fa-check"></i>
|
|
||||||
</div>
|
|
||||||
<p class="modal-message text-center" x-text="successModal.message"></p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button @click="closeSuccessModal()" class="btn btn-primary">
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error Modal
|
|
||||||
*/
|
|
||||||
errorModal: () => `
|
|
||||||
<div x-show="errorModal.show"
|
|
||||||
x-cloak
|
|
||||||
class="modal-backdrop"
|
|
||||||
@click.self="closeErrorModal()">
|
|
||||||
<div class="modal-container" @click.stop>
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3 class="modal-title" x-text="errorModal.title"></h3>
|
|
||||||
<button @click="closeErrorModal()" class="modal-close">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="modal-icon error">
|
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
|
||||||
</div>
|
|
||||||
<p class="modal-message text-center" x-text="errorModal.message"></p>
|
|
||||||
<div x-show="errorModal.details" class="modal-details" x-text="errorModal.details"></div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button @click="closeErrorModal()" class="btn btn-primary">
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loading Overlay
|
|
||||||
*/
|
|
||||||
loadingOverlay: () => `
|
|
||||||
<div x-show="loading"
|
|
||||||
x-cloak
|
|
||||||
class="loading-overlay">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Notification handling
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Search functionality
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Vendor context detection and management
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Customer account
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Shopping cart
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Product browsing
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Checkout process
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// Product search
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shop Layout Templates
|
|
||||||
* Header and Navigation specific to Customer-Facing Shop
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.shopLayoutTemplates = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shop Header
|
|
||||||
*/
|
|
||||||
header: () => `
|
|
||||||
<header class="shop-header">
|
|
||||||
<div class="shop-header-top">
|
|
||||||
<a href="/shop/home.html" class="shop-logo" x-text="vendor?.name || 'Shop'"></a>
|
|
||||||
|
|
||||||
<!-- Search Bar -->
|
|
||||||
<div class="shop-search">
|
|
||||||
<form @submit.prevent="handleSearch()" class="search-form">
|
|
||||||
<input type="text"
|
|
||||||
x-model="searchQuery"
|
|
||||||
class="search-input"
|
|
||||||
placeholder="Search products...">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i class="fas fa-search"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="shop-actions">
|
|
||||||
<button @click="goToCart()" class="cart-button">
|
|
||||||
<i class="fas fa-shopping-cart"></i>
|
|
||||||
<span x-show="cartCount > 0"
|
|
||||||
x-text="cartCount"
|
|
||||||
class="cart-count">
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<a href="/shop/account/login.html" class="btn btn-outline">
|
|
||||||
<i class="fas fa-user"></i> Account
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation -->
|
|
||||||
<nav class="shop-nav">
|
|
||||||
<a href="/shop/home.html"
|
|
||||||
class="shop-nav-item"
|
|
||||||
:class="{ 'active': isActive('home') }">
|
|
||||||
Home
|
|
||||||
</a>
|
|
||||||
<a href="/shop/products.html"
|
|
||||||
class="shop-nav-item"
|
|
||||||
:class="{ 'active': isActive('products') }">
|
|
||||||
Products
|
|
||||||
</a>
|
|
||||||
<a href="/shop/categories.html"
|
|
||||||
class="shop-nav-item"
|
|
||||||
:class="{ 'active': isActive('categories') }">
|
|
||||||
Categories
|
|
||||||
</a>
|
|
||||||
<a href="/shop/about.html"
|
|
||||||
class="shop-nav-item"
|
|
||||||
:class="{ 'active': isActive('about') }">
|
|
||||||
About
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
`,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shop Account Sidebar
|
|
||||||
*/
|
|
||||||
accountSidebar: () => `
|
|
||||||
<aside class="account-sidebar">
|
|
||||||
<nav>
|
|
||||||
<a href="/shop/account/orders.html"
|
|
||||||
class="account-nav-item"
|
|
||||||
:class="{ 'active': isActive('orders') }">
|
|
||||||
<i class="fas fa-shopping-bag"></i>
|
|
||||||
<span>My Orders</span>
|
|
||||||
</a>
|
|
||||||
<a href="/shop/account/profile.html"
|
|
||||||
class="account-nav-item"
|
|
||||||
:class="{ 'active': isActive('profile') }">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
<span>Profile</span>
|
|
||||||
</a>
|
|
||||||
<a href="/shop/account/addresses.html"
|
|
||||||
class="account-nav-item"
|
|
||||||
:class="{ 'active': isActive('addresses') }">
|
|
||||||
<i class="fas fa-map-marker-alt"></i>
|
|
||||||
<span>Addresses</span>
|
|
||||||
</a>
|
|
||||||
<button @click="confirmLogout()"
|
|
||||||
class="account-nav-item"
|
|
||||||
style="width: 100%; text-align: left; background: none; border: none; cursor: pointer;">
|
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
|
||||||
<span>Logout</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
`
|
|
||||||
};
|
|
||||||
113
static/js/vendor/dashboard.js
vendored
113
static/js/vendor/dashboard.js
vendored
@@ -1,113 +0,0 @@
|
|||||||
// Vendor Dashboard Component
|
|
||||||
function vendorDashboard() {
|
|
||||||
return {
|
|
||||||
currentUser: {},
|
|
||||||
vendor: null,
|
|
||||||
vendorRole: '',
|
|
||||||
currentSection: 'dashboard',
|
|
||||||
loading: false,
|
|
||||||
stats: {
|
|
||||||
products_count: 0,
|
|
||||||
orders_count: 0,
|
|
||||||
customers_count: 0,
|
|
||||||
revenue: 0
|
|
||||||
},
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (!this.checkAuth()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.loadDashboard();
|
|
||||||
},
|
|
||||||
|
|
||||||
checkAuth() {
|
|
||||||
const token = localStorage.getItem('vendor_token');
|
|
||||||
const user = localStorage.getItem('vendor_user');
|
|
||||||
const vendorContext = localStorage.getItem('vendor_context');
|
|
||||||
const vendorRole = localStorage.getItem('vendor_role');
|
|
||||||
|
|
||||||
if (!token || !user || !vendorContext) {
|
|
||||||
// Get vendor code from URL
|
|
||||||
const vendorCode = this.getVendorCodeFromUrl();
|
|
||||||
const redirectUrl = vendorCode ?
|
|
||||||
`/vendor/${vendorCode}/login` :
|
|
||||||
'/static/vendor/login.html';
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.currentUser = JSON.parse(user);
|
|
||||||
this.vendor = JSON.parse(vendorContext);
|
|
||||||
this.vendorRole = vendorRole || 'Member';
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing stored data:', e);
|
|
||||||
localStorage.removeItem('vendor_token');
|
|
||||||
localStorage.removeItem('vendor_user');
|
|
||||||
localStorage.removeItem('vendor_context');
|
|
||||||
localStorage.removeItem('vendor_role');
|
|
||||||
window.location.href = '/static/vendor/login.html';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getVendorCodeFromUrl() {
|
|
||||||
// Try to get vendor code from URL path
|
|
||||||
const pathParts = window.location.pathname.split('/').filter(p => p);
|
|
||||||
const vendorIndex = pathParts.indexOf('vendor');
|
|
||||||
if (vendorIndex !== -1 && pathParts[vendorIndex + 1]) {
|
|
||||||
const code = pathParts[vendorIndex + 1];
|
|
||||||
if (!['login', 'dashboard', 'admin', 'products', 'orders'].includes(code.toLowerCase())) {
|
|
||||||
return code.toUpperCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to query parameter
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
return urlParams.get('vendor');
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleLogout() {
|
|
||||||
const confirmed = await Utils.confirm(
|
|
||||||
'Are you sure you want to logout?',
|
|
||||||
'Confirm Logout'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
localStorage.removeItem('vendor_token');
|
|
||||||
localStorage.removeItem('vendor_user');
|
|
||||||
localStorage.removeItem('vendor_context');
|
|
||||||
localStorage.removeItem('vendor_role');
|
|
||||||
|
|
||||||
Utils.showToast('Logged out successfully', 'success', 2000);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = `/vendor/${this.vendor.vendor_code}/login`;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadDashboard() {
|
|
||||||
this.loading = true;
|
|
||||||
try {
|
|
||||||
// In future slices, load actual dashboard data
|
|
||||||
// const data = await apiClient.get(`/vendor/dashboard/stats`);
|
|
||||||
// this.stats = data;
|
|
||||||
|
|
||||||
// For now, show placeholder data
|
|
||||||
this.stats = {
|
|
||||||
products_count: 0,
|
|
||||||
orders_count: 0,
|
|
||||||
customers_count: 0,
|
|
||||||
revenue: 0
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load dashboard:', error);
|
|
||||||
Utils.showToast('Failed to load dashboard data', 'error');
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
170
static/js/vendor/login.js
vendored
170
static/js/vendor/login.js
vendored
@@ -1,170 +0,0 @@
|
|||||||
// Vendor Login Component
|
|
||||||
function vendorLogin() {
|
|
||||||
return {
|
|
||||||
vendor: null,
|
|
||||||
credentials: {
|
|
||||||
username: '',
|
|
||||||
password: ''
|
|
||||||
},
|
|
||||||
loading: false,
|
|
||||||
checked: false,
|
|
||||||
error: null,
|
|
||||||
success: null,
|
|
||||||
errors: {},
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// Check if already logged in
|
|
||||||
if (this.checkExistingAuth()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect vendor from URL
|
|
||||||
this.detectVendor();
|
|
||||||
},
|
|
||||||
|
|
||||||
checkExistingAuth() {
|
|
||||||
const token = localStorage.getItem('vendor_token');
|
|
||||||
const vendorContext = localStorage.getItem('vendor_context');
|
|
||||||
|
|
||||||
if (token && vendorContext) {
|
|
||||||
try {
|
|
||||||
const vendor = JSON.parse(vendorContext);
|
|
||||||
window.location.href = `/vendor/${vendor.vendor_code}/dashboard`;
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
localStorage.removeItem('vendor_token');
|
|
||||||
localStorage.removeItem('vendor_context');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
async detectVendor() {
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const vendorCode = this.getVendorCodeFromUrl();
|
|
||||||
|
|
||||||
if (!vendorCode) {
|
|
||||||
this.error = 'Vendor code not found in URL. Please use the correct vendor login link.';
|
|
||||||
this.checked = true;
|
|
||||||
this.loading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Detected vendor code:', vendorCode);
|
|
||||||
|
|
||||||
// Fetch vendor information
|
|
||||||
const response = await fetch(`/api/v1/public/vendors/by-code/${vendorCode}`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Vendor not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.vendor = await response.json();
|
|
||||||
this.checked = true;
|
|
||||||
console.log('Loaded vendor:', this.vendor);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error detecting vendor:', error);
|
|
||||||
this.error = 'Unable to load vendor information. The vendor may not exist or is inactive.';
|
|
||||||
this.checked = true;
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getVendorCodeFromUrl() {
|
|
||||||
// Try multiple methods to get vendor code
|
|
||||||
|
|
||||||
// Method 1: From URL path /vendor/VENDORCODE/login or /vendor/VENDORCODE/
|
|
||||||
const pathParts = window.location.pathname.split('/').filter(p => p);
|
|
||||||
const vendorIndex = pathParts.indexOf('vendor');
|
|
||||||
if (vendorIndex !== -1 && pathParts[vendorIndex + 1]) {
|
|
||||||
const code = pathParts[vendorIndex + 1];
|
|
||||||
// Don't return if it's a generic route like 'login', 'dashboard', etc.
|
|
||||||
if (!['login', 'dashboard', 'admin', 'products', 'orders'].includes(code.toLowerCase())) {
|
|
||||||
return code.toUpperCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: From query parameter ?vendor=VENDORCODE
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const queryVendor = urlParams.get('vendor');
|
|
||||||
if (queryVendor) {
|
|
||||||
return queryVendor.toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 3: From subdomain (for production)
|
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const parts = hostname.split('.');
|
|
||||||
if (parts.length > 2 && parts[0] !== 'www') {
|
|
||||||
// Assume subdomain is vendor code
|
|
||||||
return parts[0].toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
clearErrors() {
|
|
||||||
this.error = null;
|
|
||||||
this.errors = {};
|
|
||||||
},
|
|
||||||
|
|
||||||
validateForm() {
|
|
||||||
this.clearErrors();
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
if (!this.credentials.username.trim()) {
|
|
||||||
this.errors.username = 'Username is required';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.credentials.password) {
|
|
||||||
this.errors.password = 'Password is required';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleLogin() {
|
|
||||||
if (!this.validateForm()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
this.clearErrors();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await apiClient.post('/vendor/auth/login', {
|
|
||||||
username: this.credentials.username.trim(),
|
|
||||||
password: this.credentials.password,
|
|
||||||
vendor_code: this.vendor.vendor_code
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store authentication data
|
|
||||||
localStorage.setItem('vendor_token', response.access_token);
|
|
||||||
localStorage.setItem('vendor_user', JSON.stringify(response.user));
|
|
||||||
localStorage.setItem('vendor_context', JSON.stringify(response.vendor));
|
|
||||||
localStorage.setItem('vendor_role', response.vendor_role);
|
|
||||||
|
|
||||||
// Show success message
|
|
||||||
this.success = 'Login successful! Redirecting...';
|
|
||||||
Utils.showToast('Login successful!', 'success', 2000);
|
|
||||||
|
|
||||||
// Redirect after short delay
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = `/vendor/${this.vendor.vendor_code}/dashboard`;
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login error:', error);
|
|
||||||
this.error = error.message || 'Login failed. Please check your credentials.';
|
|
||||||
Utils.showToast(this.error, 'error');
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
static/js/vendor/marketplace.js
vendored
1
static/js/vendor/marketplace.js
vendored
@@ -1 +0,0 @@
|
|||||||
// Marketplace integration
|
|
||||||
1
static/js/vendor/media.js
vendored
1
static/js/vendor/media.js
vendored
@@ -1 +0,0 @@
|
|||||||
// Media management
|
|
||||||
1
static/js/vendor/orders.js
vendored
1
static/js/vendor/orders.js
vendored
@@ -1 +0,0 @@
|
|||||||
// Order management
|
|
||||||
1
static/js/vendor/payments.js
vendored
1
static/js/vendor/payments.js
vendored
@@ -1 +0,0 @@
|
|||||||
// Payment configuration
|
|
||||||
1
static/js/vendor/products.js
vendored
1
static/js/vendor/products.js
vendored
@@ -1 +0,0 @@
|
|||||||
// Catalog management
|
|
||||||
67
static/js/vendor/vendor-layout-templates.js
vendored
67
static/js/vendor/vendor-layout-templates.js
vendored
@@ -1,67 +0,0 @@
|
|||||||
/**
|
|
||||||
* Vendor Layout Templates
|
|
||||||
* Header and Sidebar specific to Vendor Dashboard
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.vendorLayoutTemplates = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vendor Header
|
|
||||||
*/
|
|
||||||
header: () => `
|
|
||||||
<header class="vendor-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<button @click="toggleMenu()" class="menu-toggle">
|
|
||||||
<i class="fas fa-bars"></i>
|
|
||||||
</button>
|
|
||||||
<h1 class="header-title">Vendor Dashboard</h1>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<span class="user-name" x-text="vendor?.name || 'Vendor'"></span>
|
|
||||||
<button @click="confirmLogout()" class="btn-logout">
|
|
||||||
<i class="fas fa-sign-out-alt"></i> Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
`,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vendor Sidebar
|
|
||||||
*/
|
|
||||||
sidebar: () => `
|
|
||||||
<aside class="vendor-sidebar" :class="{ 'open': menuOpen }">
|
|
||||||
<nav class="sidebar-nav">
|
|
||||||
<a href="/vendor/dashboard.html"
|
|
||||||
class="nav-item"
|
|
||||||
:class="{ 'active': isActive('dashboard') }">
|
|
||||||
<i class="fas fa-tachometer-alt"></i>
|
|
||||||
<span>Dashboard</span>
|
|
||||||
</a>
|
|
||||||
<a href="/vendor/products.html"
|
|
||||||
class="nav-item"
|
|
||||||
:class="{ 'active': isActive('products') }">
|
|
||||||
<i class="fas fa-box"></i>
|
|
||||||
<span>Products</span>
|
|
||||||
</a>
|
|
||||||
<a href="/vendor/orders.html"
|
|
||||||
class="nav-item"
|
|
||||||
:class="{ 'active': isActive('orders') }">
|
|
||||||
<i class="fas fa-shopping-bag"></i>
|
|
||||||
<span>Orders</span>
|
|
||||||
</a>
|
|
||||||
<a href="/vendor/customers.html"
|
|
||||||
class="nav-item"
|
|
||||||
:class="{ 'active': isActive('customers') }">
|
|
||||||
<i class="fas fa-users"></i>
|
|
||||||
<span>Customers</span>
|
|
||||||
</a>
|
|
||||||
<a href="/vendor/settings.html"
|
|
||||||
class="nav-item"
|
|
||||||
:class="{ 'active': isActive('settings') }">
|
|
||||||
<i class="fas fa-cog"></i>
|
|
||||||
<span>Settings</span>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
`
|
|
||||||
};
|
|
||||||
@@ -107,7 +107,13 @@ const Icons = {
|
|||||||
'database': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>`,
|
'database': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>`,
|
||||||
'light-bulb': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>`,
|
'light-bulb': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>`,
|
||||||
'book-open': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>`,
|
'book-open': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>`,
|
||||||
'play': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`
|
'play': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,
|
||||||
|
|
||||||
|
// Additional UI Icons
|
||||||
|
'table': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>`,
|
||||||
|
'cursor-click': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"/></svg>`,
|
||||||
|
'view-grid-add': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z"/></svg>`,
|
||||||
|
'code': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>`
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Address management</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<-- Address management -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title><!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Login - {{ vendor.name }}</title>
|
|
||||||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/shared/auth.css">
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="auth-page">
|
|
||||||
<div class="login-container"
|
|
||||||
x-data="customerLogin()"
|
|
||||||
x-init="checkRegistrationSuccess()"
|
|
||||||
data-vendor-id="{{ vendor.id }}"
|
|
||||||
data-vendor-name="{{ vendor.name }}"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="login-header">
|
|
||||||
{% if vendor.logo_url %}
|
|
||||||
<img src="{{ vendor.logo_url }}" alt="{{ vendor.name }}" class="auth-logo">
|
|
||||||
{% else %}
|
|
||||||
<div class="auth-logo">🛒</div>
|
|
||||||
{% endif %}
|
|
||||||
<h1>Welcome Back</h1>
|
|
||||||
<p>Sign in to {{ vendor.name }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Alert Box -->
|
|
||||||
<div x-show="alert.show"
|
|
||||||
x-transition
|
|
||||||
:class="'alert alert-' + alert.type"
|
|
||||||
x-text="alert.message"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Login Form -->
|
|
||||||
<form @submit.prevent="handleLogin">
|
|
||||||
<!-- Email -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">Email Address</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
x-model="credentials.email"
|
|
||||||
required
|
|
||||||
placeholder="your@email.com"
|
|
||||||
:class="{ 'error': errors.email }"
|
|
||||||
@input="clearAllErrors()"
|
|
||||||
>
|
|
||||||
<div x-show="errors.email"
|
|
||||||
x-text="errors.email"
|
|
||||||
class="error-message show"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Password -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<div class="password-group">
|
|
||||||
<input
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
id="password"
|
|
||||||
x-model="credentials.password"
|
|
||||||
required
|
|
||||||
placeholder="Enter your password"
|
|
||||||
:class="{ 'error': errors.password }"
|
|
||||||
@input="clearAllErrors()"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="password-toggle"
|
|
||||||
@click="showPassword = !showPassword"
|
|
||||||
>
|
|
||||||
<span x-text="showPassword ? '👁️' : '👁️🗨️'"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div x-show="errors.password"
|
|
||||||
x-text="errors.password"
|
|
||||||
class="error-message show"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Remember Me & Forgot Password -->
|
|
||||||
<div class="form-options">
|
|
||||||
<div class="remember-me">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="rememberMe"
|
|
||||||
x-model="rememberMe"
|
|
||||||
>
|
|
||||||
<label for="rememberMe">Remember me</label>
|
|
||||||
</div>
|
|
||||||
<a href="/shop/account/forgot-password" class="forgot-password">
|
|
||||||
Forgot password?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Submit Button -->
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn-login"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<span x-show="loading" class="loading-spinner"></span>
|
|
||||||
<span x-text="loading ? 'Signing in...' : 'Sign In'"></span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Register Link -->
|
|
||||||
<div class="login-footer">
|
|
||||||
<div class="auth-footer-text">Don't have an account?</div>
|
|
||||||
<a href="/shop/account/register">Create an account</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Back to Shop -->
|
|
||||||
<div class="login-footer" style="border-top: none; padding-top: 0;">
|
|
||||||
<a href="/shop">← Continue shopping</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function customerLogin() {
|
|
||||||
return {
|
|
||||||
// Data
|
|
||||||
credentials: {
|
|
||||||
email: '',
|
|
||||||
password: ''
|
|
||||||
},
|
|
||||||
rememberMe: false,
|
|
||||||
showPassword: false,
|
|
||||||
loading: false,
|
|
||||||
errors: {},
|
|
||||||
alert: {
|
|
||||||
show: false,
|
|
||||||
type: 'error',
|
|
||||||
message: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get vendor data
|
|
||||||
get vendorId() {
|
|
||||||
return this.$el.dataset.vendorId;
|
|
||||||
},
|
|
||||||
|
|
||||||
get vendorName() {
|
|
||||||
return this.$el.dataset.vendorName;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Check if redirected after registration
|
|
||||||
checkRegistrationSuccess() {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
if (urlParams.get('registered') === 'true') {
|
|
||||||
this.showAlert(
|
|
||||||
'Account created successfully! Please sign in.',
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Clear errors
|
|
||||||
clearAllErrors() {
|
|
||||||
this.errors = {};
|
|
||||||
this.alert.show = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Show alert
|
|
||||||
showAlert(message, type = 'error') {
|
|
||||||
this.alert = {
|
|
||||||
show: true,
|
|
||||||
type: type,
|
|
||||||
message: message
|
|
||||||
};
|
|
||||||
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
},
|
|
||||||
|
|
||||||
// Handle login
|
|
||||||
async handleLogin() {
|
|
||||||
this.clearAllErrors();
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (!this.credentials.email) {
|
|
||||||
this.errors.email = 'Email is required';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.credentials.password) {
|
|
||||||
this.errors.password = 'Password is required';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/customers/login`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: this.credentials.email, // API expects username
|
|
||||||
password: this.credentials.password
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.detail || 'Login failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store token and user data
|
|
||||||
localStorage.setItem('customer_token', data.access_token);
|
|
||||||
localStorage.setItem('customer_user', JSON.stringify(data.user));
|
|
||||||
|
|
||||||
// Store vendor context
|
|
||||||
localStorage.setItem('customer_vendor_id', this.vendorId);
|
|
||||||
|
|
||||||
this.showAlert('Login successful! Redirecting...', 'success');
|
|
||||||
|
|
||||||
// Redirect to account page or cart
|
|
||||||
setTimeout(() => {
|
|
||||||
const returnUrl = new URLSearchParams(window.location.search).get('return') || '/shop/account';
|
|
||||||
window.location.href = returnUrl;
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login error:', error);
|
|
||||||
this.showAlert(error.message || 'Invalid email or password');
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Order history</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<-- Order history -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Customer profile</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<-- Customer profile -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Create Account - {{ vendor.name }}</title>
|
|
||||||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/shared/auth.css">
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="auth-page">
|
|
||||||
<div class="login-container"
|
|
||||||
x-data="customerRegistration()"
|
|
||||||
data-vendor-id="{{ vendor.id }}"
|
|
||||||
data-vendor-name="{{ vendor.name }}"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="login-header">
|
|
||||||
{% if vendor.logo_url %}
|
|
||||||
<img src="{{ vendor.logo_url }}" alt="{{ vendor.name }}" class="auth-logo">
|
|
||||||
{% else %}
|
|
||||||
<div class="auth-logo">🛒</div>
|
|
||||||
{% endif %}
|
|
||||||
<h1>Create Account</h1>
|
|
||||||
<p>Join {{ vendor.name }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Alert Box -->
|
|
||||||
<div x-show="alert.show"
|
|
||||||
x-transition
|
|
||||||
:class="'alert alert-' + alert.type"
|
|
||||||
x-text="alert.message"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Registration Form -->
|
|
||||||
<form @submit.prevent="handleRegister">
|
|
||||||
<!-- First Name -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="firstName">First Name <span class="required">*</span></label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="firstName"
|
|
||||||
x-model="formData.first_name"
|
|
||||||
required
|
|
||||||
placeholder="Enter your first name"
|
|
||||||
:class="{ 'error': errors.first_name }"
|
|
||||||
@input="clearError('first_name')"
|
|
||||||
>
|
|
||||||
<div x-show="errors.first_name"
|
|
||||||
x-text="errors.first_name"
|
|
||||||
class="error-message show"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Last Name -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="lastName">Last Name <span class="required">*</span></label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="lastName"
|
|
||||||
x-model="formData.last_name"
|
|
||||||
required
|
|
||||||
placeholder="Enter your last name"
|
|
||||||
:class="{ 'error': errors.last_name }"
|
|
||||||
@input="clearError('last_name')"
|
|
||||||
>
|
|
||||||
<div x-show="errors.last_name"
|
|
||||||
x-text="errors.last_name"
|
|
||||||
class="error-message show"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Email -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">Email Address <span class="required">*</span></label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
x-model="formData.email"
|
|
||||||
required
|
|
||||||
placeholder="your@email.com"
|
|
||||||
:class="{ 'error': errors.email }"
|
|
||||||
@input="clearError('email')"
|
|
||||||
>
|
|
||||||
<div x-show="errors.email"
|
|
||||||
x-text="errors.email"
|
|
||||||
class="error-message show"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Phone (Optional) -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="phone">Phone Number</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
id="phone"
|
|
||||||
x-model="formData.phone"
|
|
||||||
placeholder="+352 123 456 789"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Password -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Password <span class="required">*</span></label>
|
|
||||||
<div class="password-group">
|
|
||||||
<input
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
id="password"
|
|
||||||
x-model="formData.password"
|
|
||||||
required
|
|
||||||
placeholder="At least 8 characters"
|
|
||||||
:class="{ 'error': errors.password }"
|
|
||||||
@input="clearError('password')"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="password-toggle"
|
|
||||||
@click="showPassword = !showPassword"
|
|
||||||
>
|
|
||||||
<span x-text="showPassword ? '👁️' : '👁️🗨️'"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="form-help">
|
|
||||||
Must contain at least 8 characters, one letter, and one number
|
|
||||||
</div>
|
|
||||||
<div x-show="errors.password"
|
|
||||||
x-text="errors.password"
|
|
||||||
class="error-message show"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Confirm Password -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="confirmPassword">Confirm Password <span class="required">*</span></label>
|
|
||||||
<input
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
id="confirmPassword"
|
|
||||||
x-model="confirmPassword"
|
|
||||||
required
|
|
||||||
placeholder="Re-enter your password"
|
|
||||||
:class="{ 'error': errors.confirmPassword }"
|
|
||||||
@input="clearError('confirmPassword')"
|
|
||||||
>
|
|
||||||
<div x-show="errors.confirmPassword"
|
|
||||||
x-text="errors.confirmPassword"
|
|
||||||
class="error-message show"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Marketing Consent -->
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="remember-me">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="marketingConsent"
|
|
||||||
x-model="formData.marketing_consent"
|
|
||||||
>
|
|
||||||
<label for="marketingConsent" style="font-weight: normal;">
|
|
||||||
I'd like to receive news and special offers
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Submit Button -->
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn-login"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<span x-show="loading" class="loading-spinner"></span>
|
|
||||||
<span x-text="loading ? 'Creating Account...' : 'Create Account'"></span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Login Link -->
|
|
||||||
<div class="login-footer">
|
|
||||||
<div class="auth-footer-text">Already have an account?</div>
|
|
||||||
<a href="/shop/account/login">Sign in instead</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function customerRegistration() {
|
|
||||||
return {
|
|
||||||
// Data
|
|
||||||
formData: {
|
|
||||||
first_name: '',
|
|
||||||
last_name: '',
|
|
||||||
email: '',
|
|
||||||
phone: '',
|
|
||||||
password: '',
|
|
||||||
marketing_consent: false
|
|
||||||
},
|
|
||||||
confirmPassword: '',
|
|
||||||
showPassword: false,
|
|
||||||
loading: false,
|
|
||||||
errors: {},
|
|
||||||
alert: {
|
|
||||||
show: false,
|
|
||||||
type: 'error',
|
|
||||||
message: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get vendor data from element
|
|
||||||
get vendorId() {
|
|
||||||
return this.$el.dataset.vendorId;
|
|
||||||
},
|
|
||||||
|
|
||||||
get vendorName() {
|
|
||||||
return this.$el.dataset.vendorName;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Clear specific error
|
|
||||||
clearError(field) {
|
|
||||||
delete this.errors[field];
|
|
||||||
},
|
|
||||||
|
|
||||||
// Clear all errors
|
|
||||||
clearAllErrors() {
|
|
||||||
this.errors = {};
|
|
||||||
this.alert.show = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Show alert
|
|
||||||
showAlert(message, type = 'error') {
|
|
||||||
this.alert = {
|
|
||||||
show: true,
|
|
||||||
type: type,
|
|
||||||
message: message
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-hide success messages
|
|
||||||
if (type === 'success') {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.alert.show = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll to top
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
},
|
|
||||||
|
|
||||||
// Validate form
|
|
||||||
validateForm() {
|
|
||||||
this.clearAllErrors();
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
// First name
|
|
||||||
if (!this.formData.first_name.trim()) {
|
|
||||||
this.errors.first_name = 'First name is required';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last name
|
|
||||||
if (!this.formData.last_name.trim()) {
|
|
||||||
this.errors.last_name = 'Last name is required';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email
|
|
||||||
if (!this.formData.email.trim()) {
|
|
||||||
this.errors.email = 'Email is required';
|
|
||||||
isValid = false;
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData.email)) {
|
|
||||||
this.errors.email = 'Please enter a valid email address';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Password
|
|
||||||
if (!this.formData.password) {
|
|
||||||
this.errors.password = 'Password is required';
|
|
||||||
isValid = false;
|
|
||||||
} else if (this.formData.password.length < 8) {
|
|
||||||
this.errors.password = 'Password must be at least 8 characters';
|
|
||||||
isValid = false;
|
|
||||||
} else if (!/[a-zA-Z]/.test(this.formData.password)) {
|
|
||||||
this.errors.password = 'Password must contain at least one letter';
|
|
||||||
isValid = false;
|
|
||||||
} else if (!/[0-9]/.test(this.formData.password)) {
|
|
||||||
this.errors.password = 'Password must contain at least one number';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm password
|
|
||||||
if (this.formData.password !== this.confirmPassword) {
|
|
||||||
this.errors.confirmPassword = 'Passwords do not match';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Handle registration
|
|
||||||
async handleRegister() {
|
|
||||||
if (!this.validateForm()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/customers/register`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(this.formData)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.detail || 'Registration failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success!
|
|
||||||
this.showAlert(
|
|
||||||
'Account created successfully! Redirecting to login...',
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Redirect to login after 2 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = '/shop/account/login?registered=true';
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Registration error:', error);
|
|
||||||
this.showAlert(error.message || 'Registration failed. Please try again.');
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,489 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Shopping Cart - {{ vendor.name }}</title>
|
|
||||||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/vendor/vendor.css">
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div x-data="shoppingCart()"
|
|
||||||
x-init="loadCart()"
|
|
||||||
data-vendor-id="{{ vendor.id }}"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="header">
|
|
||||||
<div class="header-left">
|
|
||||||
<h1>🛒 Shopping Cart</h1>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<a href="/shop" class="btn-secondary">← Continue Shopping</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div x-show="loading && items.length === 0" class="loading">
|
|
||||||
<div class="loading-spinner-lg"></div>
|
|
||||||
<p>Loading your cart...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty Cart -->
|
|
||||||
<div x-show="!loading && items.length === 0" class="empty-state">
|
|
||||||
<div class="empty-state-icon">🛒</div>
|
|
||||||
<h3>Your cart is empty</h3>
|
|
||||||
<p>Add some products to get started!</p>
|
|
||||||
<a href="/shop/products" class="btn-primary">Browse Products</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cart Items -->
|
|
||||||
<div x-show="items.length > 0" class="cart-content">
|
|
||||||
<!-- Cart Items List -->
|
|
||||||
<div class="cart-items">
|
|
||||||
<template x-for="item in items" :key="item.product_id">
|
|
||||||
<div class="cart-item-card">
|
|
||||||
<div class="item-image">
|
|
||||||
<img :src="item.image_url || '/static/images/placeholder.png'"
|
|
||||||
:alt="item.name">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item-details">
|
|
||||||
<h3 class="item-name" x-text="item.name"></h3>
|
|
||||||
<p class="item-sku" x-text="'SKU: ' + item.sku"></p>
|
|
||||||
<p class="item-price">
|
|
||||||
€<span x-text="parseFloat(item.price).toFixed(2)"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item-quantity">
|
|
||||||
<label>Quantity:</label>
|
|
||||||
<div class="quantity-controls">
|
|
||||||
<button
|
|
||||||
@click="updateQuantity(item.product_id, item.quantity - 1)"
|
|
||||||
:disabled="item.quantity <= 1 || updating"
|
|
||||||
class="btn-quantity"
|
|
||||||
>
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
:value="item.quantity"
|
|
||||||
@change="updateQuantity(item.product_id, $event.target.value)"
|
|
||||||
min="1"
|
|
||||||
max="99"
|
|
||||||
:disabled="updating"
|
|
||||||
class="quantity-input"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
@click="updateQuantity(item.product_id, item.quantity + 1)"
|
|
||||||
:disabled="updating"
|
|
||||||
class="btn-quantity"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item-total">
|
|
||||||
<label>Subtotal:</label>
|
|
||||||
<p class="item-total-price">
|
|
||||||
€<span x-text="(parseFloat(item.price) * item.quantity).toFixed(2)"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item-actions">
|
|
||||||
<button
|
|
||||||
@click="removeItem(item.product_id)"
|
|
||||||
:disabled="updating"
|
|
||||||
class="btn-remove"
|
|
||||||
title="Remove from cart"
|
|
||||||
>
|
|
||||||
🗑️
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cart Summary -->
|
|
||||||
<div class="cart-summary">
|
|
||||||
<div class="summary-card">
|
|
||||||
<h3>Order Summary</h3>
|
|
||||||
|
|
||||||
<div class="summary-row">
|
|
||||||
<span>Subtotal (<span x-text="totalItems"></span> items):</span>
|
|
||||||
<span>€<span x-text="subtotal.toFixed(2)"></span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="summary-row">
|
|
||||||
<span>Shipping:</span>
|
|
||||||
<span x-text="shipping > 0 ? '€' + shipping.toFixed(2) : 'FREE'"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="summary-row summary-total">
|
|
||||||
<span>Total:</span>
|
|
||||||
<span class="total-amount">€<span x-text="total.toFixed(2)"></span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="proceedToCheckout()"
|
|
||||||
:disabled="updating || items.length === 0"
|
|
||||||
class="btn-primary btn-checkout"
|
|
||||||
>
|
|
||||||
Proceed to Checkout
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="/shop/products" class="btn-outline">
|
|
||||||
Continue Shopping
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function shoppingCart() {
|
|
||||||
return {
|
|
||||||
items: [],
|
|
||||||
loading: false,
|
|
||||||
updating: false,
|
|
||||||
vendorId: null,
|
|
||||||
sessionId: null,
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
get totalItems() {
|
|
||||||
return this.items.reduce((sum, item) => sum + item.quantity, 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
get subtotal() {
|
|
||||||
return this.items.reduce((sum, item) =>
|
|
||||||
sum + (parseFloat(item.price) * item.quantity), 0
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
get shipping() {
|
|
||||||
// Free shipping over €50
|
|
||||||
return this.subtotal >= 50 ? 0 : 5.99;
|
|
||||||
},
|
|
||||||
|
|
||||||
get total() {
|
|
||||||
return this.subtotal + this.shipping;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
init() {
|
|
||||||
this.vendorId = this.$el.dataset.vendorId;
|
|
||||||
this.sessionId = this.getOrCreateSessionId();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get or create session ID
|
|
||||||
getOrCreateSessionId() {
|
|
||||||
let sessionId = localStorage.getItem('cart_session_id');
|
|
||||||
if (!sessionId) {
|
|
||||||
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
||||||
localStorage.setItem('cart_session_id', sessionId);
|
|
||||||
}
|
|
||||||
return sessionId;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Load cart from API
|
|
||||||
async loadCart() {
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
this.items = data.items || [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load cart:', error);
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update item quantity
|
|
||||||
async updateQuantity(productId, newQuantity) {
|
|
||||||
newQuantity = parseInt(newQuantity);
|
|
||||||
|
|
||||||
if (newQuantity < 1 || newQuantity > 99) return;
|
|
||||||
|
|
||||||
this.updating = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}/items/${productId}`,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ quantity: newQuantity })
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await this.loadCart();
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to update quantity');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Update quantity error:', error);
|
|
||||||
alert('Failed to update quantity. Please try again.');
|
|
||||||
} finally {
|
|
||||||
this.updating = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Remove item from cart
|
|
||||||
async removeItem(productId) {
|
|
||||||
if (!confirm('Remove this item from your cart?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updating = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}/items/${productId}`,
|
|
||||||
{
|
|
||||||
method: 'DELETE'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await this.loadCart();
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to remove item');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Remove item error:', error);
|
|
||||||
alert('Failed to remove item. Please try again.');
|
|
||||||
} finally {
|
|
||||||
this.updating = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Proceed to checkout
|
|
||||||
proceedToCheckout() {
|
|
||||||
// Check if customer is logged in
|
|
||||||
const token = localStorage.getItem('customer_token');
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
// Redirect to login with return URL
|
|
||||||
window.location.href = '/shop/account/login?return=/shop/checkout';
|
|
||||||
} else {
|
|
||||||
window.location.href = '/shop/checkout';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Cart-specific styles */
|
|
||||||
.cart-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 350px;
|
|
||||||
gap: var(--spacing-lg);
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-items {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-item-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 100px 1fr auto auto auto;
|
|
||||||
gap: var(--spacing-lg);
|
|
||||||
align-items: center;
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-image img {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-name {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-sku {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-price {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-quantity {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-quantity:hover:not(:disabled) {
|
|
||||||
background: var(--gray-50);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-quantity:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-input {
|
|
||||||
width: 60px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-total-price {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-remove {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-remove:hover:not(:disabled) {
|
|
||||||
background: var(--danger-color);
|
|
||||||
border-color: var(--danger-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-summary {
|
|
||||||
position: sticky;
|
|
||||||
top: var(--spacing-lg);
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card h3 {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
padding-bottom: var(--spacing-md);
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-total {
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 700;
|
|
||||||
padding-top: var(--spacing-md);
|
|
||||||
border-top: 2px solid var(--border-color);
|
|
||||||
margin-top: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.total-amount {
|
|
||||||
color: var(--primary-color);
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-checkout {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.cart-content {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-summary {
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.cart-item-card {
|
|
||||||
grid-template-columns: 80px 1fr;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-image img {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-quantity,
|
|
||||||
.item-total {
|
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-actions {
|
|
||||||
grid-column: 2;
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Checkout process</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<-- Checkout process -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Shop homepage</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<-- Shop homepage -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
228
static/shop/js/shop-layout.js
Normal file
228
static/shop/js/shop-layout.js
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
// static/shop/js/shop-layout.js
|
||||||
|
/**
|
||||||
|
* Shop Layout Component
|
||||||
|
* Provides base functionality for vendor shop pages
|
||||||
|
* Works with vendor-specific themes
|
||||||
|
*/
|
||||||
|
|
||||||
|
const shopLog = {
|
||||||
|
info: (...args) => console.info('🛒 [SHOP]', ...args),
|
||||||
|
warn: (...args) => console.warn('⚠️ [SHOP]', ...args),
|
||||||
|
error: (...args) => console.error('❌ [SHOP]', ...args),
|
||||||
|
debug: (...args) => console.log('🔍 [SHOP]', ...args)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shop Layout Data
|
||||||
|
* Base Alpine.js component for shop pages
|
||||||
|
*/
|
||||||
|
function shopLayoutData() {
|
||||||
|
return {
|
||||||
|
// Theme state
|
||||||
|
dark: localStorage.getItem('shop-theme') === 'dark',
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
mobileMenuOpen: false,
|
||||||
|
searchOpen: false,
|
||||||
|
cartCount: 0,
|
||||||
|
|
||||||
|
// Cart state
|
||||||
|
cart: [],
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
init() {
|
||||||
|
shopLog.info('Shop layout initializing...');
|
||||||
|
|
||||||
|
// Load cart from localStorage
|
||||||
|
this.loadCart();
|
||||||
|
|
||||||
|
// Listen for cart updates
|
||||||
|
window.addEventListener('cart-updated', () => {
|
||||||
|
this.loadCart();
|
||||||
|
});
|
||||||
|
|
||||||
|
shopLog.info('Shop layout initialized');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Theme management
|
||||||
|
toggleTheme() {
|
||||||
|
this.dark = !this.dark;
|
||||||
|
localStorage.setItem('shop-theme', this.dark ? 'dark' : 'light');
|
||||||
|
shopLog.debug('Theme toggled:', this.dark ? 'dark' : 'light');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mobile menu
|
||||||
|
toggleMobileMenu() {
|
||||||
|
this.mobileMenuOpen = !this.mobileMenuOpen;
|
||||||
|
if (this.mobileMenuOpen) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMobileMenu() {
|
||||||
|
this.mobileMenuOpen = false;
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Search
|
||||||
|
openSearch() {
|
||||||
|
this.searchOpen = true;
|
||||||
|
shopLog.debug('Search opened');
|
||||||
|
// Focus search input after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = document.querySelector('#search-input');
|
||||||
|
if (input) input.focus();
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
closeSearch() {
|
||||||
|
this.searchOpen = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cart management
|
||||||
|
loadCart() {
|
||||||
|
try {
|
||||||
|
const cartData = localStorage.getItem('shop-cart');
|
||||||
|
if (cartData) {
|
||||||
|
this.cart = JSON.parse(cartData);
|
||||||
|
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
shopLog.error('Failed to load cart:', error);
|
||||||
|
this.cart = [];
|
||||||
|
this.cartCount = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addToCart(product, quantity = 1) {
|
||||||
|
shopLog.info('Adding to cart:', product.name, 'x', quantity);
|
||||||
|
|
||||||
|
// Find existing item
|
||||||
|
const existingIndex = this.cart.findIndex(item => item.id === product.id);
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// Update quantity
|
||||||
|
this.cart[existingIndex].quantity += quantity;
|
||||||
|
} else {
|
||||||
|
// Add new item
|
||||||
|
this.cart.push({
|
||||||
|
id: product.id,
|
||||||
|
name: product.name,
|
||||||
|
price: product.price,
|
||||||
|
image: product.image,
|
||||||
|
quantity: quantity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save and update
|
||||||
|
this.saveCart();
|
||||||
|
this.showToast(`${product.name} added to cart`, 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCartItem(productId, quantity) {
|
||||||
|
const index = this.cart.findIndex(item => item.id === productId);
|
||||||
|
if (index !== -1) {
|
||||||
|
if (quantity <= 0) {
|
||||||
|
this.cart.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.cart[index].quantity = quantity;
|
||||||
|
}
|
||||||
|
this.saveCart();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFromCart(productId) {
|
||||||
|
this.cart = this.cart.filter(item => item.id !== productId);
|
||||||
|
this.saveCart();
|
||||||
|
this.showToast('Item removed from cart', 'info');
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCart() {
|
||||||
|
this.cart = [];
|
||||||
|
this.saveCart();
|
||||||
|
this.showToast('Cart cleared', 'info');
|
||||||
|
},
|
||||||
|
|
||||||
|
saveCart() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('shop-cart', JSON.stringify(this.cart));
|
||||||
|
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||||
|
|
||||||
|
// Dispatch custom event
|
||||||
|
window.dispatchEvent(new CustomEvent('cart-updated'));
|
||||||
|
|
||||||
|
shopLog.debug('Cart saved:', this.cart.length, 'items');
|
||||||
|
} catch (error) {
|
||||||
|
shopLog.error('Failed to save cart:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get cart total
|
||||||
|
get cartTotal() {
|
||||||
|
return this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Toast notifications
|
||||||
|
showToast(message, type = 'info') {
|
||||||
|
const container = document.getElementById('toast-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast toast-${type} transform transition-all duration-300 mb-2`;
|
||||||
|
|
||||||
|
// Color based on type
|
||||||
|
const colors = {
|
||||||
|
success: 'bg-green-500',
|
||||||
|
error: 'bg-red-500',
|
||||||
|
warning: 'bg-yellow-500',
|
||||||
|
info: 'bg-blue-500'
|
||||||
|
};
|
||||||
|
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-3">
|
||||||
|
<span>${message}</span>
|
||||||
|
<button onclick="this.parentElement.parentElement.remove()"
|
||||||
|
class="ml-4 hover:opacity-75">
|
||||||
|
<svg class="w-4 h-4" 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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
// Auto-remove after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Format currency
|
||||||
|
formatPrice(price) {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD'
|
||||||
|
}).format(price);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
formatDate(dateString) {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make available globally
|
||||||
|
window.shopLayoutData = shopLayoutData;
|
||||||
|
|
||||||
|
shopLog.info('Shop layout module loaded');
|
||||||
@@ -1,771 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{ product.name if product else 'Product' }} - {{ vendor.name }}</title>
|
|
||||||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/vendor/vendor.css">
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div x-data="productDetail()"
|
|
||||||
x-init="loadProduct()"
|
|
||||||
data-vendor-id="{{ vendor.id }}"
|
|
||||||
data-product-id="{{ product_id }}"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="header">
|
|
||||||
<div class="header-left">
|
|
||||||
<a href="/shop/products" class="btn-back">← Back to Products</a>
|
|
||||||
<h1>{{ vendor.name }}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<a href="/shop/cart" class="btn-primary">
|
|
||||||
🛒 Cart (<span x-text="cartCount"></span>)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div x-show="loading" class="container">
|
|
||||||
<div class="loading">
|
|
||||||
<div class="loading-spinner-lg"></div>
|
|
||||||
<p>Loading product...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Product Detail -->
|
|
||||||
<div x-show="!loading && product" class="container">
|
|
||||||
<div class="product-detail-container">
|
|
||||||
<!-- Product Images -->
|
|
||||||
<div class="product-images">
|
|
||||||
<div class="main-image">
|
|
||||||
<img
|
|
||||||
:src="selectedImage || '/static/images/placeholder.png'"
|
|
||||||
:alt="product?.marketplace_product?.title"
|
|
||||||
class="product-main-image"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Thumbnail Gallery -->
|
|
||||||
<div class="image-gallery" x-show="product?.marketplace_product?.images?.length > 1">
|
|
||||||
<template x-for="(image, index) in product?.marketplace_product?.images" :key="index">
|
|
||||||
<img
|
|
||||||
:src="image"
|
|
||||||
:alt="`Product image ${index + 1}`"
|
|
||||||
class="thumbnail"
|
|
||||||
:class="{ 'active': selectedImage === image }"
|
|
||||||
@click="selectedImage = image"
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Product Info -->
|
|
||||||
<div class="product-info-detail">
|
|
||||||
<h1 x-text="product?.marketplace_product?.title" class="product-title-detail"></h1>
|
|
||||||
|
|
||||||
<!-- Brand & Category -->
|
|
||||||
<div class="product-meta">
|
|
||||||
<span x-show="product?.marketplace_product?.brand" class="meta-item">
|
|
||||||
<strong>Brand:</strong> <span x-text="product?.marketplace_product?.brand"></span>
|
|
||||||
</span>
|
|
||||||
<span x-show="product?.marketplace_product?.google_product_category" class="meta-item">
|
|
||||||
<strong>Category:</strong> <span x-text="product?.marketplace_product?.google_product_category"></span>
|
|
||||||
</span>
|
|
||||||
<span class="meta-item">
|
|
||||||
<strong>SKU:</strong> <span x-text="product?.product_id || product?.marketplace_product?.mpn"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Price -->
|
|
||||||
<div class="product-pricing">
|
|
||||||
<div x-show="product?.sale_price && product?.sale_price < product?.price">
|
|
||||||
<span class="price-original">€<span x-text="parseFloat(product?.price).toFixed(2)"></span></span>
|
|
||||||
<span class="price-sale">€<span x-text="parseFloat(product?.sale_price).toFixed(2)"></span></span>
|
|
||||||
<span class="price-badge">SALE</span>
|
|
||||||
</div>
|
|
||||||
<div x-show="!product?.sale_price || product?.sale_price >= product?.price">
|
|
||||||
<span class="price-current">€<span x-text="parseFloat(product?.price || 0).toFixed(2)"></span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Availability -->
|
|
||||||
<div class="product-availability">
|
|
||||||
<span
|
|
||||||
x-show="product?.available_inventory > 0"
|
|
||||||
class="availability-badge in-stock"
|
|
||||||
>
|
|
||||||
✓ In Stock (<span x-text="product?.available_inventory"></span> available)
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
x-show="!product?.available_inventory || product?.available_inventory <= 0"
|
|
||||||
class="availability-badge out-of-stock"
|
|
||||||
>
|
|
||||||
✗ Out of Stock
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<div class="product-description">
|
|
||||||
<h3>Description</h3>
|
|
||||||
<p x-text="product?.marketplace_product?.description || 'No description available'"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional Details -->
|
|
||||||
<div class="product-details" x-show="hasAdditionalDetails">
|
|
||||||
<h3>Product Details</h3>
|
|
||||||
<ul>
|
|
||||||
<li x-show="product?.marketplace_product?.gtin">
|
|
||||||
<strong>GTIN:</strong> <span x-text="product?.marketplace_product?.gtin"></span>
|
|
||||||
</li>
|
|
||||||
<li x-show="product?.condition">
|
|
||||||
<strong>Condition:</strong> <span x-text="product?.condition"></span>
|
|
||||||
</li>
|
|
||||||
<li x-show="product?.marketplace_product?.color">
|
|
||||||
<strong>Color:</strong> <span x-text="product?.marketplace_product?.color"></span>
|
|
||||||
</li>
|
|
||||||
<li x-show="product?.marketplace_product?.size">
|
|
||||||
<strong>Size:</strong> <span x-text="product?.marketplace_product?.size"></span>
|
|
||||||
</li>
|
|
||||||
<li x-show="product?.marketplace_product?.material">
|
|
||||||
<strong>Material:</strong> <span x-text="product?.marketplace_product?.material"></span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add to Cart Section -->
|
|
||||||
<div class="add-to-cart-section">
|
|
||||||
<!-- Quantity Selector -->
|
|
||||||
<div class="quantity-selector">
|
|
||||||
<label>Quantity:</label>
|
|
||||||
<div class="quantity-controls">
|
|
||||||
<button
|
|
||||||
@click="decreaseQuantity()"
|
|
||||||
:disabled="quantity <= (product?.min_quantity || 1)"
|
|
||||||
class="btn-quantity"
|
|
||||||
>
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
x-model.number="quantity"
|
|
||||||
:min="product?.min_quantity || 1"
|
|
||||||
:max="product?.max_quantity || product?.available_inventory"
|
|
||||||
class="quantity-input"
|
|
||||||
@change="validateQuantity()"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
@click="increaseQuantity()"
|
|
||||||
:disabled="quantity >= (product?.max_quantity || product?.available_inventory)"
|
|
||||||
class="btn-quantity"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add to Cart Button -->
|
|
||||||
<button
|
|
||||||
@click="addToCart()"
|
|
||||||
:disabled="!canAddToCart || addingToCart"
|
|
||||||
class="btn-add-to-cart"
|
|
||||||
>
|
|
||||||
<span x-show="!addingToCart">
|
|
||||||
🛒 Add to Cart
|
|
||||||
</span>
|
|
||||||
<span x-show="addingToCart">
|
|
||||||
<span class="loading-spinner"></span> Adding...
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Total Price -->
|
|
||||||
<div class="total-price">
|
|
||||||
<strong>Total:</strong> €<span x-text="totalPrice.toFixed(2)"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Related Products / You May Also Like -->
|
|
||||||
<div class="related-products" x-show="relatedProducts.length > 0">
|
|
||||||
<h2>You May Also Like</h2>
|
|
||||||
<div class="product-grid">
|
|
||||||
<template x-for="related in relatedProducts" :key="related.id">
|
|
||||||
<div class="product-card">
|
|
||||||
<img
|
|
||||||
:src="related.image_url || '/static/images/placeholder.png'"
|
|
||||||
:alt="related.name"
|
|
||||||
class="product-image"
|
|
||||||
@click="viewProduct(related.id)"
|
|
||||||
>
|
|
||||||
<div class="product-info">
|
|
||||||
<h3
|
|
||||||
class="product-title"
|
|
||||||
@click="viewProduct(related.id)"
|
|
||||||
x-text="related.name"
|
|
||||||
></h3>
|
|
||||||
<p class="product-price">
|
|
||||||
€<span x-text="parseFloat(related.price).toFixed(2)"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toast Notification -->
|
|
||||||
<div
|
|
||||||
x-show="toast.show"
|
|
||||||
x-transition
|
|
||||||
:class="'toast toast-' + toast.type"
|
|
||||||
x-text="toast.message"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function productDetail() {
|
|
||||||
return {
|
|
||||||
// Data
|
|
||||||
product: null,
|
|
||||||
relatedProducts: [],
|
|
||||||
loading: false,
|
|
||||||
addingToCart: false,
|
|
||||||
quantity: 1,
|
|
||||||
selectedImage: null,
|
|
||||||
cartCount: 0,
|
|
||||||
vendorId: null,
|
|
||||||
productId: null,
|
|
||||||
sessionId: null,
|
|
||||||
|
|
||||||
// Toast notification
|
|
||||||
toast: {
|
|
||||||
show: false,
|
|
||||||
type: 'success',
|
|
||||||
message: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
get canAddToCart() {
|
|
||||||
return this.product?.is_active &&
|
|
||||||
this.product?.available_inventory > 0 &&
|
|
||||||
this.quantity > 0 &&
|
|
||||||
this.quantity <= this.product?.available_inventory;
|
|
||||||
},
|
|
||||||
|
|
||||||
get totalPrice() {
|
|
||||||
const price = this.product?.sale_price || this.product?.price || 0;
|
|
||||||
return price * this.quantity;
|
|
||||||
},
|
|
||||||
|
|
||||||
get hasAdditionalDetails() {
|
|
||||||
return this.product?.marketplace_product?.gtin ||
|
|
||||||
this.product?.condition ||
|
|
||||||
this.product?.marketplace_product?.color ||
|
|
||||||
this.product?.marketplace_product?.size ||
|
|
||||||
this.product?.marketplace_product?.material;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
init() {
|
|
||||||
this.vendorId = this.$el.dataset.vendorId;
|
|
||||||
this.productId = this.$el.dataset.productId;
|
|
||||||
this.sessionId = this.getOrCreateSessionId();
|
|
||||||
this.loadCartCount();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get or create session ID
|
|
||||||
getOrCreateSessionId() {
|
|
||||||
let sessionId = localStorage.getItem('cart_session_id');
|
|
||||||
if (!sessionId) {
|
|
||||||
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
||||||
localStorage.setItem('cart_session_id', sessionId);
|
|
||||||
}
|
|
||||||
return sessionId;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Load product details
|
|
||||||
async loadProduct() {
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/products/${this.productId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Product not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.product = await response.json();
|
|
||||||
|
|
||||||
// Set default image
|
|
||||||
if (this.product?.marketplace_product?.image_link) {
|
|
||||||
this.selectedImage = this.product.marketplace_product.image_link;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial quantity
|
|
||||||
this.quantity = this.product?.min_quantity || 1;
|
|
||||||
|
|
||||||
// Load related products (optional)
|
|
||||||
await this.loadRelatedProducts();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load product:', error);
|
|
||||||
this.showToast('Failed to load product', 'error');
|
|
||||||
// Redirect back to products after error
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = '/shop/products';
|
|
||||||
}, 2000);
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Load related products (same category or brand)
|
|
||||||
async loadRelatedProducts() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/products?limit=4`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
// Filter out current product
|
|
||||||
this.relatedProducts = data.products
|
|
||||||
.filter(p => p.id !== parseInt(this.productId))
|
|
||||||
.slice(0, 4);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load related products:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Load cart count
|
|
||||||
async loadCartCount() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
this.cartCount = (data.items || []).reduce((sum, item) =>
|
|
||||||
sum + item.quantity, 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load cart count:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Quantity controls
|
|
||||||
increaseQuantity() {
|
|
||||||
const max = this.product?.max_quantity || this.product?.available_inventory;
|
|
||||||
if (this.quantity < max) {
|
|
||||||
this.quantity++;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
decreaseQuantity() {
|
|
||||||
const min = this.product?.min_quantity || 1;
|
|
||||||
if (this.quantity > min) {
|
|
||||||
this.quantity--;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
validateQuantity() {
|
|
||||||
const min = this.product?.min_quantity || 1;
|
|
||||||
const max = this.product?.max_quantity || this.product?.available_inventory;
|
|
||||||
|
|
||||||
if (this.quantity < min) {
|
|
||||||
this.quantity = min;
|
|
||||||
} else if (this.quantity > max) {
|
|
||||||
this.quantity = max;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Add to cart
|
|
||||||
async addToCart() {
|
|
||||||
if (!this.canAddToCart) return;
|
|
||||||
|
|
||||||
this.addingToCart = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}/items`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
product_id: parseInt(this.productId),
|
|
||||||
quantity: this.quantity
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
this.cartCount += this.quantity;
|
|
||||||
this.showToast(
|
|
||||||
`${this.quantity} item(s) added to cart!`,
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset quantity to minimum
|
|
||||||
this.quantity = this.product?.min_quantity || 1;
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
throw new Error(error.detail || 'Failed to add to cart');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Add to cart error:', error);
|
|
||||||
this.showToast(error.message || 'Failed to add to cart', 'error');
|
|
||||||
} finally {
|
|
||||||
this.addingToCart = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// View other product
|
|
||||||
viewProduct(productId) {
|
|
||||||
window.location.href = `/shop/products/${productId}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Show toast notification
|
|
||||||
showToast(message, type = 'success') {
|
|
||||||
this.toast = {
|
|
||||||
show: true,
|
|
||||||
type: type,
|
|
||||||
message: message
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.toast.show = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Product Detail Styles */
|
|
||||||
.product-detail-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: var(--spacing-2xl);
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
margin-bottom: var(--spacing-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-images {
|
|
||||||
position: sticky;
|
|
||||||
top: var(--spacing-lg);
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-image {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--gray-50);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-main-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-gallery {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail:hover {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail.active {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-info-detail {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-title-detail {
|
|
||||||
font-size: var(--font-4xl);
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
padding-bottom: var(--spacing-md);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-item {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-pricing {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
background: var(--gray-50);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-original {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-decoration: line-through;
|
|
||||||
margin-right: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-sale {
|
|
||||||
font-size: var(--font-4xl);
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-current {
|
|
||||||
font-size: var(--font-4xl);
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-badge {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--danger-color);
|
|
||||||
color: white;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-left: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-availability {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.availability-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.availability-badge.in-stock {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
.availability-badge.out-of-stock {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description h3 {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description p {
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-details {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-details h3 {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-details ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-details li {
|
|
||||||
padding: var(--spacing-sm) 0;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-details li:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-to-cart-section {
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 2px solid var(--primary-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-selector {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-selector label {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-quantity {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-quantity:hover:not(:disabled) {
|
|
||||||
background: var(--gray-50);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-quantity:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-input {
|
|
||||||
width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 10px;
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-add-to-cart {
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px 32px;
|
|
||||||
background: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-add-to-cart:hover:not(:disabled) {
|
|
||||||
background: var(--primary-dark);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-add-to-cart:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.total-price {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
background: var(--gray-50);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
text-align: center;
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-products {
|
|
||||||
margin-top: var(--spacing-2xl);
|
|
||||||
padding-top: var(--spacing-2xl);
|
|
||||||
border-top: 2px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-products h2 {
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
font-size: var(--font-3xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.product-detail-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-images {
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.product-title-detail {
|
|
||||||
font-size: var(--font-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-current,
|
|
||||||
.price-sale {
|
|
||||||
font-size: var(--font-3xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-to-cart-section {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Products - {{ vendor.name }}</title>
|
|
||||||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/vendor/vendor.css">
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div x-data="productCatalog()"
|
|
||||||
x-init="loadProducts()"
|
|
||||||
data-vendor-id="{{ vendor.id }}"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="header">
|
|
||||||
<div class="header-left">
|
|
||||||
<h1>{{ vendor.name }} - Products</h1>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<a href="/shop/cart" class="btn-primary">
|
|
||||||
🛒 Cart (<span x-text="cartCount"></span>)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- Filters & Search -->
|
|
||||||
<div class="filter-bar">
|
|
||||||
<div class="search-box">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
x-model="filters.search"
|
|
||||||
@input.debounce.500ms="loadProducts()"
|
|
||||||
placeholder="Search products..."
|
|
||||||
class="search-input"
|
|
||||||
>
|
|
||||||
<span class="search-icon">🔍</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
|
||||||
<select
|
|
||||||
x-model="filters.sort"
|
|
||||||
@change="loadProducts()"
|
|
||||||
class="form-select"
|
|
||||||
>
|
|
||||||
<option value="name_asc">Name (A-Z)</option>
|
|
||||||
<option value="name_desc">Name (Z-A)</option>
|
|
||||||
<option value="price_asc">Price (Low to High)</option>
|
|
||||||
<option value="price_desc">Price (High to Low)</option>
|
|
||||||
<option value="newest">Newest First</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="clearFilters()"
|
|
||||||
class="btn-secondary"
|
|
||||||
x-show="hasActiveFilters"
|
|
||||||
>
|
|
||||||
Clear Filters
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div x-show="loading" class="loading">
|
|
||||||
<div class="loading-spinner-lg"></div>
|
|
||||||
<p>Loading products...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div x-show="!loading && products.length === 0" class="empty-state">
|
|
||||||
<div class="empty-state-icon">📦</div>
|
|
||||||
<h3>No products found</h3>
|
|
||||||
<p x-show="hasActiveFilters">Try adjusting your filters</p>
|
|
||||||
<p x-show="!hasActiveFilters">Check back soon for new products!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Product Grid -->
|
|
||||||
<div x-show="!loading && products.length > 0" class="product-grid">
|
|
||||||
<template x-for="product in products" :key="product.id">
|
|
||||||
<div class="product-card">
|
|
||||||
<!-- Product Image -->
|
|
||||||
<div class="product-image-wrapper">
|
|
||||||
<img
|
|
||||||
:src="product.image_url || '/static/images/placeholder.png'"
|
|
||||||
:alt="product.name"
|
|
||||||
class="product-image"
|
|
||||||
@click="viewProduct(product.id)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
x-show="!product.is_active || product.inventory_level <= 0"
|
|
||||||
class="out-of-stock-badge"
|
|
||||||
>
|
|
||||||
Out of Stock
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Product Info -->
|
|
||||||
<div class="product-info">
|
|
||||||
<h3
|
|
||||||
class="product-title"
|
|
||||||
@click="viewProduct(product.id)"
|
|
||||||
x-text="product.name"
|
|
||||||
></h3>
|
|
||||||
|
|
||||||
<p class="product-sku" x-text="'SKU: ' + product.sku"></p>
|
|
||||||
|
|
||||||
<p class="product-price">
|
|
||||||
€<span x-text="parseFloat(product.price).toFixed(2)"></span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p
|
|
||||||
class="product-description"
|
|
||||||
x-text="product.description || 'No description available'"
|
|
||||||
></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Product Actions -->
|
|
||||||
<div class="product-actions">
|
|
||||||
<button
|
|
||||||
@click="viewProduct(product.id)"
|
|
||||||
class="btn-outline btn-sm"
|
|
||||||
>
|
|
||||||
View Details
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="addToCart(product)"
|
|
||||||
:disabled="!product.is_active || product.inventory_level <= 0 || addingToCart[product.id]"
|
|
||||||
class="btn-primary btn-sm"
|
|
||||||
>
|
|
||||||
<span x-show="!addingToCart[product.id]">Add to Cart</span>
|
|
||||||
<span x-show="addingToCart[product.id]">
|
|
||||||
<span class="loading-spinner"></span> Adding...
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
<div x-show="totalPages > 1" class="pagination">
|
|
||||||
<button
|
|
||||||
@click="changePage(currentPage - 1)"
|
|
||||||
:disabled="currentPage === 1"
|
|
||||||
class="pagination-btn"
|
|
||||||
>
|
|
||||||
← Previous
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span class="pagination-info">
|
|
||||||
Page <span x-text="currentPage"></span> of <span x-text="totalPages"></span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="changePage(currentPage + 1)"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
class="pagination-btn"
|
|
||||||
>
|
|
||||||
Next →
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toast Notification -->
|
|
||||||
<div
|
|
||||||
x-show="toast.show"
|
|
||||||
x-transition
|
|
||||||
:class="'toast toast-' + toast.type"
|
|
||||||
class="toast"
|
|
||||||
x-text="toast.message"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function productCatalog() {
|
|
||||||
return {
|
|
||||||
products: [],
|
|
||||||
loading: false,
|
|
||||||
addingToCart: {},
|
|
||||||
cartCount: 0,
|
|
||||||
vendorId: null,
|
|
||||||
sessionId: null,
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
filters: {
|
|
||||||
search: '',
|
|
||||||
sort: 'name_asc',
|
|
||||||
category: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
currentPage: 1,
|
|
||||||
perPage: 12,
|
|
||||||
totalProducts: 0,
|
|
||||||
|
|
||||||
// Toast notification
|
|
||||||
toast: {
|
|
||||||
show: false,
|
|
||||||
type: 'success',
|
|
||||||
message: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
get totalPages() {
|
|
||||||
return Math.ceil(this.totalProducts / this.perPage);
|
|
||||||
},
|
|
||||||
|
|
||||||
get hasActiveFilters() {
|
|
||||||
return this.filters.search !== '' ||
|
|
||||||
this.filters.category !== '' ||
|
|
||||||
this.filters.sort !== 'name_asc';
|
|
||||||
},
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
init() {
|
|
||||||
this.vendorId = this.$el.dataset.vendorId;
|
|
||||||
this.sessionId = this.getOrCreateSessionId();
|
|
||||||
this.loadCartCount();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get or create session ID
|
|
||||||
getOrCreateSessionId() {
|
|
||||||
let sessionId = localStorage.getItem('cart_session_id');
|
|
||||||
if (!sessionId) {
|
|
||||||
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
||||||
localStorage.setItem('cart_session_id', sessionId);
|
|
||||||
}
|
|
||||||
return sessionId;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Load products from API
|
|
||||||
async loadProducts() {
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: this.currentPage,
|
|
||||||
per_page: this.perPage,
|
|
||||||
search: this.filters.search,
|
|
||||||
sort: this.filters.sort
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.filters.category) {
|
|
||||||
params.append('category', this.filters.category);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/products?${params}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
this.products = data.products || [];
|
|
||||||
this.totalProducts = data.total || 0;
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to load products');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Load products error:', error);
|
|
||||||
this.showToast('Failed to load products', 'error');
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Load cart count
|
|
||||||
async loadCartCount() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
this.cartCount = (data.items || []).reduce((sum, item) =>
|
|
||||||
sum + item.quantity, 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load cart count:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Add product to cart
|
|
||||||
async addToCart(product) {
|
|
||||||
this.addingToCart[product.id] = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}/items`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
product_id: product.id,
|
|
||||||
quantity: 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
this.cartCount++;
|
|
||||||
this.showToast(`${product.name} added to cart!`, 'success');
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to add to cart');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Add to cart error:', error);
|
|
||||||
this.showToast('Failed to add to cart. Please try again.', 'error');
|
|
||||||
} finally {
|
|
||||||
this.addingToCart[product.id] = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// View product details
|
|
||||||
viewProduct(productId) {
|
|
||||||
window.location.href = `/shop/products/${productId}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Change page
|
|
||||||
changePage(page) {
|
|
||||||
if (page >= 1 && page <= this.totalPages) {
|
|
||||||
this.currentPage = page;
|
|
||||||
this.loadProducts();
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Clear filters
|
|
||||||
clearFilters() {
|
|
||||||
this.filters = {
|
|
||||||
search: '',
|
|
||||||
sort: 'name_asc',
|
|
||||||
category: ''
|
|
||||||
};
|
|
||||||
this.currentPage = 1;
|
|
||||||
this.loadProducts();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Show toast notification
|
|
||||||
showToast(message, type = 'success') {
|
|
||||||
this.toast = {
|
|
||||||
show: true,
|
|
||||||
type: type,
|
|
||||||
message: message
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.toast.show = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Product-specific styles */
|
|
||||||
.filter-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
flex: 2;
|
|
||||||
min-width: 300px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
padding-left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-image-wrapper {
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.out-of-stock-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
background: var(--danger-color);
|
|
||||||
color: white;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-title {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-title:hover {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-sku {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toast notification */
|
|
||||||
.toast {
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
padding: 16px 24px;
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow-xl);
|
|
||||||
z-index: 10000;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-success {
|
|
||||||
border-left: 4px solid var(--success-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-error {
|
|
||||||
border-left: 4px solid var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.filter-bar {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box,
|
|
||||||
.filter-group {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Search results page</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<-- Search results page -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user