// Admin Dashboard - Smart & Powerful const { API_BASE, API_ORIGIN } = (() => { const cleanOrigin = (value) => value ? value.replace(/\/$/, '') : ''; const params = new URLSearchParams(window.location.search); const overrideParam = cleanOrigin(params.get('api_origin')); let storedOverride = ''; try { storedOverride = cleanOrigin(localStorage.getItem('marketplace_api_origin')); } catch (error) { storedOverride = ''; } let origin = overrideParam || storedOverride; if (overrideParam && overrideParam !== storedOverride) { try { localStorage.setItem('marketplace_api_origin', overrideParam); } catch (error) { // ignore storage errors (private mode, etc.) } } const { protocol, hostname, port } = window.location; const isLocalHost = ['localhost', '127.0.0.1', '0.0.0.0'].includes(hostname); if (!origin && isLocalHost && port !== '8100') { origin = `${protocol}//127.0.0.1:8100`; } if (origin) { const normalized = cleanOrigin(origin); return { API_BASE: `${normalized}/marketplace/api`, API_ORIGIN: normalized }; } return { API_BASE: '/marketplace/api', API_ORIGIN: '' }; })(); const resolveAssetUrl = (path) => { if (!path) return ''; if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/') && API_ORIGIN) { return `${API_ORIGIN}${path}`; } return path; }; class AdminDashboard { constructor() { this.token = localStorage.getItem('admin_token'); this.currentSection = 'stats'; this.data = { apps: [], articles: [], categories: [], sponsors: [] }; this.editingItem = null; this.init(); } async init() { // Check auth if (!this.token) { this.showLogin(); return; } // Try to load stats to verify token try { await this.loadStats(); this.showDashboard(); this.setupEventListeners(); await this.loadAllData(); } catch (error) { if (error.status === 401) { this.showLogin(); } } } showLogin() { document.getElementById('login-screen').classList.remove('hidden'); document.getElementById('admin-dashboard').classList.add('hidden'); // Set up login button click handler const loginBtn = document.getElementById('login-btn'); if (loginBtn) { loginBtn.onclick = async () => { const password = document.getElementById('password').value; await this.login(password); }; } } async login(password) { try { const response = await fetch(`${API_BASE}/admin/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); if (!response.ok) throw new Error('Invalid password'); const data = await response.json(); this.token = data.token; localStorage.setItem('admin_token', this.token); document.getElementById('login-screen').classList.add('hidden'); this.showDashboard(); this.setupEventListeners(); await this.loadAllData(); } catch (error) { document.getElementById('login-error').textContent = 'Invalid password'; document.getElementById('password').value = ''; } } showDashboard() { document.getElementById('login-screen').classList.add('hidden'); document.getElementById('admin-dashboard').classList.remove('hidden'); } setupEventListeners() { // Navigation document.querySelectorAll('.nav-btn').forEach(btn => { btn.onclick = () => this.switchSection(btn.dataset.section); }); // Logout document.getElementById('logout-btn').onclick = () => this.logout(); // Export/Backup document.getElementById('export-btn').onclick = () => this.exportData(); document.getElementById('backup-btn').onclick = () => this.backupDatabase(); // Search ['apps', 'articles'].forEach(type => { const searchInput = document.getElementById(`${type}-search`); if (searchInput) { searchInput.oninput = (e) => this.filterTable(type, e.target.value); } }); // Category filter const categoryFilter = document.getElementById('apps-filter'); if (categoryFilter) { categoryFilter.onchange = (e) => this.filterByCategory(e.target.value); } // Save button in modal document.getElementById('save-btn').onclick = () => this.saveItem(); } async loadAllData() { try { await this.loadStats(); } catch (e) { console.error('Failed to load stats:', e); } try { await this.loadApps(); } catch (e) { console.error('Failed to load apps:', e); } try { await this.loadArticles(); } catch (e) { console.error('Failed to load articles:', e); } try { await this.loadCategories(); } catch (e) { console.error('Failed to load categories:', e); } try { await this.loadSponsors(); } catch (e) { console.error('Failed to load sponsors:', e); } this.populateCategoryFilter(); } async apiCall(endpoint, options = {}) { const isFormData = options.body instanceof FormData; const headers = { 'Authorization': `Bearer ${this.token}`, ...options.headers }; if (!isFormData && !headers['Content-Type']) { headers['Content-Type'] = 'application/json'; } const response = await fetch(`${API_BASE}${endpoint}`, { ...options, headers }); if (response.status === 401) { this.logout(); throw { status: 401 }; } if (!response.ok) throw new Error(`API Error: ${response.status}`); return response.json(); } async loadStats() { const stats = await this.apiCall(`/admin/stats?_=${Date.now()}`, { cache: 'no-store' }); document.getElementById('stat-apps').textContent = stats.apps.total; document.getElementById('stat-featured').textContent = stats.apps.featured; document.getElementById('stat-sponsored').textContent = stats.apps.sponsored; document.getElementById('stat-articles').textContent = stats.articles; document.getElementById('stat-sponsors').textContent = stats.sponsors.active; document.getElementById('stat-views').textContent = this.formatNumber(stats.total_views); } async loadApps() { this.data.apps = await this.apiCall(`/apps?limit=100&_=${Date.now()}`, { cache: 'no-store' }); this.renderAppsTable(this.data.apps); } async loadArticles() { this.data.articles = await this.apiCall(`/articles?limit=100&_=${Date.now()}`, { cache: 'no-store' }); this.renderArticlesTable(this.data.articles); } async loadCategories() { const cacheBuster = Date.now(); this.data.categories = await this.apiCall(`/categories?_=${cacheBuster}`, { cache: 'no-store' }); this.renderCategoriesTable(this.data.categories); } async loadSponsors() { const cacheBuster = Date.now(); this.data.sponsors = await this.apiCall(`/sponsors?limit=100&_=${cacheBuster}`, { cache: 'no-store' }); this.renderSponsorsTable(this.data.sponsors); } renderAppsTable(apps) { const table = document.getElementById('apps-table'); table.innerHTML = ` ${apps.map(app => ` `).join('')}
ID Name Category Type Rating Downloads Status Actions
${app.id} ${app.name} ${app.category} ${app.type} ◆ ${app.rating}/5 ${this.formatNumber(app.downloads)} ${app.featured ? 'Featured' : ''} ${app.sponsored ? '' : ''}
`; } renderArticlesTable(articles) { const table = document.getElementById('articles-table'); table.innerHTML = ` ${articles.map(article => ` `).join('')}
ID Title Category Author Published Views Actions
${article.id} ${article.title} ${article.category} ${article.author} ${new Date(article.published_date).toLocaleDateString()} ${this.formatNumber(article.views)}
`; } renderCategoriesTable(categories) { const table = document.getElementById('categories-table'); table.innerHTML = ` ${categories.map(cat => ` `).join('')}
Order Icon Name Description Actions
${cat.order_index} ${cat.icon} ${cat.name} ${cat.description}
`; } renderSponsorsTable(sponsors) { const table = document.getElementById('sponsors-table'); table.innerHTML = ` ${sponsors.map(sponsor => ` `).join('')}
ID Logo Company Tier Start End Status Actions
${sponsor.id} ${sponsor.logo_url ? `` : '-'} ${sponsor.company_name} ${sponsor.tier} ${new Date(sponsor.start_date).toLocaleDateString()} ${new Date(sponsor.end_date).toLocaleDateString()} ${sponsor.active ? 'Active' : 'Inactive'}
`; } showAddForm(type) { this.editingItem = null; this.showModal(type, null); } async editItem(type, id) { const item = this.data[type].find(i => i.id === id); if (item) { this.editingItem = item; this.showModal(type, item); } } async duplicateItem(type, id) { const item = this.data[type].find(i => i.id === id); if (item) { const newItem = { ...item }; delete newItem.id; newItem.name = `${newItem.name || newItem.title} (Copy)`; if (newItem.slug) newItem.slug = `${newItem.slug}-copy-${Date.now()}`; this.editingItem = null; this.showModal(type, newItem); } } showModal(type, item) { const modal = document.getElementById('form-modal'); const title = document.getElementById('modal-title'); const body = document.getElementById('modal-body'); title.textContent = item ? `Edit ${type.slice(0, -1)}` : `Add New ${type.slice(0, -1)}`; if (type === 'apps') { body.innerHTML = this.getAppForm(item); } else if (type === 'articles') { body.innerHTML = this.getArticleForm(item); } else if (type === 'categories') { body.innerHTML = this.getCategoryForm(item); } else if (type === 'sponsors') { body.innerHTML = this.getSponsorForm(item); } modal.classList.remove('hidden'); modal.dataset.type = type; if (type === 'sponsors') { this.setupLogoUploadHandlers(); } } getAppForm(app) { return `
Supports markdown: **bold**, *italic*, [links](url), # headers, etc.
`; } getArticleForm(article) { return `
`; } getCategoryForm(category) { return `
`; } getSponsorForm(sponsor) { const existingFile = sponsor?.logo_url ? sponsor.logo_url.split('/').pop().split('?')[0] : ''; return ` `; } async saveItem() { const modal = document.getElementById('form-modal'); const type = modal.dataset.type; try { if (type === 'sponsors') { const fileInput = document.getElementById('form-logo-file'); if (fileInput && fileInput.files && fileInput.files[0]) { const formData = new FormData(); formData.append('file', fileInput.files[0]); formData.append('folder', 'sponsors'); const uploadResponse = await this.apiCall('/admin/upload-image', { method: 'POST', body: formData }); if (!uploadResponse.url) { throw new Error('Image upload failed'); } document.getElementById('form-logo-url').value = uploadResponse.url; } } const data = this.collectFormData(type); if (this.editingItem) { await this.apiCall(`/admin/${type}/${this.editingItem.id}`, { method: 'PUT', body: JSON.stringify(data) }); } else { await this.apiCall(`/admin/${type}`, { method: 'POST', body: JSON.stringify(data) }); } this.closeModal(); await this[`load${type.charAt(0).toUpperCase() + type.slice(1)}`](); await this.loadStats(); } catch (error) { alert('Error saving item: ' + error.message); } } collectFormData(type) { const data = {}; if (type === 'apps') { data.name = document.getElementById('form-name').value; data.slug = document.getElementById('form-slug').value || this.generateSlug(data.name); data.description = document.getElementById('form-description').value; data.category = document.getElementById('form-category').value; data.type = document.getElementById('form-type').value; const rating = parseFloat(document.getElementById('form-rating').value); const downloads = parseInt(document.getElementById('form-downloads').value, 10); data.rating = Number.isFinite(rating) ? rating : 0; data.downloads = Number.isFinite(downloads) ? downloads : 0; data.image = document.getElementById('form-image').value; data.website_url = document.getElementById('form-website').value; data.github_url = document.getElementById('form-github').value; data.pricing = document.getElementById('form-pricing').value; data.contact_email = document.getElementById('form-email').value; data.featured = document.getElementById('form-featured').checked ? 1 : 0; data.sponsored = document.getElementById('form-sponsored').checked ? 1 : 0; data.long_description = document.getElementById('form-long-description').value; data.installation_command = document.getElementById('form-installation').value; data.examples = document.getElementById('form-examples').value; data.integration_guide = document.getElementById('form-integration').value; data.documentation = document.getElementById('form-documentation').value; data.requirements = document.getElementById('form-requirements').value; } else if (type === 'articles') { data.title = document.getElementById('form-title').value; data.slug = this.generateSlug(data.title); data.author = document.getElementById('form-author').value; data.category = document.getElementById('form-category').value; data.featured_image = document.getElementById('form-image').value; data.content = document.getElementById('form-content').value; } else if (type === 'categories') { data.name = document.getElementById('form-name').value; data.slug = this.generateSlug(data.name); data.icon = document.getElementById('form-icon').value; data.description = document.getElementById('form-description').value; const orderIndex = parseInt(document.getElementById('form-order').value, 10); data.order_index = Number.isFinite(orderIndex) ? orderIndex : 0; } else if (type === 'sponsors') { data.company_name = document.getElementById('form-name').value; data.logo_url = document.getElementById('form-logo-url').value; data.tier = document.getElementById('form-tier').value; data.landing_url = document.getElementById('form-landing').value; data.banner_url = document.getElementById('form-banner').value; data.start_date = document.getElementById('form-start').value; data.end_date = document.getElementById('form-end').value; data.active = document.getElementById('form-active').checked ? 1 : 0; } return data; } setupLogoUploadHandlers() { const fileInput = document.getElementById('form-logo-file'); const preview = document.getElementById('form-logo-preview'); const logoUrlInput = document.getElementById('form-logo-url'); const trigger = document.getElementById('form-logo-button'); const fileNameEl = document.getElementById('form-logo-filename'); if (!fileInput || !preview || !logoUrlInput) return; const setFileName = (text) => { if (fileNameEl) { fileNameEl.textContent = text; } }; const setEmptyState = () => { preview.innerHTML = 'No logo uploaded'; preview.classList.add('empty'); setFileName('No file selected'); }; const setExistingState = () => { if (logoUrlInput.value) { const existingFile = logoUrlInput.value.split('/').pop().split('?')[0]; preview.innerHTML = `Logo preview`; preview.classList.remove('empty'); setFileName(existingFile ? `Current: ${existingFile}` : 'Current logo'); } else { setEmptyState(); } }; setExistingState(); if (trigger) { trigger.onclick = () => fileInput.click(); } fileInput.addEventListener('change', (event) => { const file = event.target.files && event.target.files[0]; if (!file) { setExistingState(); return; } setFileName(file.name); const reader = new FileReader(); reader.onload = () => { preview.innerHTML = `Logo preview`; preview.classList.remove('empty'); }; reader.readAsDataURL(file); }); } async deleteItem(type, id) { if (!confirm(`Are you sure you want to delete this ${type.slice(0, -1)}?`)) return; try { await this.apiCall(`/admin/${type}/${id}`, { method: 'DELETE' }); await this[`load${type.charAt(0).toUpperCase() + type.slice(1)}`](); await this.loadStats(); } catch (error) { alert('Error deleting item: ' + error.message); } } async deleteCategory(id) { const hasApps = this.data.apps.some(app => app.category === this.data.categories.find(c => c.id === id)?.name ); if (hasApps) { alert('Cannot delete category with existing apps'); return; } await this.deleteItem('categories', id); } closeModal() { document.getElementById('form-modal').classList.add('hidden'); this.editingItem = null; } switchSection(section) { // Update navigation document.querySelectorAll('.nav-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.section === section); }); // Show section document.querySelectorAll('.content-section').forEach(sec => { sec.classList.remove('active'); }); document.getElementById(`${section}-section`).classList.add('active'); this.currentSection = section; } filterTable(type, query) { const items = this.data[type].filter(item => { const searchText = Object.values(item).join(' ').toLowerCase(); return searchText.includes(query.toLowerCase()); }); if (type === 'apps') { this.renderAppsTable(items); } else if (type === 'articles') { this.renderArticlesTable(items); } } filterByCategory(category) { const apps = category ? this.data.apps.filter(app => app.category === category) : this.data.apps; this.renderAppsTable(apps); } populateCategoryFilter() { const filter = document.getElementById('apps-filter'); if (!filter) return; filter.innerHTML = ''; this.data.categories.forEach(cat => { filter.innerHTML += ``; }); } async exportData() { const data = { apps: this.data.apps, articles: this.data.articles, categories: this.data.categories, sponsors: this.data.sponsors, exported: new Date().toISOString() }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `marketplace-export-${Date.now()}.json`; a.click(); } async backupDatabase() { // In production, this would download the SQLite file alert('Database backup would be implemented on the server side'); } generateSlug(text) { return text.toLowerCase() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .trim(); } formatNumber(num) { if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; return num.toString(); } logout() { localStorage.removeItem('admin_token'); this.token = null; this.showLogin(); } } // Initialize const admin = new AdminDashboard();