Files
gkachele-saas/demo/templates/customizer.html

1090 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<!--
GKACHELE™ PageBuilder
Version: 3.1 - Apple Design + Flask Integration
Based on saas-demo.html
-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PageBuilder - {{ site_name }} - GKACHELE™</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f5f5f7;
color: #1d1d1f;
overflow: hidden;
}
/* LOADING SPINNER */
.loading-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
z-index: 9999;
justify-content: center;
align-items: center;
flex-direction: column;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: #fa7921;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* LAYOUT PRINCIPAL */
.editor-screen {
display: flex;
height: 100vh;
flex-direction: column;
background: #f5f5f7;
}
/* TOP NAVBAR */
.navbar {
background: white;
border-bottom: 1px solid #e5e5e7;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.navbar-brand {
font-size: 16px;
font-weight: 700;
color: #fa7921;
display: flex;
align-items: center;
gap: 8px;
}
.navbar-info {
display: flex;
align-items: center;
gap: 20px;
}
.nav-item {
font-size: 12px;
color: #86868b;
}
.nav-item strong {
color: #1d1d1f;
}
.nav-actions {
display: flex;
gap: 6px;
align-items: center;
}
/* EDITOR CONTAINER */
.editor-container {
display: flex;
flex: 1;
overflow: hidden;
gap: 0;
}
/* SIDEBAR */
.sidebar {
width: 260px;
background: white;
border-right: 1px solid #e5e5e7;
overflow-y: auto;
padding: 16px 0;
}
.sidebar-section {
margin-bottom: 0;
border-bottom: 1px solid #e5e5e7;
}
.sidebar-title {
padding: 10px 16px;
font-weight: 600;
font-size: 10px;
text-transform: uppercase;
color: #a1a1a6;
letter-spacing: 0.7px;
}
.block-item {
padding: 10px 16px;
cursor: grab;
user-select: none;
display: flex;
align-items: center;
gap: 10px;
color: #1d1d1f;
font-size: 12px;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.block-item:hover {
background: #f5f5f7;
color: #fa7921;
border-left-color: #fa7921;
}
.block-item.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.block-item i {
color: #a1a1a6;
font-size: 13px;
width: 14px;
}
.block-counter {
padding: 12px 16px;
background: #fef5ed;
border: 1px solid #fac9a6;
border-radius: 8px;
margin: 12px 12px 0 12px;
font-size: 12px;
color: #1d1d1f;
}
.block-counter strong {
color: #fa7921;
}
/* CANVAS AREA */
.canvas-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #f5f5f7;
}
.canvas-header {
background: white;
border-bottom: 1px solid #e5e5e7;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.canvas-header h3 {
font-size: 12px;
color: #86868b;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.canvas-tools {
display: flex;
gap: 6px;
}
.canvas-main {
flex: 1;
overflow-y: auto;
padding: 24px;
background: #f5f5f7;
transition: all 0.3s;
}
.canvas-wrapper {
max-width: 100%;
background: white;
border-radius: 16px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
padding: 40px;
min-height: 600px;
position: relative;
overflow: hidden;
}
/* BLOQUES */
.blocks-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
position: relative;
z-index: 2;
}
.block-element {
padding: 18px;
background: #f5f5f7;
border: 1px solid #e5e5e7;
border-radius: 10px;
position: relative;
transition: all 0.2s;
cursor: grab;
}
.block-element:hover {
border-color: #fa7921;
background: #fef5ed;
box-shadow: 0 2px 8px rgba(250, 121, 33, 0.1);
}
.block-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.block-label {
font-weight: 600;
color: #1d1d1f;
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.block-label i {
color: #fa7921;
font-size: 11px;
}
.block-controls {
display: flex;
gap: 3px;
}
.block-ctrl {
background: white;
border: 1px solid #e5e5e7;
width: 28px;
height: 28px;
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #86868b;
font-size: 11px;
transition: all 0.2s;
}
.block-ctrl:hover {
border-color: #fa7921;
color: #fa7921;
background: #fef5ed;
}
.resize-handle {
position: absolute;
width: 14px;
height: 14px;
background: white;
border: 2px solid #fa7921;
border-radius: 2px;
cursor: se-resize;
opacity: 0;
right: -6px;
bottom: -6px;
transition: opacity 0.2s;
z-index: 10;
}
.block-element:hover .resize-handle {
opacity: 1;
}
.block-content {
font-size: 13px;
line-height: 1.5;
color: #424245;
}
.canvas-empty {
text-align: center;
padding: 60px 40px;
color: #a1a1a6;
grid-column: 1 / -1;
}
.canvas-empty i {
font-size: 48px;
color: #d2d2d7;
margin-bottom: 16px;
}
.canvas-empty p {
font-size: 14px;
}
/* BUTTONS */
.btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: #fa7921;
color: white;
}
.btn-primary:hover {
background: #f26b1f;
}
.btn-secondary {
background: #f5f5f7;
color: #1d1d1f;
}
.btn-secondary:hover {
background: #e5e5e7;
}
.btn-sm {
padding: 8px 12px;
font-size: 12px;
}
.btn-icon {
background: none;
border: none;
cursor: pointer;
padding: 8px;
color: #86868b;
transition: all 0.2s;
font-size: 16px;
}
.btn-icon:hover {
color: #1d1d1f;
}
/* PREVIEW MODAL (Apple Browser Style) */
.preview-modal {
display: none;
position: fixed;
z-index: 2000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
justify-content: center;
align-items: center;
}
.preview-modal.active {
display: flex;
}
.preview-container {
background: white;
border-radius: 12px;
width: 96%;
max-width: 1200px;
height: 90vh;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid #d2d2d7;
}
.browser-toolbar {
background: #f5f5f7;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 16px;
border-bottom: 1px solid #e5e5e7;
}
.browser-actions {
display: flex;
gap: 8px;
}
.browser-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.dot-red {
background: #ff5f56;
cursor: pointer;
}
.dot-yellow {
background: #ffbd2e;
}
.dot-green {
background: #27c93f;
}
.browser-url-bar {
flex: 1;
background: white;
border: 1px solid #d2d2d7;
border-radius: 6px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #424245;
}
.browser-url-bar i {
margin-right: 6px;
font-size: 10px;
color: #86868b;
}
.preview-content {
flex: 1;
overflow-y: auto;
background: white;
position: relative;
}
.preview-page {
max-width: 100%;
margin: 0 auto;
min-height: 100%;
}
/* EDITOR MODAL */
.modal-overlay {
display: none;
position: fixed;
z-index: 3000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(5px);
justify-content: center;
align-items: center;
}
.modal-overlay.active {
display: flex;
}
.modal-dialog {
background: white;
border-radius: 16px;
width: 92%;
max-width: 480px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
max-height: 85vh;
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid #e5e5e7;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
font-size: 16px;
font-weight: 600;
}
.modal-body {
padding: 20px;
overflow-y: auto;
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid #e5e5e7;
display: flex;
justify-content: flex-end;
gap: 10px;
background: #f9f9fa;
border-radius: 0 0 16px 16px;
}
.field-row {
margin-bottom: 12px;
}
.field-row label {
display: block;
font-size: 12px;
font-weight: 600;
margin-bottom: 6px;
color: #1d1d1f;
}
.field-row input,
.field-row select,
.field-row textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #d2d2d7;
border-radius: 8px;
font-size: 13px;
font-family: inherit;
}
.field-row input:focus {
outline: none;
border-color: #fa7921;
box-shadow: 0 0 0 2px rgba(250, 121, 33, 0.1);
}
.color-picker {
height: 36px;
padding: 2px;
}
</style>
</head>
<body>
<!-- LOADING -->
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
<div style="margin-top:16px;font-size:14px;font-weight:600">Guardando...</div>
</div>
<!-- EDITOR SCREEN -->
<div class="editor-screen">
<div class="navbar">
<div class="navbar-brand">
<i class="fas fa-layer-group"></i> GKACHELE™ Builder
<span
style="font-size:10px; color:#a1a1a6; margin-left:8px; font-family:monospace; background:#e5e5e7; padding:2px 6px; border-radius:4px;">{{
security_hash }}</span>
</div>
<div class="navbar-info">
<div class="nav-item">Sitio: <strong>{{ site_name }}</strong></div>
<div class="nav-item">Plan: <strong style="text-transform: capitalize; color: #fa7921;">{{ user_plan
}}</strong></div>
</div>
<div class="nav-actions">
<button class="btn btn-secondary btn-sm" onclick="openPreview()"><i class="fas fa-eye"></i> Vista
Previa</button>
<button class="btn btn-primary btn-sm" onclick="savePage()"><i class="fas fa-save"></i>
Publicar</button>
<button class="btn-icon" onclick="window.location.href='/dashboard'" title="Salir"><i
class="fas fa-times"></i></button>
</div>
</div>
<div class="editor-container">
<!-- SIDEBAR -->
<div class="sidebar">
<div class="sidebar-section">
<div class="sidebar-title">Básicos (Todos)</div>
<div class="block-item" draggable="true" data-type="heading"><i class="fas fa-heading"></i>
Encabezado</div>
<div class="block-item" draggable="true" data-type="paragraph"><i class="fas fa-align-left"></i>
Párrafo</div>
<div class="block-item" draggable="true" data-type="image"><i class="fas fa-image"></i> Imagen</div>
<div class="block-item" draggable="true" data-type="video"><i class="fab fa-youtube"></i> Video
Embed</div>
<div class="block-item" draggable="true" data-type="separator"><i class="fas fa-minus"></i>
Separador</div>
<div class="block-item" draggable="true" data-type="list"><i class="fas fa-list"></i> Lista</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">Avanzados</div>
<div class="block-item" draggable="true" data-type="gallery"><i class="fas fa-images"></i>
Galería / Slider</div>
<div class="block-item" draggable="true" data-type="map"><i class="fas fa-map-marker-alt"></i> Mapa
</div>
<div class="block-item" draggable="true" data-type="form"><i class="fas fa-envelope"></i>
Formulario</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">Social & Contacto</div>
<div class="block-item" draggable="true" data-type="whatsapp_float"><i
class="fab fa-whatsapp"></i> Botón WhatsApp</div>
<div class="block-item" draggable="true" data-type="social"><i class="fas fa-share-alt"></i> Redes
Sociales</div>
</div>
<div class="block-counter">
<strong id="blockCount">0</strong> / <strong id="blockLimit">--</strong> bloques
</div>
</div>
<!-- CANVAS -->
<div class="canvas-panel">
<div class="canvas-header">
<h3>Lienzo - {{ rubro|title }}</h3>
<div class="canvas-tools">
<button class="btn btn-secondary btn-sm" onclick="setCanvasBackground()"><i
class="fas fa-paint-roller"></i> Fondo</button>
</div>
</div>
<div class="canvas-main">
<div class="canvas-wrapper" id="canvasWrapper">
<!-- Background Layer -->
<div id="bgLayer" style="position:absolute; inset:0; z-index:0; overflow:hidden;"></div>
<!-- Content Layer -->
<div id="canvasContent" class="blocks-container">
<!-- Blocks Injected Here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- PREVIEW MODAL -->
<div class="preview-modal" id="previewModal">
<div class="preview-container">
<div class="browser-toolbar">
<div class="browser-actions">
<div class="browser-dot dot-red" onclick="closePreview()"></div>
<div class="browser-dot dot-yellow"></div>
<div class="browser-dot dot-green"></div>
</div>
<div class="browser-url-bar">
<i class="fas fa-lock"></i> gkachele-saas.com/{{ slug }}
</div>
</div>
<div class="preview-content" id="previewBody">
<!-- Preview Rendered Here -->
</div>
</div>
</div>
<!-- EDITOR MODAL -->
<div class="modal-overlay" id="editorModal">
<div class="modal-dialog">
<div class="modal-header">
<h3 id="modalTitle">Editar Bloque</h3>
<button class="btn-icon" onclick="closeModal()"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body" id="modalBody">
<!-- Fields Injected Here -->
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal()">Cancelar</button>
<button class="btn btn-primary" onclick="saveBlockChanges()">Guardar</button>
</div>
</div>
</div>
<script>
// --- STATE MANAGEMENT ---
const SITE_CONFIG = {
plan: "{{ user_plan }}",
rubro: "{{ rubro }}",
site_id: "{{ site_id }}",
limits: { base: 10, pro: 50, premium: 9999 }
};
let state = {
blocks: {{ content.get('blocks', []) | tojson | safe }},
bg: {{ content.get('bg', None) | tojson | safe }},
editingIdx: null,
draggedType: null
};
// --- INITIALIZATION ---
document.addEventListener('DOMContentLoaded', () => {
updateBlockCounter();
// Auto-populate for new sites based on Rubro
if (state.blocks.length === 0) {
initRubroContent();
} else {
renderBlocks();
applyBackground();
}
});
function initRubroContent() {
const rubro = SITE_CONFIG.rubro.toLowerCase();
let starters = [];
// Common Header
starters.push({ id: Math.random(), type: 'heading', colSpan: 4, data: { text: 'Bienvenidos a ' + '{{ site_name }}', color: '#1d1d1f' } });
if (rubro === 'restaurante') {
starters.push({ id: Math.random(), type: 'image', colSpan: 4, data: { url: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&q=80', alt: 'Plato' } });
starters.push({ id: Math.random(), type: 'paragraph', colSpan: 2, data: { text: 'Ofrecemos la mejor experiencia culinaria...' } });
starters.push({ id: Math.random(), type: 'list', colSpan: 2, data: { title: 'Menú Del Día', items: ['Entrante: Sopa', 'Principal: Chuleta', 'Postre: Flan'] } });
} else {
starters.push({ id: Math.random(), type: 'paragraph', colSpan: 4, data: { text: 'Describe tu negocio aquí...' } });
}
state.blocks = starters;
renderBlocks();
}
// --- DRAG & DROP SYSTEM ---
document.addEventListener('dragstart', (e) => {
// Use closest to ensure we capture the block even if icon/text is clicked
const item = e.target.closest('.block-item');
if (item) {
state.draggedType = item.dataset.type;
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('text/plain', item.dataset.type); // Required for Firefox
}
});
// Use content container as drop zone (Matches saas-demo.html)
const dropZone = document.getElementById('canvasContent');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
// Removed specific styling to avoid layout shift
});
dropZone.addEventListener('dragleave', (e) => {
// Removed specific styling to avoid layout shift
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
if (!state.draggedType) return;
const limit = SITE_CONFIG.limits[SITE_CONFIG.plan] || 10;
if (state.blocks.length >= limit) {
alert(`Plan ${SITE_CONFIG.plan.toUpperCase()} limitado a ${limit} bloques.`);
return;
}
const newBlock = {
id: Math.random(),
type: state.draggedType,
colSpan: 1, // Default width
data: getDefaultData(state.draggedType)
};
state.blocks.push(newBlock);
renderBlocks();
updateBlockCounter();
// Reset
state.draggedType = null;
});
function getDefaultData(type) {
switch (type) {
case 'heading': return { text: 'Nuevo Título', color: '#1d1d1f' };
case 'paragraph': return { text: 'Escribe tu contenido aquí...' };
case 'image': return { url: 'https://via.placeholder.com/400x300', alt: 'Imagen' };
case 'video': return { url: 'https://www.youtube.com/embed/dQw4w9WgXcQ' };
case 'separator': return { style: 'solid', color: '#e5e5e7' };
case 'list': return { title: 'Lista', items: ['Item 1', 'Item 2'] };
case 'gallery': return { images: ['https://via.placeholder.com/150', 'https://via.placeholder.com/150'], speed: 3000 };
case 'map': return { address: 'Madrid, España' };
case 'form': return { email: '{{ user_email }}', fields: ['Nombre', 'Mensaje'] };
case 'whatsapp_float': return { number: '', message: 'Hola!' };
case 'social': return { fb: '', ig: '' };
default: return {};
}
}
// --- RENDERING SYSTEM ---
function renderBlocks() {
const container = document.getElementById('canvasContent');
if (state.blocks.length === 0) {
container.innerHTML = `<div class="canvas-empty"><i class="fas fa-magic"></i><p>Arrastra bloques aquí</p></div>`;
return;
}
container.innerHTML = state.blocks.map((b, idx) => `
<div class="block-element" style="grid-column: span ${b.colSpan};">
<div class="block-head">
<div class="block-label"><i class="fas fa-${getIcon(b.type)}"></i> ${b.type.toUpperCase()}</div>
<div class="block-controls">
<button class="block-ctrl" onclick="editBlock(${idx})"><i class="fas fa-pen"></i></button>
<button class="block-ctrl" onclick="deleteBlock(${idx})"><i class="fas fa-trash"></i></button>
</div>
</div>
<div class="block-content">
${getPreviewHTML(b)}
</div>
<div class="resize-handle" onmousedown="startResize(event, ${idx})"></div>
</div>
`).join('');
}
function getIcon(type) {
const map = { heading: 'heading', paragraph: 'align-left', image: 'image', video: 'play-circle', gallery: 'images', map: 'map-marker', form: 'envelope', whatsapp_float: 'whatsapp', social: 'share-alt' };
return map[type] || 'cube';
}
function getPreviewHTML(b) {
// Simplified preview for Editor
if (b.type === 'heading') return `<h3 style="color:${b.data.color}">${b.data.text}</h3>`;
if (b.type === 'image') return `<img src="${b.data.url}" style="max-width:100%; height:120px; object-fit:cover; border-radius:6px;">`;
if (b.type === 'video') return `<div style="background:#000; color:#fff; padding:10px; text-align:center; border-radius:6px;"><i class="fab fa-youtube"></i> Video Embed</div>`;
if (b.type === 'gallery') return `<div style="display:flex; gap:4px; overflow:hidden;"><img src="${b.data.images[0]}" style="height:50px"><img src="${b.data.images[1] || ''}" style="height:50px"></div>`;
if (b.type === 'map') return `<div style="background:#e1e1e1; padding:10px; text-align:center; border-radius:6px;">Mapa: ${b.data.address}</div>`;
return `<p style="font-size:12px; color:#666;">${JSON.stringify(b.data).substring(0, 50)}...</p>`;
}
function updateBlockCounter() {
document.getElementById('blockCount').textContent = state.blocks.length;
document.getElementById('blockLimit').textContent = SITE_CONFIG.limits[SITE_CONFIG.plan];
}
// --- EDITING SYSTEM (Modal) ---
function editBlock(idx) {
state.editingIdx = idx;
const b = state.blocks[idx];
const modalBody = document.getElementById('modalBody');
document.getElementById('modalTitle').textContent = `Editar ${b.type}`;
let fields = '';
if (b.type === 'heading') {
fields += createField('text', 'Texto', b.data.text);
fields += createField('color', 'Color', b.data.color, 'color');
} else if (b.type === 'image') {
fields += createField('url', 'URL Imagen', b.data.url);
} else if (b.type === 'video') {
fields += createField('url', 'URL Video (Youtube/Vimeo)', b.data.url);
} else if (b.type === 'paragraph') {
fields += `<div class="field-row"><label>Contenido</label><textarea id="field_text" rows="4">${b.data.text}</textarea></div>`;
} else if (b.type === 'map') {
fields += createField('address', 'Dirección Completa', b.data.address);
} else {
fields += `<p>Edición rápida no disponible para este bloque. Usa JSON:</p>`;
fields += `<textarea id="field_json" rows="5" style="width:100%;">${JSON.stringify(b.data)}</textarea>`;
}
modalBody.innerHTML = fields;
document.getElementById('editorModal').classList.add('active');
}
function createField(id, label, value, type = 'text') {
return `<div class="field-row"><label>${label}</label><input type="${type}" id="field_${id}" value="${value}" class="${type === 'color' ? 'color-picker' : ''}"></div>`;
}
function saveBlockChanges() {
const b = state.blocks[state.editingIdx];
// Simple mapping based on known IDs
if (document.getElementById('field_text')) b.data.text = document.getElementById('field_text').value;
if (document.getElementById('field_url')) b.data.url = document.getElementById('field_url').value;
if (document.getElementById('field_color')) b.data.color = document.getElementById('field_color').value;
if (document.getElementById('field_address')) b.data.address = document.getElementById('field_address').value;
if (document.getElementById('field_json')) {
try { b.data = JSON.parse(document.getElementById('field_json').value); }
catch (e) { alert('JSON Inválido'); return; }
}
closeModal();
renderBlocks();
}
function closeModal() {
document.getElementById('editorModal').classList.remove('active');
}
function deleteBlock(idx) {
if (confirm('¿Eliminar bloque?')) {
state.blocks.splice(idx, 1);
renderBlocks();
updateBlockCounter();
}
}
// --- RESIZE LOGIC ---
function startResize(e, idx) {
const startX = e.clientX;
const startSpan = state.blocks[idx].colSpan || 1;
function onMove(ev) {
const diff = ev.clientX - startX;
const steps = Math.round(diff / 200); // Sensitivity
let newSpan = Math.max(1, Math.min(4, startSpan + steps));
state.blocks[idx].colSpan = newSpan;
renderBlocks();
}
function onUp() {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
}
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
}
// --- BACKGROUND LOGIC ---
function setCanvasBackground() {
const plan = (SITE_CONFIG.plan || 'base').toLowerCase();
const allowed = plan === 'base'
? ['image']
: (plan === 'pro' ? ['color', 'image'] : ['color', 'image', 'video']);
const typeInput = prompt(`¿Tipo de fondo? (${allowed.join(', ')})`, allowed[0]);
if (!typeInput) return;
const type = typeInput.toLowerCase().trim();
if (!allowed.includes(type)) {
alert(`Este plan permite: ${allowed.join(', ')}`);
return;
}
let val = '';
if (type === 'video') {
val = prompt("URL del Video (MP4):");
} else if (type === 'image') {
val = prompt("URL de la imagen:");
} else {
val = prompt("Color (Hex, ej. #ffffff):");
}
if (!val) return;
state.bg = { type, value: val };
applyBackground();
}
function applyBackground() {
const bgLayer = document.getElementById('bgLayer');
bgLayer.innerHTML = '';
bgLayer.style.background = 'white';
if (!state.bg) return;
if (state.bg.type === 'color') {
bgLayer.style.background = state.bg.value;
} else if (state.bg.type === 'image') {
bgLayer.style.backgroundImage = `url('${state.bg.value}')`;
bgLayer.style.backgroundSize = 'cover';
} else if (state.bg.type === 'video') {
bgLayer.innerHTML = `<video src="${state.bg.value}" autoplay muted loop playsinline style="width:100%; height:100%; object-fit:cover; opacity:0.6;"></video>`;
}
}
// --- PREVIEW SYSTEM ---
function openPreview() {
const preview = document.getElementById('previewBody');
preview.innerHTML = '';
// 1. BG
const prevBg = document.createElement('div');
prevBg.style.position = 'fixed'; prevBg.style.inset = 0; prevBg.style.zIndex = -1;
if (state.bg) {
if (state.bg.type === 'color') prevBg.style.background = state.bg.value;
if (state.bg.type === 'image') { prevBg.style.background = `url(${state.bg.value}) center/cover`; }
if (state.bg.type === 'video') prevBg.innerHTML = `<video src="${state.bg.value}" autoplay loop muted style="width:100%;height:100%;object-fit:cover"></video>`;
}
preview.appendChild(prevBg);
// 2. Blocks
const container = document.createElement('div');
container.style.maxWidth = '900px';
container.style.margin = '0 auto';
container.style.padding = '40px 20px';
container.style.display = 'grid';
container.style.gridTemplateColumns = 'repeat(4, 1fr)';
container.style.gap = '20px';
state.blocks.forEach(b => {
const el = document.createElement('div');
el.style.gridColumn = `span ${b.colSpan}`;
el.style.background = 'rgba(255,255,255,0.9)';
el.style.padding = '20px';
el.style.borderRadius = '12px';
// Content Render
if (b.type === 'heading') el.innerHTML = `<h2 style="color:${b.data.color}">${b.data.text}</h2>`;
else if (b.type === 'image') el.innerHTML = `<img src="${b.data.url}" style="width:100%; border-radius:8px">`;
else if (b.type === 'paragraph') el.innerHTML = `<p>${b.data.text}</p>`;
else el.innerHTML = getPreviewHTML(b); // Fallback
container.appendChild(el);
});
preview.appendChild(container);
document.getElementById('previewModal').classList.add('active');
}
function closePreview() {
document.getElementById('previewModal').classList.remove('active');
}
// --- SERVER SAVE ---
async function savePage() {
document.getElementById('loadingOverlay').style.display = 'flex';
try {
const resp = await fetch('/api/customizer/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
site_id: SITE_CONFIG.site_id,
blocks: state.blocks,
bg: state.bg
})
});
const res = await resp.json();
if (res.success) alert('¡Guardado!');
else alert('Error: ' + res.error);
} catch (e) {
console.error(e);
alert('Error de conexión');
} finally {
document.getElementById('loadingOverlay').style.display = 'none';
}
}
</script>
</body>
</html>