/** * WHP Integration for Site Builder * Provides save/load functionality via WHP API */ class WHPIntegration { constructor(editor, apiUrl = '/api/site-builder.php') { this.editor = editor; this.apiUrl = apiUrl; this.currentSiteId = null; this.currentSiteName = 'Untitled Site'; this.init(); } init() { // Add save/load buttons to the editor this.addToolbarButtons(); // Auto-save every 30 seconds setInterval(() => this.autoSave(), 30000); // Load site list on startup this.loadSiteList(); } addToolbarButtons() { // Add "Save to WHP" button const saveBtn = document.createElement('button'); saveBtn.id = 'btn-whp-save'; saveBtn.className = 'nav-btn primary'; saveBtn.innerHTML = ` Save `; saveBtn.onclick = () => this.showSaveDialog(); // Add "Load from WHP" button const loadBtn = document.createElement('button'); loadBtn.id = 'btn-whp-load'; loadBtn.className = 'nav-btn'; loadBtn.innerHTML = ` Load `; loadBtn.onclick = () => this.showLoadDialog(); // Insert buttons before export button const exportBtn = document.getElementById('btn-export'); if (exportBtn && exportBtn.parentNode) { exportBtn.parentNode.insertBefore(loadBtn, exportBtn); exportBtn.parentNode.insertBefore(saveBtn, exportBtn); // Add divider const divider = document.createElement('span'); divider.className = 'divider'; exportBtn.parentNode.insertBefore(divider, exportBtn); } } async saveToWHP(siteId = null, siteName = null) { const html = this.editor.getHtml(); const css = this.editor.getCss(); const grapesjs = this.editor.getProjectData(); 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 WHP API first, fall back to localStorage 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 { // Load existing sites list const sitesJson = localStorage.getItem('whp-sites') || '[]'; const sites = JSON.parse(sitesJson); // Update or add 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); } } async loadFromWHP(siteId) { 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.currentSiteName = site.name; this.showNotification(`Loaded "${site.name}" 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'); } } async loadSiteList() { 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 server-side project storage 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); } // Final fallback: localStorage try { return JSON.parse(localStorage.getItem('whp-sites') || '[]'); } catch (e) { return []; } } } async deleteSite(siteId) { 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 server-side project storage 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); } // Final fallback: localStorage 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; } } } showSaveDialog() { const siteName = prompt('Enter a name for your site:', this.currentSiteName); if (siteName) { this.currentSiteName = siteName; this.saveToWHP(this.currentSiteId, siteName); } } async showLoadDialog() { const sites = await this.loadSiteList(); if (sites.length === 0) { alert('No saved sites found.'); return; } // Create a simple modal for site selection 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); // Close on background click modal.onclick = (e) => { if (e.target === modal) { modal.remove(); } }; return modal; } autoSave() { if (this.currentSiteId) { this.saveToWHP(this.currentSiteId, this.currentSiteName); } } 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', () => { // Wait for editor to be defined 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);