feat: add platform homepage and content management system with improved UI
Implemented a comprehensive CMS for managing platform homepage and content pages: - Platform homepage manager with template selection (default, minimal, modern) - Content pages CRUD with platform defaults and vendor overrides - Sidebar navigation for Content Management section - Dedicated API endpoints for creating, updating, deleting pages - Template support for customizable homepage layouts - Header/footer navigation integration for content pages - Comprehensive documentation for platform homepage setup - Migration script for creating initial platform pages UI improvements: - Fixed action buttons styling in content pages table to match design system - Added proper hover states, rounded corners, and better contrast - Increased button size and padding for better usability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
171
static/admin/js/content-page-edit.js
Normal file
171
static/admin/js/content-page-edit.js
Normal file
@@ -0,0 +1,171 @@
|
||||
// static/admin/js/content-page-edit.js
|
||||
|
||||
// Use centralized logger
|
||||
const contentPageEditLog = window.LogConfig.loggers.contentPageEdit || window.LogConfig.createLogger('contentPageEdit');
|
||||
|
||||
// ============================================
|
||||
// CONTENT PAGE EDITOR FUNCTION
|
||||
// ============================================
|
||||
function contentPageEditor(pageId) {
|
||||
return {
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Page identifier for sidebar active state
|
||||
currentPage: 'content-pages',
|
||||
|
||||
// Editor state
|
||||
pageId: pageId,
|
||||
form: {
|
||||
slug: '',
|
||||
title: '',
|
||||
content: '',
|
||||
content_format: 'html',
|
||||
template: 'default',
|
||||
meta_description: '',
|
||||
meta_keywords: '',
|
||||
is_published: false,
|
||||
show_in_header: false,
|
||||
show_in_footer: true,
|
||||
display_order: 0,
|
||||
vendor_id: null
|
||||
},
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
successMessage: null,
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
contentPageEditLog.info('=== CONTENT PAGE EDITOR INITIALIZING ===');
|
||||
contentPageEditLog.info('Page ID:', this.pageId);
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._contentPageEditInitialized) {
|
||||
contentPageEditLog.warn('Content page editor already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._contentPageEditInitialized = true;
|
||||
|
||||
if (this.pageId) {
|
||||
// Edit mode - load existing page
|
||||
contentPageEditLog.group('Loading page for editing');
|
||||
await this.loadPage();
|
||||
contentPageEditLog.groupEnd();
|
||||
} else {
|
||||
// Create mode - use default values
|
||||
contentPageEditLog.info('Create mode - using default form values');
|
||||
}
|
||||
|
||||
contentPageEditLog.info('=== CONTENT PAGE EDITOR INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Load existing page
|
||||
async loadPage() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
contentPageEditLog.info(`Fetching page ${this.pageId}...`);
|
||||
|
||||
const response = await apiClient.get(`/admin/content-pages/${this.pageId}`);
|
||||
|
||||
contentPageEditLog.debug('API Response:', response);
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Invalid API response');
|
||||
}
|
||||
|
||||
// Handle response - API returns object directly
|
||||
const page = response.data || response;
|
||||
this.form = {
|
||||
slug: page.slug || '',
|
||||
title: page.title || '',
|
||||
content: page.content || '',
|
||||
content_format: page.content_format || 'html',
|
||||
template: page.template || 'default',
|
||||
meta_description: page.meta_description || '',
|
||||
meta_keywords: page.meta_keywords || '',
|
||||
is_published: page.is_published || false,
|
||||
show_in_header: page.show_in_header || false,
|
||||
show_in_footer: page.show_in_footer !== undefined ? page.show_in_footer : true,
|
||||
display_order: page.display_order || 0,
|
||||
vendor_id: page.vendor_id
|
||||
};
|
||||
|
||||
contentPageEditLog.info('Page loaded successfully');
|
||||
|
||||
} catch (err) {
|
||||
contentPageEditLog.error('Error loading page:', err);
|
||||
this.error = err.message || 'Failed to load page';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Save page (create or update)
|
||||
async savePage() {
|
||||
if (this.saving) return;
|
||||
|
||||
this.saving = true;
|
||||
this.error = null;
|
||||
this.successMessage = null;
|
||||
|
||||
try {
|
||||
contentPageEditLog.info(this.pageId ? 'Updating page...' : 'Creating page...');
|
||||
|
||||
const payload = {
|
||||
slug: this.form.slug,
|
||||
title: this.form.title,
|
||||
content: this.form.content,
|
||||
content_format: this.form.content_format,
|
||||
template: this.form.template,
|
||||
meta_description: this.form.meta_description,
|
||||
meta_keywords: this.form.meta_keywords,
|
||||
is_published: this.form.is_published,
|
||||
show_in_header: this.form.show_in_header,
|
||||
show_in_footer: this.form.show_in_footer,
|
||||
display_order: this.form.display_order,
|
||||
vendor_id: this.form.vendor_id
|
||||
};
|
||||
|
||||
contentPageEditLog.debug('Payload:', payload);
|
||||
|
||||
let response;
|
||||
if (this.pageId) {
|
||||
// Update existing page
|
||||
response = await apiClient.put(`/admin/content-pages/${this.pageId}`, payload);
|
||||
this.successMessage = 'Page updated successfully!';
|
||||
contentPageEditLog.info('Page updated');
|
||||
} else {
|
||||
// Create new page
|
||||
response = await apiClient.post('/admin/content-pages/platform', payload);
|
||||
this.successMessage = 'Page created successfully!';
|
||||
contentPageEditLog.info('Page created');
|
||||
|
||||
// Redirect to edit page after creation
|
||||
const pageData = response.data || response;
|
||||
if (pageData && pageData.id) {
|
||||
setTimeout(() => {
|
||||
window.location.href = `/admin/content-pages/${pageData.id}/edit`;
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear success message after 3 seconds
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
|
||||
} catch (err) {
|
||||
contentPageEditLog.error('Error saving page:', err);
|
||||
this.error = err.message || 'Failed to save page';
|
||||
|
||||
// Scroll to top to show error
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
161
static/admin/js/content-pages.js
Normal file
161
static/admin/js/content-pages.js
Normal file
@@ -0,0 +1,161 @@
|
||||
// static/admin/js/content-pages.js
|
||||
|
||||
// Use centralized logger
|
||||
const contentPagesLog = window.LogConfig.loggers.contentPages || window.LogConfig.createLogger('contentPages');
|
||||
|
||||
// ============================================
|
||||
// CONTENT PAGES MANAGER FUNCTION
|
||||
// ============================================
|
||||
function contentPagesManager() {
|
||||
return {
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Page identifier for sidebar active state
|
||||
currentPage: 'content-pages',
|
||||
|
||||
// Content pages specific state
|
||||
allPages: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// Tabs and filters
|
||||
activeTab: 'all', // all, platform, vendor
|
||||
searchQuery: '',
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
contentPagesLog.info('=== CONTENT PAGES MANAGER INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._contentPagesInitialized) {
|
||||
contentPagesLog.warn('Content pages manager already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._contentPagesInitialized = true;
|
||||
|
||||
contentPagesLog.group('Loading content pages');
|
||||
await this.loadPages();
|
||||
contentPagesLog.groupEnd();
|
||||
|
||||
contentPagesLog.info('=== CONTENT PAGES MANAGER INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Computed: Platform pages
|
||||
get platformPages() {
|
||||
return this.allPages.filter(page => page.is_platform_default);
|
||||
},
|
||||
|
||||
// Computed: Vendor pages
|
||||
get vendorPages() {
|
||||
return this.allPages.filter(page => page.is_vendor_override);
|
||||
},
|
||||
|
||||
// Computed: Filtered pages based on active tab and search
|
||||
get filteredPages() {
|
||||
let pages = [];
|
||||
|
||||
// Filter by tab
|
||||
if (this.activeTab === 'platform') {
|
||||
pages = this.platformPages;
|
||||
} else if (this.activeTab === 'vendor') {
|
||||
pages = this.vendorPages;
|
||||
} else {
|
||||
pages = this.allPages;
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (this.searchQuery) {
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
pages = pages.filter(page =>
|
||||
page.title.toLowerCase().includes(query) ||
|
||||
page.slug.toLowerCase().includes(query) ||
|
||||
(page.vendor_name && page.vendor_name.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
// Sort by display_order, then title
|
||||
return pages.sort((a, b) => {
|
||||
if (a.display_order !== b.display_order) {
|
||||
return a.display_order - b.display_order;
|
||||
}
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
},
|
||||
|
||||
// Load all content pages
|
||||
async loadPages() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
contentPagesLog.info('Fetching all content pages...');
|
||||
|
||||
// Fetch all pages (platform + vendor, published + unpublished)
|
||||
const response = await apiClient.get('/admin/content-pages/?include_unpublished=true');
|
||||
|
||||
contentPagesLog.debug('API Response:', response);
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Invalid API response');
|
||||
}
|
||||
|
||||
// Handle response - API returns array directly
|
||||
this.allPages = Array.isArray(response) ? response : (response.data || response.items || []);
|
||||
contentPagesLog.info(`Loaded ${this.allPages.length} pages`);
|
||||
|
||||
} catch (err) {
|
||||
contentPagesLog.error('Error loading content pages:', err);
|
||||
this.error = err.message || 'Failed to load content pages';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Delete a page
|
||||
async deletePage(page) {
|
||||
if (!confirm(`Are you sure you want to delete "${page.title}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
contentPagesLog.info(`Deleting page: ${page.id}`);
|
||||
|
||||
await apiClient.delete(`/admin/content-pages/${page.id}`);
|
||||
|
||||
// Remove from local array
|
||||
this.allPages = this.allPages.filter(p => p.id !== page.id);
|
||||
|
||||
contentPagesLog.info('Page deleted successfully');
|
||||
|
||||
} catch (err) {
|
||||
contentPagesLog.error('Error deleting page:', err);
|
||||
alert(`Failed to delete page: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Format date helper
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '—';
|
||||
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return 'Today';
|
||||
} else if (diffDays === 1) {
|
||||
return 'Yesterday';
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
} else {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
154
static/admin/js/platform-homepage.js
Normal file
154
static/admin/js/platform-homepage.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// static/admin/js/platform-homepage.js
|
||||
|
||||
// Use centralized logger
|
||||
const platformHomepageLog = window.LogConfig.loggers.platformHomepage || window.LogConfig.createLogger('platformHomepage');
|
||||
|
||||
// ============================================
|
||||
// PLATFORM HOMEPAGE MANAGER FUNCTION
|
||||
// ============================================
|
||||
function platformHomepageManager() {
|
||||
return {
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Page identifier for sidebar active state
|
||||
currentPage: 'platform-homepage',
|
||||
|
||||
// Platform homepage specific state
|
||||
page: null,
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
successMessage: null,
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
platformHomepageLog.info('=== PLATFORM HOMEPAGE MANAGER INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._platformHomepageInitialized) {
|
||||
platformHomepageLog.warn('Platform homepage manager already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._platformHomepageInitialized = true;
|
||||
|
||||
platformHomepageLog.group('Loading platform homepage');
|
||||
await this.loadPlatformHomepage();
|
||||
platformHomepageLog.groupEnd();
|
||||
|
||||
platformHomepageLog.info('=== PLATFORM HOMEPAGE MANAGER INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Load platform homepage from API
|
||||
async loadPlatformHomepage() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
platformHomepageLog.info('Fetching platform homepage...');
|
||||
|
||||
// Fetch all platform pages
|
||||
const response = await apiClient.get('/admin/content-pages/platform?include_unpublished=true');
|
||||
|
||||
platformHomepageLog.debug('API Response:', response);
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Invalid API response');
|
||||
}
|
||||
|
||||
// Handle response - API returns array directly
|
||||
const pages = Array.isArray(response) ? response : (response.data || response.items || []);
|
||||
|
||||
// Find the platform_homepage page
|
||||
const homepage = pages.find(page => page.slug === 'platform_homepage');
|
||||
|
||||
if (!homepage) {
|
||||
platformHomepageLog.warn('Platform homepage not found, creating default...');
|
||||
// Initialize with default values
|
||||
this.page = {
|
||||
id: null,
|
||||
slug: 'platform_homepage',
|
||||
title: 'Welcome to Our Multi-Vendor Marketplace',
|
||||
content: '<p>Connect vendors with customers worldwide. Build your online store and reach millions of shoppers.</p>',
|
||||
template: 'default',
|
||||
content_format: 'html',
|
||||
meta_description: 'Leading multi-vendor marketplace platform. Connect with thousands of vendors and discover millions of products.',
|
||||
meta_keywords: 'marketplace, multi-vendor, e-commerce, online shopping',
|
||||
is_published: false,
|
||||
show_in_header: false,
|
||||
show_in_footer: false,
|
||||
display_order: 0
|
||||
};
|
||||
} else {
|
||||
this.page = { ...homepage };
|
||||
platformHomepageLog.info('Platform homepage loaded:', this.page);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
platformHomepageLog.error('Error loading platform homepage:', err);
|
||||
this.error = err.message || 'Failed to load platform homepage';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Save platform homepage
|
||||
async savePage() {
|
||||
if (this.saving) return;
|
||||
|
||||
this.saving = true;
|
||||
this.error = null;
|
||||
this.successMessage = null;
|
||||
|
||||
try {
|
||||
platformHomepageLog.info('Saving platform homepage...');
|
||||
|
||||
const payload = {
|
||||
slug: 'platform_homepage',
|
||||
title: this.page.title,
|
||||
content: this.page.content,
|
||||
content_format: this.page.content_format || 'html',
|
||||
template: this.page.template,
|
||||
meta_description: this.page.meta_description,
|
||||
meta_keywords: this.page.meta_keywords,
|
||||
is_published: this.page.is_published,
|
||||
show_in_header: false, // Homepage never in header
|
||||
show_in_footer: false, // Homepage never in footer
|
||||
display_order: 0,
|
||||
vendor_id: null // Platform default
|
||||
};
|
||||
|
||||
platformHomepageLog.debug('Payload:', payload);
|
||||
|
||||
let response;
|
||||
if (this.page.id) {
|
||||
// Update existing page
|
||||
response = await apiClient.put(`/admin/content-pages/${this.page.id}`, payload);
|
||||
platformHomepageLog.info('Platform homepage updated');
|
||||
} else {
|
||||
// Create new page
|
||||
response = await apiClient.post('/admin/content-pages/platform', payload);
|
||||
platformHomepageLog.info('Platform homepage created');
|
||||
}
|
||||
|
||||
if (response) {
|
||||
// Handle response - API returns object directly
|
||||
const pageData = response.data || response;
|
||||
this.page = { ...pageData };
|
||||
this.successMessage = 'Platform homepage saved successfully!';
|
||||
|
||||
// Clear success message after 3 seconds
|
||||
setTimeout(() => {
|
||||
this.successMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
platformHomepageLog.error('Error saving platform homepage:', err);
|
||||
this.error = err.message || 'Failed to save platform homepage';
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user