/** * WHP Integration for Site Builder * Provides save/load functionality via WHP API when running inside * the Web Hosting Panel, or standalone fallbacks otherwise. * * WHP mode is detected by the presence of window.WHP_CONFIG, which is * injected by the PHP wrapper page. */ const isWHP = !!window.WHP_CONFIG; class WHPIntegration { constructor(editor, apiUrl) { this.editor = editor; if (isWHP) { this.apiUrl = WHP_CONFIG.apiUrl; this.currentSiteId = WHP_CONFIG.siteId; this.currentSiteName = WHP_CONFIG.siteName || WHP_CONFIG.siteDomain || 'Site'; } else { this.apiUrl = apiUrl || '/api/site-builder.php'; this.currentSiteId = null; this.currentSiteName = 'Untitled Site'; } this.init(); } init() { this.addToolbarButtons(); if (isWHP) { // In WHP mode, add the "Back to Panel" link and site domain badge this.addBackButton(); this.addSiteDomainBadge(); } // Auto-save every 30 seconds setInterval(() => this.autoSave(), 30000); // In standalone mode, load the site list on startup. // In WHP mode we always work on one site so no list is needed. if (!isWHP) { this.loadSiteList(); } } // ------------------------------------------------- // Toolbar buttons // ------------------------------------------------- addToolbarButtons() { // Add "Save" button const saveBtn = document.createElement('button'); saveBtn.id = 'btn-whp-save'; saveBtn.className = 'nav-btn primary'; saveBtn.innerHTML = ` Save `; if (isWHP) { // In WHP mode, save immediately (site name comes from WHP) saveBtn.onclick = () => this.saveToWHP(); } else { // Standalone: prompt for a name saveBtn.onclick = () => this.showSaveDialog(); } if (isWHP) { // In WHP mode we only show the Save button (no Load button) const exportBtn = document.getElementById('btn-export'); if (exportBtn && exportBtn.parentNode) { exportBtn.parentNode.insertBefore(saveBtn, exportBtn); const divider = document.createElement('span'); divider.className = 'divider'; exportBtn.parentNode.insertBefore(divider, exportBtn); } } else { // Standalone: show both Save and Load const loadBtn = document.createElement('button'); loadBtn.id = 'btn-whp-load'; loadBtn.className = 'nav-btn'; loadBtn.innerHTML = ` Load `; loadBtn.onclick = () => this.showLoadDialog(); const exportBtn = document.getElementById('btn-export'); if (exportBtn && exportBtn.parentNode) { exportBtn.parentNode.insertBefore(loadBtn, exportBtn); exportBtn.parentNode.insertBefore(saveBtn, exportBtn); const divider = document.createElement('span'); divider.className = 'divider'; exportBtn.parentNode.insertBefore(divider, exportBtn); } } } // ------------------------------------------------- // WHP-only: "Back to Panel" button // ------------------------------------------------- addBackButton() { const backBtn = document.createElement('a'); backBtn.href = WHP_CONFIG.backUrl; backBtn.className = 'nav-btn'; backBtn.innerHTML = ` Back to Panel `; const nav = document.querySelector('.nav-left') || document.querySelector('.editor-nav'); if (nav) { nav.insertBefore(backBtn, nav.firstChild); } } // ------------------------------------------------- // WHP-only: show site domain in the nav bar // ------------------------------------------------- addSiteDomainBadge() { const badge = document.createElement('span'); badge.className = 'nav-site-domain'; badge.style.cssText = 'display:inline-flex;align-items:center;padding:4px 10px;margin-left:8px;font-size:13px;color:#a1a1aa;border:1px solid #3f3f46;border-radius:4px;user-select:all;'; badge.textContent = WHP_CONFIG.siteDomain; const nav = document.querySelector('.nav-left') || document.querySelector('.editor-nav'); if (nav) { nav.appendChild(badge); } } // ------------------------------------------------- // Save // ------------------------------------------------- async saveToWHP(siteId = null, siteName = null) { const html = this.editor.getHtml(); const css = this.editor.getCss(); const grapesjs = this.editor.getProjectData(); if (isWHP) { // ---- WHP mode ---- // Save current page content first if (typeof saveCurrentPageContent === 'function') { saveCurrentPageContent(); } // Build pages array from sitePages (multi-page support) // Files are saved as .html on disk; .htaccess handles clean URLs const allPages = window.sitePages || []; const pagesData = allPages.map(page => ({ filename: page.slug === 'index' ? 'index.html' : page.slug + '.html', slug: page.slug, title: page.name || 'Page', html: page.html || '', css: page.css || '' })); // If no pages tracked, fall back to single-page mode if (pagesData.length === 0) { pagesData.push({ filename: 'index.html', title: WHP_CONFIG.siteName || 'Home', html: html, css: css }); } // Sync navigation across pages if checkbox is checked const syncNavCheckbox = document.getElementById('nav-sync-all-pages'); if (syncNavCheckbox && syncNavCheckbox.checked && pagesData.length > 1) { // Find nav HTML in the current page const navMatch = html.match(/]*class="[^"]*site-navbar[^"]*"[^>]*>[\s\S]*?<\/nav>/i); if (navMatch) { const navHtml = navMatch[0]; pagesData.forEach(page => { if (page.html) { // Replace existing nav or prepend it const hasNav = /]*class="[^"]*site-navbar[^"]*"[^>]*>[\s\S]*?<\/nav>/i.test(page.html); if (hasNav) { page.html = page.html.replace(/]*class="[^"]*site-navbar[^"]*"[^>]*>[\s\S]*?<\/nav>/i, navHtml); } } }); } } const data = { site_id: WHP_CONFIG.siteId, name: WHP_CONFIG.siteName, html: html, css: css, pages: pagesData, grapesjs: grapesjs, modified: Math.floor(Date.now() / 1000) }; try { const response = await fetch(`${this.apiUrl}?action=save`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': WHP_CONFIG.csrfToken }, body: JSON.stringify(data) }); if (!response.ok) throw new Error('API returned ' + response.status); const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { throw new Error('API returned non-JSON response'); } const result = await response.json(); if (result.success) { this.showNotification('Saved successfully!', 'success'); return result; } else { throw new Error(result.error || 'Save failed'); } } catch (error) { console.error('WHP save failed:', error.message); this.showNotification('Save failed: ' + error.message, 'error'); return null; } } else { // ---- Standalone mode (original behaviour) ---- const data = { id: siteId || this.currentSiteId || 'site_' + Date.now(), name: siteName || this.currentSiteName, html: html, css: css, grapesjs: grapesjs, modified: Math.floor(Date.now() / 1000), created: this.currentSiteId ? undefined : Math.floor(Date.now() / 1000) }; try { const response = await fetch(`${this.apiUrl}?action=save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!response.ok) throw new Error('API returned ' + response.status); const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { throw new Error('API returned non-JSON response'); } const result = await response.json(); if (result.success) { this.currentSiteId = result.site.id; this.currentSiteName = result.site.name; this.showNotification(`Saved "${result.site.name}" successfully!`, 'success'); return result.site; } else { throw new Error(result.error || 'Save failed'); } } catch (error) { console.log('WHP API not available, using localStorage fallback:', error.message); return this._saveToLocalStorage(data); } } } async _saveToLocalStorage(data) { // Try server-side project storage first try { const resp = await fetch('/api/projects/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (resp.ok) { const result = await resp.json(); if (result.success) { this.currentSiteId = data.id; this.currentSiteName = data.name; this.showNotification(`Saved "${data.name}" to server!`, 'success'); return { id: data.id, name: data.name }; } } } catch (e) { console.log('Server project save not available, trying localStorage:', e.message); } // Fall back to localStorage with size check try { const sitesJson = localStorage.getItem('whp-sites') || '[]'; const sites = JSON.parse(sitesJson); const idx = sites.findIndex(s => s.id === data.id); if (idx >= 0) { sites[idx] = data; } else { sites.push(data); } localStorage.setItem('whp-sites', JSON.stringify(sites)); this.currentSiteId = data.id; this.currentSiteName = data.name; this.showNotification(`Saved "${data.name}" to local storage!`, 'success'); return { id: data.id, name: data.name }; } catch (err) { if (err.name === 'QuotaExceededError' || err.message.includes('quota')) { this.showNotification('Storage full! Start server.py for unlimited storage.', 'error'); } else { this.showNotification(`Save failed: ${err.message}`, 'error'); } console.error('localStorage save error:', err); } } // ------------------------------------------------- // Load // ------------------------------------------------- async loadFromWHP(siteId) { if (isWHP) { // ---- WHP mode ---- try { const response = await fetch(`${this.apiUrl}?action=load&site_id=${encodeURIComponent(WHP_CONFIG.siteId)}`); if (!response.ok) throw new Error('API returned ' + response.status); const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) throw new Error('Non-JSON response'); const result = await response.json(); if (result.success) { this._applySiteData(result.site || result); } else { throw new Error(result.error || 'Load failed'); } } catch (error) { console.error('WHP load failed:', error.message); this.showNotification('Load failed: ' + error.message, 'error'); } } else { // ---- Standalone mode ---- try { const response = await fetch(`${this.apiUrl}?action=load&id=${encodeURIComponent(siteId)}`); if (!response.ok) throw new Error('API returned ' + response.status); const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) throw new Error('Non-JSON response'); const result = await response.json(); if (result.success) { this._applySiteData(result.site); } else { throw new Error(result.error || 'Load failed'); } } catch (error) { console.log('WHP API not available, loading from localStorage:', error.message); this._loadFromLocalStorage(siteId); } } } _applySiteData(site) { if (site.grapesjs) { this.editor.loadProjectData(site.grapesjs); } else { this.editor.setComponents(site.html || ''); this.editor.setStyle(site.css || ''); } this.currentSiteId = site.id || this.currentSiteId; this.currentSiteName = site.name || this.currentSiteName; this.showNotification(`Loaded "${this.currentSiteName}" successfully!`, 'success'); } async _loadFromLocalStorage(siteId) { // Try server-side project storage first try { const resp = await fetch('/api/projects/' + encodeURIComponent(siteId)); if (resp.ok) { const result = await resp.json(); if (result.success && result.project) { this._applySiteData(result.project); return; } } } catch (e) { console.log('Server project load not available, trying localStorage:', e.message); } // Fall back to localStorage try { const sites = JSON.parse(localStorage.getItem('whp-sites') || '[]'); const site = sites.find(s => s.id === siteId); if (site) { this._applySiteData(site); } else { this.showNotification('Site not found in local storage', 'error'); } } catch (err) { this.showNotification(`Load failed: ${err.message}`, 'error'); } } // ------------------------------------------------- // Site list // ------------------------------------------------- async loadSiteList() { if (isWHP) { // ---- WHP mode ---- try { const response = await fetch(`${this.apiUrl}?action=list`); if (!response.ok) throw new Error('API returned ' + response.status); const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) throw new Error('Non-JSON response'); const result = await response.json(); if (result.success) { return result.sites; } else { throw new Error(result.error || 'Failed to load site list'); } } catch (error) { console.error('WHP loadSiteList failed:', error.message); return []; } } else { // ---- Standalone mode ---- try { const response = await fetch(`${this.apiUrl}?action=list`); if (!response.ok) throw new Error('API returned ' + response.status); const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) throw new Error('Non-JSON response'); const result = await response.json(); if (result.success) { return result.sites; } else { throw new Error(result.error || 'Failed to load site list'); } } catch (error) { console.log('WHP API not available, trying server project list:', error.message); try { const resp = await fetch('/api/projects/list'); if (resp.ok) { const result = await resp.json(); if (result.success && Array.isArray(result.projects)) { return result.projects; } } } catch (e) { console.log('Server project list not available, using localStorage:', e.message); } try { return JSON.parse(localStorage.getItem('whp-sites') || '[]'); } catch (e) { return []; } } } } // ------------------------------------------------- // Delete // ------------------------------------------------- async deleteSite(siteId) { if (isWHP) { // ---- WHP mode ---- try { const response = await fetch(`${this.apiUrl}?action=delete&site_id=${encodeURIComponent(siteId)}`, { method: 'DELETE', headers: { 'X-CSRF-Token': WHP_CONFIG.csrfToken } }); if (!response.ok) throw new Error('API returned ' + response.status); const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) throw new Error('Non-JSON response'); const result = await response.json(); if (result.success) { this.showNotification('Site deleted successfully', 'success'); return true; } else { throw new Error(result.error || 'Delete failed'); } } catch (error) { console.error('WHP delete failed:', error.message); this.showNotification('Delete failed: ' + error.message, 'error'); return false; } } else { // ---- Standalone mode ---- try { const response = await fetch(`${this.apiUrl}?action=delete&id=${encodeURIComponent(siteId)}`); if (!response.ok) throw new Error('API returned ' + response.status); const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) throw new Error('Non-JSON response'); const result = await response.json(); if (result.success) { this.showNotification('Site deleted successfully', 'success'); return true; } else { throw new Error(result.error || 'Delete failed'); } } catch (error) { console.log('WHP API not available, trying server project delete:', error.message); try { const resp = await fetch('/api/projects/' + encodeURIComponent(siteId), { method: 'DELETE' }); if (resp.ok) { this.showNotification('Site deleted from server', 'success'); return true; } } catch (e) { console.log('Server project delete not available, using localStorage:', e.message); } try { const sites = JSON.parse(localStorage.getItem('whp-sites') || '[]'); const filtered = sites.filter(s => s.id !== siteId); localStorage.setItem('whp-sites', JSON.stringify(filtered)); this.showNotification('Site deleted from local storage', 'success'); return true; } catch (err) { this.showNotification(`Delete failed: ${err.message}`, 'error'); return false; } } } } // ------------------------------------------------- // Dialogs (standalone mode only) // ------------------------------------------------- showSaveDialog() { // In WHP mode, save immediately without prompting if (isWHP) { this.saveToWHP(); return; } const siteName = prompt('Enter a name for your site:', this.currentSiteName); if (siteName) { this.currentSiteName = siteName; this.saveToWHP(this.currentSiteId, siteName); } } async showLoadDialog() { // In WHP mode there is no load dialog -- we always work on one site if (isWHP) { this.loadFromWHP(); return; } const sites = await this.loadSiteList(); if (sites.length === 0) { alert('No saved sites found.'); return; } const modal = this.createLoadModal(sites); document.body.appendChild(modal); } createLoadModal(sites) { const modal = document.createElement('div'); modal.className = 'whp-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; `; const content = document.createElement('div'); content.style.cssText = ` background: white; border-radius: 8px; padding: 24px; max-width: 600px; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); `; let html = '

Load Site

'; html += '
'; sites.forEach(site => { const date = new Date(site.modified * 1000).toLocaleString(); html += `
${site.name}
Modified: ${date}
`; }); html += '
'; html += ''; content.innerHTML = html; modal.appendChild(content); modal.onclick = (e) => { if (e.target === modal) { modal.remove(); } }; return modal; } // ------------------------------------------------- // Auto-save // ------------------------------------------------- autoSave() { if (isWHP) { // In WHP mode we always have a siteId, so always auto-save this.saveToWHP(); } else { // Standalone: only auto-save if a site is loaded if (this.currentSiteId) { this.saveToWHP(this.currentSiteId, this.currentSiteName); } } } // ------------------------------------------------- // Notifications // ------------------------------------------------- showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 80px; right: 20px; padding: 16px 24px; background: ${type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : '#0066cc'}; color: white; border-radius: 4px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2); z-index: 10001; animation: slideIn 0.3s ease-out; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease-out'; setTimeout(() => notification.remove(), 300); }, 3000); } } // Auto-initialize when GrapesJS editor is ready document.addEventListener('DOMContentLoaded', () => { const checkEditor = setInterval(() => { if (window.editor) { window.whpInt = new WHPIntegration(window.editor); clearInterval(checkEditor); } }, 100); }); // Add animation styles const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } } `; document.head.appendChild(style);