1090 lines
36 KiB
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>
|