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:
2025-11-28 07:17:30 +01:00
parent 9f8ad71d85
commit 83a6831b2e
20 changed files with 4177 additions and 0 deletions

View 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;
}
}
};
}

View 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'
});
}
}
};
}

View 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;
}
}
};
}