Files
gkachele-saas/saas-demo.html

621 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<!--
FILE VERSION HISTORY
Version 1.0 - original
Version 2.0 - 2026-01-29: Mejoras: permitir múltiples bloques, redimensionamiento por columnas (colSpan) y fondo multimedia para plan Premium
-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PageBuilder Pro - Apple Design</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; }
/* AUTENTICACIÓN */
.auth-screen { display: flex; height: 100vh; background: white; }
.auth-screen.hidden { display: none !important; }
.auth-left { flex: 1; background: linear-gradient(135deg, #fa7921 0%, #fbb03b 100%); display: flex; flex-direction: column; justify-content: center; align-items: center; color: white; padding: 60px; text-align: center; }
.auth-left h1 { font-size: 56px; margin-bottom: 20px; font-weight: 700; letter-spacing: -1px; }
.auth-left p { font-size: 15px; opacity: 0.95; margin-bottom: 40px; max-width: 420px; line-height: 1.7; }
.auth-features { list-style: none; margin-top: 30px; }
.auth-features li { margin-bottom: 14px; font-size: 14px; opacity: 0.9; }
.auth-right { flex: 1; display: flex; justify-content: center; align-items: center; padding: 60px; overflow-y: auto; }
.auth-box { width: 100%; max-width: 420px; }
.auth-box h2 { font-size: 28px; margin-bottom: 12px; color: #1d1d1f; font-weight: 700; }
.auth-box p { color: #86868b; margin-bottom: 24px; font-size: 14px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 8px; font-weight: 600; color: #1d1d1f; font-size: 13px; }
.form-group input, .form-group select { width: 100%; padding: 11px 14px; border: 1px solid #d2d2d7; border-radius: 10px; font-family: inherit; font-size: 14px; transition: all 0.2s; background: white; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #fa7921; box-shadow: 0 0 0 2px rgba(250, 121, 33, 0.08); }
.plan-options { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 16px; }
.plan-option { padding: 12px; border: 2px solid #d2d2d7; border-radius: 10px; cursor: pointer; text-align: center; transition: all 0.2s; background: white; }
.plan-option:hover { border-color: #fa7921; background: #fef5ed; }
.plan-option.selected { border-color: #fa7921; background: #fa7921; color: white; }
.plan-option strong { display: block; font-size: 13px; }
.plan-option small { display: block; font-size: 11px; margin-top: 4px; opacity: 0.8; }
.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; width: 100%; }
.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; }
/* LAYOUT PRINCIPAL */
.editor-screen { display: none; height: 100vh; flex-direction: column; background: #f5f5f7; }
.editor-screen.active { display: flex; }
/* 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; }
.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 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; }
.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; }
/* BLOQUES */
.blocks-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.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; }
.canvas-empty i { font-size: 48px; color: #d2d2d7; margin-bottom: 16px; }
.canvas-empty p { font-size: 14px; }
/* PREVIEW MODAL */
.preview-modal { display: none; position: fixed; z-index: 2000; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); align-items: center; justify-content: center; }
.preview-modal.active { display: flex; }
.preview-container { background: white; border-radius: 20px; width: 96%; max-width: 1100px; height: 92vh; box-shadow: 0 20px 60px rgba(0,0,0,0.3); display: flex; flex-direction: column; overflow: hidden; }
.preview-header { background: #f5f5f7; border-bottom: 1px solid #e5e5e7; padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; }
.preview-header h2 { font-size: 15px; font-weight: 600; color: #1d1d1f; }
.preview-close { background: white; border: 1px solid #d2d2d7; width: 30px; height: 30px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #86868b; transition: all 0.2s; font-size: 14px; }
.preview-close:hover { background: #f5f5f7; color: #1d1d1f; }
.preview-content { flex: 1; overflow-y: auto; padding: 36px 30px; background: white; }
.preview-page { max-width: 900px; margin: 0 auto; }
/* EDITOR MODAL */
.modal-overlay { display: none; position: fixed; z-index: 1000; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.3); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); align-items: center; justify-content: center; }
.modal-overlay.active { display: flex; }
.modal-dialog { background: white; border-radius: 16px; width: 92%; max-width: 520px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); display: flex; flex-direction: column; max-height: 88vh; }
.modal-header { background: #f5f5f7; border-bottom: 1px solid #e5e5e7; padding: 20px; display: flex; justify-content: space-between; align-items: center; border-radius: 16px 16px 0 0; }
.modal-header h2 { font-size: 16px; color: #1d1d1f; font-weight: 700; }
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
.modal-footer { background: #f5f5f7; border-top: 1px solid #e5e5e7; padding: 14px 20px; display: flex; gap: 10px; justify-content: flex-end; border-radius: 0 0 16px 16px; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
.field-row.full { grid-template-columns: 1fr; }
.color-picker { width: 100%; height: 36px; border: 1px solid #d2d2d7; border-radius: 8px; cursor: pointer; }
input, select { font-size: 13px; }
/* SCROLL */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #d2d2d7; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a1a1a6; }
</style>
</head>
<body>
<!-- AUTENTICACIÓN -->
<div class="auth-screen" id="authScreen">
<div class="auth-left">
<h1> PageBuilder</h1>
<p>Crea experiencias digitales increíbles sin código</p>
<ul class="auth-features">
<li> Diseño profesional & minimalista</li>
<li> Bloques redimensionables</li>
<li> Layouts en múltiples columnas</li>
<li> Vista previa en tiempo real</li>
<li> Integración de redes sociales</li>
<li> Mapas y geolocalización</li>
</ul>
</div>
<div class="auth-right">
<div class="auth-box">
<h2>Comenzar</h2>
<p>Elige tu rubro y plan</p>
<form id="authForm">
<div class="form-group">
<label>Nombre Completo</label>
<input type="text" id="userName" required placeholder="Tu nombre">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="userEmail" required placeholder="tu@email.com">
</div>
<div class="form-group">
<label>Nombre del Negocio</label>
<input type="text" id="businessName" required placeholder="Mi Empresa">
</div>
<div class="form-group">
<label>Rubro/Industria</label>
<select id="rubicSelect" required>
<option value="">Selecciona tu rubro</option>
<option value="ecommerce">E-commerce</option>
<option value="servicios">Servicios</option>
<option value="agencia">Agencia Digital</option>
<option value="consulting">Consultoría</option>
<option value="educacion">Educación</option>
<option value="tecnologia">Tecnología</option>
<option value="otro">Otro</option>
</select>
</div>
<div class="form-group">
<label>Plan</label>
<div class="plan-options">
<div class="plan-option" data-plan="starter">
<strong>Starter</strong>
<small>10 bloques</small>
</div>
<div class="plan-option" data-plan="pro">
<strong>Pro</strong>
<small>50 bloques</small>
</div>
<div class="plan-option" data-plan="premium">
<strong>Premium</strong>
<small>Ilimitado</small>
</div>
</div>
<input type="hidden" id="planSelect" name="planSelect" value="">
</div>
<button type="submit" class="btn btn-primary">Crear Mi Página</button>
</form>
</div>
</div>
</div>
<!-- EDITOR -->
<div class="editor-screen" id="editorScreen">
<div class="navbar">
<div class="navbar-brand"> PageBuilder</div>
<div class="navbar-info">
<div class="nav-item">Página: <strong id="topPageName"></strong></div>
<div class="nav-item">Rubro: <strong id="topRubric"></strong></div>
<div class="nav-item" id="userNav"></div>
</div>
<div class="nav-actions">
<button class="btn btn-secondary btn-sm" onclick="openPreview()"> Vista Previa</button>
<button class="btn btn-primary btn-sm" onclick="savePage()"> Guardar</button>
<button class="btn-icon" onclick="logout()" title="Salir"></button>
</div>
</div>
<div class="editor-container">
<!-- SIDEBAR -->
<div class="sidebar">
<div class="sidebar-section">
<div class="sidebar-title"> Contenido</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="title"><i class="fas fa-star"></i> Título</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"> Media</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="gallery"><i class="fas fa-images"></i> Galería</div>
<div class="block-item" draggable="true" data-type="video"><i class="fas fa-video"></i> Video</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title"> Interactivo</div>
<div class="block-item" draggable="true" data-type="button"><i class="fas fa-hand-pointer"></i> Botón</div>
<div class="block-item" draggable="true" data-type="form"><i class="fas fa-wpforms"></i> Formulario</div>
<div class="block-item" draggable="true" data-type="map"><i class="fas fa-map"></i> Mapa</div>
<div class="block-item" draggable="true" data-type="social"><i class="fas fa-share"></i> Sociales</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title"> Diseño</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="spacer"><i class="fas fa-square"></i> Espaciado</div>
</div>
<div class="block-counter">
<strong id="blockCount">0</strong> / <strong id="blockLimit">50</strong> bloques
</div>
</div>
<!-- CANVAS -->
<div class="canvas-panel">
<div class="canvas-header">
<h3>Lienzo de Diseño</h3>
<div class="canvas-tools">
<button class="btn btn-secondary btn-sm"> Deshacer</button>
<button class="btn btn-secondary btn-sm"> Rehacer</button>
<button class="btn btn-secondary btn-sm" onclick="setBackground()"> Fondo</button>
</div>
</div>
<div class="canvas-main">
<div class="canvas-wrapper">
<div id="canvasContent" class="blocks-container">
<div class="canvas-empty">
<i class="fas fa-arrow-pointer"></i>
<p>Arrastra bloques desde el panel para empezar</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- PREVIEW MODAL -->
<div class="preview-modal" id="previewModal">
<div class="preview-container">
<div class="preview-header">
<h2>Vista Previa - <strong id="previewTitle"></strong></h2>
<button class="preview-close" onclick="closePreview()"></button>
</div>
<div class="preview-content">
<div class="preview-page" id="previewContent"></div>
</div>
</div>
</div>
<!-- EDITOR MODAL -->
<div class="modal-overlay" id="editorModal">
<div class="modal-dialog">
<div class="modal-header">
<h2 id="modalTitle">Editar Bloque</h2>
<button class="btn-icon" onclick="closeModal()"></button>
</div>
<div class="modal-body" id="modalBody"></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>
let state = {
user: null,
page: null,
blocks: [],
editingIdx: null,
draggedType: null,
planLimits: { starter: 10, pro: 50, premium: 9999 }
};
// AUTENTICACIÓN
let planSelect = null;
document.querySelectorAll('.plan-option').forEach(opt => {
opt.addEventListener('click', function() {
document.querySelectorAll('.plan-option').forEach(o => o.classList.remove('selected'));
this.classList.add('selected');
planSelect = this.dataset.plan; // store selected plan here instead of a missing DOM element
});
});
document.getElementById('authForm').addEventListener('submit', (e) => {
e.preventDefault();
const selectedPlan = document.querySelector('.plan-option.selected');
if (!selectedPlan) {
alert('Selecciona un plan');
return;
}
state.user = {
name: document.getElementById('userName').value,
email: document.getElementById('userEmail').value,
business: document.getElementById('businessName').value,
rubric: document.getElementById('rubicSelect').value,
plan: selectedPlan.dataset.plan
};
state.page = { name: state.user.business };
state.blocks = [];
showEditor();
});
function showEditor() {
document.getElementById('authScreen').classList.add('hidden');
document.getElementById('editorScreen').classList.add('active');
document.getElementById('topPageName').textContent = state.page.name;
document.getElementById('topRubric').textContent = state.user.rubric;
document.getElementById('userNav').innerHTML = `<strong>${state.user.name}</strong> ${state.user.plan.toUpperCase()}`;
updateBlockCounter();
}
function logout() {
document.getElementById('authScreen').classList.remove('hidden');
document.getElementById('editorScreen').classList.remove('active');
state = { user: null, page: null, blocks: [], editingIdx: null, draggedType: null, planLimits: { starter: 10, pro: 50, premium: 9999 } };
document.querySelectorAll('.plan-option').forEach(o => o.classList.remove('selected'));
document.getElementById('authForm').reset();
}
// DRAG & DROP
document.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('block-item')) {
state.draggedType = e.target.dataset.type;
e.dataTransfer.effectAllowed = 'copy';
}
});
document.getElementById('canvasContent').addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
document.getElementById('canvasContent').addEventListener('drop', (e) => {
e.preventDefault();
if (!state.draggedType) return;
const limit = state.planLimits[state.user.plan];
if (state.blocks.length >= limit) {
alert(`Límite: ${limit} bloques para plan ${state.user.plan}`);
return;
}
const block = {
id: Math.random(),
type: state.draggedType,
data: getDefaultData(state.draggedType),
colSpan: 1
};
state.blocks.push(block);
renderBlocks();
updateBlockCounter();
state.draggedType = null;
});
function getDefaultData(type) {
const defaults = {
heading: { text: 'Encabezado', color: '#1d1d1f' },
paragraph: { text: 'Tu texto aquí...' },
title: { text: 'Título Grande', color: '#fa7921' },
image: { url: 'https://via.placeholder.com/500x300?text=Imagen', alt: 'Imagen' },
gallery: { images: ['https://via.placeholder.com/300x300', 'https://via.placeholder.com/300x300', 'https://via.placeholder.com/300x300'], columns: 3 },
video: { url: '', provider: 'youtube' },
button: { text: 'Haz Clic', url: '#', bg: '#fa7921', color: 'white' },
form: { fields: [{name: 'Nombre', type: 'text'}, {name: 'Email', type: 'email'}], submitText: 'Enviar' },
map: { address: 'Madrid, España', lat: 40.4168, lng: -3.7038 },
social: { facebook: '', instagram: '', linkedin: '', twitter: '', youtube: '' },
separator: { color: '#e5e5e7', height: 1 },
spacer: { height: 30 },
list: { items: ['Elemento 1', 'Elemento 2', 'Elemento 3'] }
};
return defaults[type] || {};
}
function renderBlocks() {
const canvas = document.getElementById('canvasContent');
if (state.blocks.length === 0) {
canvas.innerHTML = '<div class="canvas-empty" style="grid-column: 1/-1"><i class="fas fa-arrow-pointer"></i><p>Arrastra bloques desde el panel para empezar</p></div>';
return;
}
canvas.innerHTML = state.blocks.map((b, i) => `
<div class="block-element" data-id="${b.id}" style="grid-column: span ${b.colSpan}">
<div class="block-head">
<div class="block-label">${getName(b.type)}</div>
<div class="block-controls">
<button class="block-ctrl" onclick="editBlock(${i})" title="Editar"></button>
<button class="block-ctrl" onclick="deleteBlock(${i})" title="Eliminar"></button>
</div>
</div>
<div class="block-content">${getPreview(b)}</div>
<div class="resize-handle" onmousedown="startResize(event, ${i})"></div>
</div>
`).join('');
}
function getName(type) {
const names = {
heading: 'Encabezado', paragraph: 'Párrafo', title: 'Título', image: 'Imagen', gallery: 'Galería',
video: 'Video', button: 'Botón', form: 'Formulario', map: 'Mapa', social: 'Sociales',
separator: 'Separador', spacer: 'Espaciado', list: 'Lista'
};
return names[type] || type;
}
function getPreview(b) {
switch(b.type) {
case 'heading': return `<h3 style="color:${b.data.color};margin:0;font-size:16px">${b.data.text}</h3>`;
case 'paragraph': return b.data.text.substring(0, 60) + '...';
case 'title': return `<h2 style="color:${b.data.color};margin:0;font-size:24px">${b.data.text}</h2>`;
case 'image': return `<img src="${b.data.url}" style="width:100%;height:180px;object-fit:cover;border-radius:8px">`;
case 'gallery': return `<div style="display:grid;grid-template-columns:repeat(${b.data.columns},1fr);gap:8px">${b.data.images.map(img=>`<img src="${img}" style="width:100%;height:120px;object-fit:cover;border-radius:6px">`).join('')}</div>`;
case 'button': return `<button style="background:${b.data.bg};color:${b.data.color};padding:10px 20px;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:12px">${b.data.text}</button>`;
case 'form': return `<form style="display:flex;flex-direction:column;gap:8px">${b.data.fields.map(f=>`<input type="${f.type}" placeholder="${f.name}" style="padding:8px;border:1px solid #d2d2d7;border-radius:6px;font-size:12px">`).join('')}<button style="background:#fa7921;color:white;padding:8px;border:none;border-radius:6px;cursor:pointer;font-size:12px">${b.data.submitText}</button></form>`;
case 'map': return `<div style="background:#f5f5f7;padding:16px;border-radius:8px;text-align:center;font-size:12px"><i class="fas fa-map-pin" style="color:#fa7921"></i><p>${b.data.address}</p></div>`;
case 'social': return `<div style="display:flex;gap:8px;justify-content:center">${Object.entries(b.data).map(([k,v])=>v?`<a href="${v}" target="_blank" style="font-size:14px;color:#fa7921"><i class="fab fa-${k}"></i></a>`:'').join('')}</div>`;
case 'separator': return `<hr style="border:none;border-top:${b.data.height}px solid ${b.data.color};margin:0">`;
case 'spacer': return `<div style="height:${b.data.height}px"></div>`;
case 'list': return `<ul style="margin:0;padding-left:16px;font-size:12px">${b.data.items.map(i=>`<li>${i}</li>`).join('')}</ul>`;
default: return `<em>${getName(b.type)}</em>`;
}
}
function editBlock(idx) {
state.editingIdx = idx;
const b = state.blocks[idx];
document.getElementById('modalTitle').textContent = `Editar ${getName(b.type)}`;
let form = '';
switch(b.type) {
case 'heading':
form = `<div class="field-row full"><label>Texto</label><input type="text" id="editText" value="${b.data.text}" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px"></div><div class="field-row full"><label>Color</label><input type="color" id="editColor" value="${b.data.color}" class="color-picker"></div>`;
break;
case 'button':
form = `<div class="field-row full"><label>Texto</label><input type="text" id="editText" value="${b.data.text}" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px"></div><div class="field-row full"><label>URL</label><input type="url" id="editUrl" value="${b.data.url}" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px"></div><div class="field-row"><div><label>Color Fondo</label><input type="color" id="editBg" value="${b.data.bg}" class="color-picker"></div><div><label>Color Texto</label><input type="color" id="editTextColor" value="${b.data.color}" class="color-picker"></div></div>`;
break;
case 'social':
form = `<div style="display:flex;flex-direction:column;gap:12px"><div><label>Facebook</label><input type="url" id="editFb" value="${b.data.facebook}" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px;width:100%"></div><div><label>Instagram</label><input type="url" id="editIg" value="${b.data.instagram}" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px;width:100%"></div><div><label>LinkedIn</label><input type="url" id="editLi" value="${b.data.linkedin}" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px;width:100%"></div><div><label>Twitter</label><input type="url" id="editTw" value="${b.data.twitter}" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px;width:100%"></div></div>`;
break;
case 'map':
form = `<div class="field-row full"><label>Dirección</label><input type="text" id="editAddress" value="${b.data.address}" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px"></div><div class="field-row"><div><label>Latitud</label><input type="number" id="editLat" value="${b.data.lat}" step="0.0001" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px"></div><div><label>Longitud</label><input type="number" id="editLng" value="${b.data.lng}" step="0.0001" style="padding:10px;border:1px solid #d2d2d7;border-radius:8px"></div></div>`;
break;
default: form = '<p>Sin opciones</p>';
}
document.getElementById('modalBody').innerHTML = form;
document.getElementById('editorModal').classList.add('active');
}
function saveBlockChanges() {
const b = state.blocks[state.editingIdx];
switch(b.type) {
case 'heading':
b.data.text = document.getElementById('editText').value;
b.data.color = document.getElementById('editColor').value;
break;
case 'button':
b.data.text = document.getElementById('editText').value;
b.data.url = document.getElementById('editUrl').value;
b.data.bg = document.getElementById('editBg').value;
b.data.color = document.getElementById('editTextColor').value;
break;
case 'social':
b.data.facebook = document.getElementById('editFb').value;
b.data.instagram = document.getElementById('editIg').value;
b.data.linkedin = document.getElementById('editLi').value;
b.data.twitter = document.getElementById('editTw').value;
break;
case 'map':
b.data.address = document.getElementById('editAddress').value;
b.data.lat = parseFloat(document.getElementById('editLat').value);
b.data.lng = parseFloat(document.getElementById('editLng').value);
break;
}
closeModal();
renderBlocks();
}
function closeModal() {
document.getElementById('editorModal').classList.remove('active');
}
function deleteBlock(idx) {
state.blocks.splice(idx, 1);
renderBlocks();
updateBlockCounter();
}
function updateBlockCounter() {
document.getElementById('blockCount').textContent = state.blocks.length;
const userPlan = state.user && state.user.plan ? state.user.plan : null;
document.getElementById('blockLimit').textContent = userPlan ? state.planLimits[userPlan] : '—';
}
function openPreview() {
const preview = document.getElementById('previewContent');
preview.innerHTML = state.blocks.map(b => `<div style="margin-bottom:24px">${getPreview(b)}</div>`).join('');
document.getElementById('previewTitle').textContent = state.page.name;
document.getElementById('previewModal').classList.add('active');
}
function closePreview() {
document.getElementById('previewModal').classList.remove('active');
}
function savePage() {
localStorage.setItem(`page_${state.user.email}`, JSON.stringify({user:state.user, page:state.page, blocks:state.blocks}));
alert(` Página guardada (${state.blocks.length} bloques)`);
}
// Allow premium users to set an image or video background for the canvas wrapper
function setBackground() {
if (!state.user || state.user.plan !== 'premium') {
alert('Fondo disponible solo para plan Premium');
return;
}
const type = prompt('Tipo de fondo: escribe "image" o "video" (sin comillas)', 'image');
if (!type) return;
const url = prompt('Pega la URL del recurso (imagen o vídeo)');
if (!url) return;
const wrapper = document.querySelector('.canvas-wrapper');
// remove existing bg media if any
const existing = document.getElementById('bgMedia');
if (existing) existing.remove();
if (type.toLowerCase() === 'video') {
const div = document.createElement('div');
div.id = 'bgMedia';
div.style.position = 'absolute';
div.style.inset = '0';
div.style.zIndex = '0';
div.style.overflow = 'hidden';
div.innerHTML = `<video src="${url}" autoplay muted loop playsinline style="width:100%;height:100%;object-fit:cover;opacity:0.85"></video>`;
wrapper.style.position = 'relative';
wrapper.prepend(div);
// ensure content sits above
wrapper.querySelectorAll('*:not(#bgMedia)').forEach(el => { if (el !== div) el.style.position = 'relative'; });
} else {
wrapper.style.backgroundImage = `url('${url}')`;
wrapper.style.backgroundSize = 'cover';
wrapper.style.backgroundPosition = 'center';
}
}
function startResize(e, idx) {
const block = document.querySelector(`[data-id="${state.blocks[idx].id}"]`);
if (!block) return;
const startX = e.clientX;
const startSpan = state.blocks[idx].colSpan || 1;
const minSpan = 1;
const maxSpan = 4;
function onMouseMove(ev) {
const delta = ev.clientX - startX;
// one column step per ~260px (matches min width)
const change = Math.round(delta / 260);
let newSpan = Math.min(maxSpan, Math.max(minSpan, startSpan + change));
block.style.gridColumn = `span ${newSpan}`;
}
function onMouseUp(ev) {
const delta = ev.clientX - startX;
const change = Math.round(delta / 260);
let finalSpan = Math.min(maxSpan, Math.max(minSpan, startSpan + change));
state.blocks[idx].colSpan = finalSpan;
renderBlocks(); // re-render to persist layout and controls
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
</script>
</body>
</html>