Files
gkachele-saas/demo/templates/customizer.html
2026-02-12 19:05:58 +01:00

1663 lines
66 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personalizar - {{ site_name or 'Sitio' }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<style>
/*
* GKACHELE™ CUSTOMIZER STYLE CLONE
* Colors:
* - Admin Bar: #1d2327
* - Sidebar BG: #f0f0f1
* - Active Item: #fff
* - Primary Button: #2271b1 (Hover: #135e96)
* - Text: #2c3338
*/
:root {
--gk-admin-bar: #1d2327;
--gk-sidebar-bg: #f0f0f1;
--gk-white: #ffffff;
--gk-border: #dcdcde;
--gk-text: #2c3338;
--gk-blue: #2271b1;
--gk-blue-hover: #135e96;
--gk-red: #d63638;
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* GK STYLE CLONE - Premium UI */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--gk-text);
background: #f1f1f1;
}
.gk-full-overlay {
display: flex;
height: 100vh;
}
.gk-sidebar {
width: 300px;
background: var(--gk-sidebar-bg);
border-right: 1px solid var(--gk-border);
display: flex;
flex-direction: column;
z-index: 10;
}
.gk-header {
height: 46px;
background: var(--gk-admin-bar);
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
}
.gk-logo {
font-weight: 800;
letter-spacing: -0.5px;
color: var(--gk-blue);
}
/* ACTIONS BAR (Left Side Controls) */
.gk-full-overlay {
display: flex;
flex: 1;
height: 100%;
}
.gk-full-overlay-sidebar {
width: 300px;
min-width: 300px;
background: var(--gk-sidebar-bg);
border-right: 1px solid var(--gk-border);
display: flex;
flex-direction: column;
position: relative;
z-index: 10;
}
/* HEADER SIDEBAR */
.customize-controls-close {
height: 45px;
display: flex;
align-items: center;
padding: 0 15px;
background: #fff;
border-bottom: 1px solid var(--wp-border);
justify-content: space-between;
}
.customize-controls-close h2 {
font-size: 14px;
font-weight: 600;
margin: 0;
}
.customize-controls-close .close-btn {
color: #787c82;
text-decoration: none;
font-size: 16px;
}
.customize-controls-close .close-btn:hover {
color: var(--wp-red);
}
/* CONTROLS CONTAINER */
.customize-pane-child {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.accordion-section {
border-bottom: 1px solid var(--wp-border);
background: #fff;
}
.accordion-section-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
cursor: pointer;
background: #fff;
transition: background var(--transition);
font-weight: 600;
font-size: 14px;
color: #1d2327;
border-left: 4px solid transparent;
}
.accordion-section-title:hover {
background: #f6f7f7;
color: var(--wp-blue);
}
.accordion-section-title.open {
border-left-color: var(--wp-blue);
background: #f6f7f7;
}
.accordion-section-title:after {
content: '\f078';
/* FontAwesome chevron-down */
font-family: 'Font Awesome 6 Free';
font-weight: 900;
color: #a7aaad;
font-size: 12px;
transition: transform 0.3s;
}
.accordion-section-title.open:after {
transform: rotate(180deg);
}
.accordion-section-content {
display: none;
padding: 20px;
background: #f6f7f7;
border-bottom: 1px solid var(--wp-border);
}
.accordion-section-content.visible {
display: block;
}
/* INPUTS */
.customize-control {
margin-bottom: 15px;
}
.customize-control-title {
display: block;
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
color: #1d2327;
}
.customize-control-description {
display: block;
margin-top: 2px;
margin-bottom: 8px;
color: #646970;
font-style: italic;
font-size: 12px;
}
input[type="text"],
input[type="url"],
input[type="email"],
input[type="file"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="tel"],
textarea,
select {
width: 100%;
padding: 0 8px;
line-height: 2;
min-height: 30px;
box-shadow: 0 0 0 transparent;
border-radius: 4px;
border: 1px solid #8c8f94;
background-color: #fff;
color: #2c3338;
transition: 0.2s;
font-size: 13px;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--wp-blue);
box-shadow: 0 0 0 1px var(--wp-blue);
outline: none;
}
textarea {
line-height: 1.5;
padding: 6px 8px;
min-height: 80px;
}
/* BUTTONS */
.button {
display: inline-block;
text-decoration: none;
font-size: 13px;
line-height: 2.15384615;
min-height: 30px;
margin: 0;
padding: 0 10px;
cursor: pointer;
border-width: 1px;
border-style: solid;
-webkit-appearance: none;
appearance: none;
border-radius: 3px;
white-space: nowrap;
box-sizing: border-box;
background: #f6f7f7;
color: #2271b1;
border-color: #2271b1;
transition: 0.2s;
font-weight: 500;
}
.button-primary {
background: var(--wp-blue);
border-color: var(--wp-blue);
color: #fff;
}
.button-primary:hover {
background: var(--wp-blue-hover);
border-color: var(--wp-blue-hover);
color: #fff;
}
.button-secondary {
color: #2271b1;
border-color: #2271b1;
background: #f6f7f7;
}
.button-secondary:hover {
background: #f0f0f1;
border-color: #0a4b78;
color: #0a4b78;
}
/* FOOTER ACTIONS */
.gk-full-overlay-footer {
height: 46px;
background: #fff;
border-top: 1px solid var(--gk-border);
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 12px;
gap: 10px;
}
.spinner {
display: none;
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* PREVIEW AREA */
.gk-full-overlay-main {
flex: 1;
position: relative;
background: #f0f0f1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.preview-iframe-wrapper {
width: 100%;
height: 100%;
position: relative;
}
iframe {
width: 100%;
height: 100%;
border: 0;
}
/* RESPONSIVE TOGGLES (Bottom left) */
.devices-wrapper {
position: absolute;
bottom: 12px;
left: 20px;
display: flex;
gap: 15px;
z-index: 20;
}
.device-btn {
background: none;
border: none;
color: #a7aaad;
cursor: pointer;
font-size: 16px;
padding: 0;
transition: color 0.2s;
}
.device-btn:hover,
.device-btn.active {
color: #1d2327;
}
/* COLOR PICKER */
.color-picker-wrapper {
display: flex;
align-items: center;
gap: 10px;
}
.color-picker-wrapper input[type="color"] {
width: 40px;
height: 30px;
padding: 0 2px;
border: 1px solid #8c8f94;
cursor: pointer;
}
/* BLOCKS LIST */
.blocks-list-item {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
margin-bottom: 8px;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
/* Future DnD */
}
.blocks-list-item:hover {
border-color: #8c8f94;
}
.block-title {
font-weight: 500;
color: #1d2327;
}
.block-actions {
display: flex;
gap: 5px;
}
.notification-toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(40, 40, 40, 0.9);
color: white;
padding: 20px 40px;
border-radius: 5px;
font-size: 16px;
z-index: 9999;
display: none;
}
/* MODAL PARA GESTIONAR PLATOS */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
.modal-content {
background: #fff;
border-radius: 8px;
padding: 30px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--wp-border);
}
.modal-header h3 {
margin: 0;
font-size: 18px;
color: var(--gk-text);
}
.modal-close {
background: none;
border: none;
font-size: 24px;
color: #787c82;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
color: var(--wp-red);
}
.menu-item-form {
background: #f6f7f7;
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
}
.menu-item-form input,
.menu-item-form textarea {
margin-bottom: 10px;
}
.menu-item-list {
margin-top: 20px;
}
.menu-item-card {
background: #fff;
border: 1px solid var(--wp-border);
border-radius: 4px;
padding: 15px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.menu-item-info h4 {
margin: 0 0 5px 0;
font-size: 16px;
color: var(--gk-text);
}
.menu-item-info p {
margin: 0;
font-size: 13px;
color: #646970;
}
.menu-item-actions {
display: flex;
gap: 10px;
}
/* MODAL INLINE EDIT */
.modal-body {
max-width: 500px;
}
/* ESTILOS WORDPRESS PARA ELEMENTOS EDITABLES EN PREVIEW */
.gk-editable {
position: relative;
cursor: pointer;
transition: all 0.2s;
}
.gk-editable:hover {
outline: 2px dashed #2271b1;
outline-offset: 2px;
background: rgba(34, 113, 177, 0.05);
}
.gk-editable::before {
content: '✏️';
position: absolute;
top: -8px;
right: -8px;
background: #2271b1;
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 3px;
opacity: 0;
transition: opacity 0.2s;
z-index: 1000;
pointer-events: none;
}
.gk-editable:hover::before {
opacity: 1;
}
</style>
</head>
<body>
<div class="gk-full-overlay">
<!-- SIDEBAR CONTROLS -->
<div class="gk-sidebar">
<div class="gk-header">
<div class="gk-logo">GKACHELE™</div>
<div style="font-size: 12px; font-weight: 500;">{{ user_plan|upper }}</div>
<a href="/dashboard" class="close-btn" style="color: #fff; text-decoration: none;"><i
class="fa-solid fa-xmark"></i></a>
</div>
<div class="customize-pane-child">
<!-- TEMPLATE SELECTOR -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-paintbrush" style="margin-right:8px; color:var(--gk-blue);"></i>
Plantilla & Plan</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<span class="customize-control-title">Template Actual</span>
<select id="theme_selector" onchange="confirmChangeTheme(this)">
{% for theme_id, theme_info in available_themes.items() %}
<option value="{{ theme_id }}" {% if theme==theme_id %}selected{% endif %}>
{{ theme_info.name }} (Plan {{ theme_info.plan|upper }})
</option>
{% endfor %}
</select>
<span class="customize-control-description">Estás en el plan <strong>{{ user_plan|upper
}}</strong>.</span>
</div>
</div>
</div>
<!-- IDENTIDAD DEL SITIO -->
<!-- IDENTIDAD DEL SITIO -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-id-card" style="margin-right:8px; color:#555;"></i> Identidad del
Sitio</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<span class="customize-control-title">Nombre del Sitio</span>
<input type="text" id="site_name" value="{{ content.site_name or '' }}"
oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">Título Hero</span>
<input type="text" id="hero_title" value="{{ content.hero_title or '' }}"
oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">Descripción Corta</span>
<textarea id="hero_description"
oninput="updatePreview()">{{ content.hero_description or '' }}</textarea>
</div>
<div class="customize-control">
<span class="customize-control-title">Logo URL</span>
<input type="url" id="media_logo" value="{{ content.media.logo if content.media else '' }}"
oninput="updatePreview()">
</div>
</div>
</div>
<!-- COLORES -->
<!-- COLORES -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-palette" style="margin-right:8px; color:#555;"></i> Colores</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<span class="customize-control-title">Color Primario</span>
<div class="color-picker-wrapper">
<input type="color" id="color_primary"
value="{{ content.colors.primary if content.colors else '#2271b1' }}"
oninput="syncColor('color_primary')">
<input type="text" id="val_color_primary"
value="{{ content.colors.primary if content.colors else '#2271b1' }}"
oninput="syncTextLimit('color_primary')">
</div>
</div>
<div class="customize-control">
<span class="customize-control-title">Color Secundario</span>
<div class="color-picker-wrapper">
<input type="color" id="color_secondary"
value="{{ content.colors.secondary if content.colors else '#f0f0f1' }}"
oninput="syncColor('color_secondary')">
<input type="text" id="val_color_secondary"
value="{{ content.colors.secondary if content.colors else '#f0f0f1' }}"
oninput="syncTextLimit('color_secondary')">
</div>
</div>
<div class="customize-control">
<span class="customize-control-title">Color Texto</span>
<div class="color-picker-wrapper">
<input type="color" id="color_text"
value="{{ content.colors.text if content.colors else '#2c3338' }}"
oninput="syncColor('color_text')">
<input type="text" id="val_color_text"
value="{{ content.colors.text if content.colors else '#2c3338' }}"
oninput="syncTextLimit('color_text')">
</div>
</div>
</div>
</div>
<!-- DATOS DE CONTACTO (WhatsApp, Mapas) -->
<!-- DATOS DE CONTACTO (WhatsApp, Mapas) -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-location-dot" style="margin-right:8px; color:#555;"></i> Contacto &
Ubicación</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<span class="customize-control-title">Dirección</span>
<input type="text" id="contacto_direccion"
value="{{ content.direccion if content.direccion else '' }}" oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">Teléfono</span>
<input type="tel" id="contacto_telefono"
value="{{ content.telefono if content.telefono else '' }}" oninput="updatePreview()">
</div>
<!-- NUEVO: WhatsApp -->
<div class="customize-control">
<span class="customize-control-title">WhatsApp (Solo números)</span>
<input type="tel" id="redes_whatsapp" placeholder="34600000000"
value="{{ content.redes_sociales.whatsapp if content.redes_sociales else '' }}"
oninput="updatePreview()">
<span class="customize-control-description">Añadirá un botón flotante si se rellena.</span>
</div>
<div class="customize-control">
<span class="customize-control-title">Email</span>
<input type="email" id="contacto_email" value="{{ content.email if content.email else '' }}"
oninput="updatePreview()">
</div>
</div>
</div>
<!-- REDES SOCIALES -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-share-nodes" style="margin-right:8px; color:#555;"></i> Redes
Sociales</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<span class="customize-control-title">Instagram URL</span>
<input type="url" id="redes_instagram"
value="{{ content.redes_sociales.instagram if content.redes_sociales else '' }}"
oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">Facebook URL</span>
<input type="url" id="redes_facebook"
value="{{ content.redes_sociales.facebook if content.redes_sociales else '' }}"
oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">TikTok URL</span>
<input type="url" id="redes_tiktok"
value="{{ content.redes_sociales.tiktok if content.redes_sociales else '' }}"
oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">LinkedIn URL</span>
<input type="url" id="redes_linkedin"
value="{{ content.redes_sociales.linkedin if content.redes_sociales else '' }}"
oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">YouTube URL</span>
<input type="url" id="redes_youtube"
value="{{ content.redes_sociales.youtube if content.redes_sociales else '' }}"
oninput="updatePreview()">
</div>
</div>
</div>
<!-- HORARIOS -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-clock" style="margin-right:8px; color:#555;"></i> Horarios de
Atención</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<span class="customize-control-title">Lunes - Viernes</span>
<input type="text" id="horarios_lunes_viernes"
value="{{ content.horarios.lunes_viernes if content.horarios else '12:00 PM - 10:00 PM' }}"
placeholder="12:00 PM - 10:00 PM" oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">Sábados</span>
<input type="text" id="horarios_sabados"
value="{{ content.horarios.sabados if content.horarios else '12:00 PM - 11:00 PM' }}"
placeholder="12:00 PM - 11:00 PM" oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">Domingos</span>
<input type="text" id="horarios_domingos"
value="{{ content.horarios.domingos if content.horarios else '12:00 PM - 9:00 PM' }}"
placeholder="12:00 PM - 9:00 PM" oninput="updatePreview()">
</div>
</div>
</div>
<!-- ESPECIALIDAD CULINARIA -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-star" style="margin-right:8px; color:#555;"></i> Especialidad
Culinaria</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<span class="customize-control-title">Título</span>
<input type="text" id="especialidad_titulo"
value="{{ content.especialidad_culinaria.titulo if content.especialidad_culinaria else 'Nuestra Especialidad' }}"
placeholder="Nuestra Especialidad" oninput="updatePreview()">
</div>
<div class="customize-control">
<span class="customize-control-title">Descripción</span>
<textarea id="especialidad_descripcion"
placeholder="Cada plato es una obra de arte culinaria..."
oninput="updatePreview()">{{ content.especialidad_culinaria.descripcion if content.especialidad_culinaria else 'Cada plato es una obra de arte culinaria, preparado con ingredientes frescos y técnicas tradicionales que honran la autenticidad de nuestros sabores.' }}</textarea>
</div>
<div class="customize-control">
<span class="customize-control-title">URL de Imagen</span>
<input type="url" id="especialidad_imagen"
value="{{ content.especialidad_culinaria.imagen if content.especialidad_culinaria else '' }}"
placeholder="https://..." oninput="updatePreview()">
</div>
</div>
</div>
<!-- CAPACIDAD -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-users" style="margin-right:8px; color:#555;"></i> Capacidad</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<span class="customize-control-title">Capacidad del Restaurante</span>
<input type="number" id="capacidad"
value="{{ content.capacidad if content.capacidad else '50' }}" placeholder="50" min="1"
oninput="updatePreview()">
<span class="customize-control-description">Número de personas que puede albergar el
restaurante.</span>
</div>
</div>
</div>
<!-- GESTIÓN DE BLOQUES -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-cubes" style="margin-right:8px; color:#555;"></i> Bloques de
Contenido</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<button class="button button-secondary" style="width: 100%"
onclick="document.getElementById('add-block-panel').style.display='block'">+
Añadir
Bloque</button>
</div>
<div id="blocks_list_container">
<!-- JS populated list -->
</div>
<!-- Mini Panel para añadir (toggle) -->
<div id="add-block-panel"
style="display:none; border-top: 1px solid #ddd; margin-top: 10px; padding-top: 10px;">
<span class="customize-control-title">Nuevo Bloque</span>
<select id="block_type_selector" style="margin-bottom: 5px;">
<option value="texto">Texto</option>
<option value="imagen">Imagen</option>
<option value="video">Video</option>
<option value="mapa">Mapa</option>
</select>
<button class="button button-primary" onclick="addNewBlockClientSide()">Insertar</button>
<button class="button button-link"
style="color:red; border:none; background:none; cursor:pointer; font-size:12px;"
onclick="document.getElementById('add-block-panel').style.display='none'">Cancelar</button>
</div>
</div>
</div>
<!-- MENÚ Y PLATOS -->
<div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-utensils" style="margin-right:8px; color:#555;"></i> Menú
Restaurante</span>
</div>
<div class="accordion-section-content">
<div class="customize-control">
<span class="customize-control-title">Enlace PDF Menú</span>
<input type="url" id="menu_url" value="{{ content.menu_url if content.menu_url else '' }}"
oninput="updatePreview()">
</div>
<div class="customize-control">
<button class="button button-secondary" onclick="gestionarPlatosMenu()">Gestionar
Platos</button>
</div>
</div>
</div>
</div>
<!-- FOOTER: PUBLISH ACTIONS -->
<div class="wp-full-overlay-footer">
<div class="spinner" id="saving-spinner"></div>
<!-- Separate Save Logic -->
<button type="button" class="button button-secondary"
onclick="window.location.reload()">Descartar</button>
<button type="button" class="button button-primary" id="save-btn"
onclick="publishChanges()">Publicar</button>
</div>
</div>
<!-- PREVIEW AREA -->
<div class="wp-full-overlay-main">
<div class="preview-iframe-wrapper">
<iframe id="preview-iframe" src="/api/customizer/preview-frame/{{ site_id }}"></iframe>
</div>
<!-- DEVICE TOGGLES -->
<div class="devices-wrapper">
<button class="device-btn active" onclick="setDevice('desktop')" title="Escritorio"><i
class="fa-solid fa-desktop"></i></button>
<button class="device-btn" onclick="setDevice('tablet')" title="Tablet"><i
class="fa-solid fa-tablet-screen-button"></i></button>
<button class="device-btn" onclick="setDevice('mobile')" title="Móvil"><i
class="fa-solid fa-mobile-screen-button"></i></button>
</div>
</div>
</div>
<!-- NOTIFICATION TOAST -->
<div id="notification" class="notification-toast"></div>
<!-- MODAL GESTIÓN PLATOS -->
<div id="menu-modal" class="modal-overlay" onclick="if(event.target === this) closeMenuModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h3><i class="fa-solid fa-utensils" style="margin-right:8px;"></i>Gestionar Platos del Menú</h3>
<button class="modal-close" onclick="closeMenuModal()">&times;</button>
</div>
<div class="menu-item-form">
<span class="customize-control-title">Añadir Nuevo Plato</span>
<input type="text" id="new-plato-nombre" placeholder="Nombre del plato" style="margin-bottom: 8px;">
<textarea id="new-plato-descripcion" placeholder="Descripción del plato"
style="margin-bottom: 8px; min-height: 60px;"></textarea>
<input type="text" id="new-plato-precio" placeholder="Precio (ej: 25.00)" style="margin-bottom: 8px;">
<button class="button button-primary" onclick="addMenuPlato()">Añadir Plato</button>
</div>
<div class="menu-item-list" id="menu-items-list">
<!-- JS populated -->
</div>
</div>
</div>
<!-- MODAL EDICIÓN BLOQUES -->
<div id="block-edit-modal" class="modal-overlay" onclick="if(event.target === this) closeBlockEditModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h3><i class="fa-solid fa-pen-to-square" style="margin-right:8px;"></i>Editar Bloque</h3>
<button class="modal-close" onclick="closeBlockEditModal()">&times;</button>
</div>
<div class="modal-body" id="block-edit-form" style="padding: 20px;">
<!-- JS populated -->
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end; padding: 0 20px 20px;">
<button class="button button-secondary" onclick="closeBlockEditModal()">Cancelar</button>
<button class="button button-primary" onclick="saveBlockEdit()">Guardar Cambios</button>
</div>
</div>
</div>
<script>
const siteId = {{ site_id }};
let currentTheme = '{{ theme }}';
// ==========================================
// UI INTERACTION
// ==========================================
function toggleSection(element) {
// Close others (Accordion behavior)
/*
document.querySelectorAll('.accordion-section-title').forEach(el => {
if(el !== element) {
el.classList.remove('open');
el.nextElementSibling.classList.remove('visible');
}
});
*/
// Toggle current
element.classList.toggle('open');
element.nextElementSibling.classList.toggle('visible');
}
function setDevice(device) {
const wrapper = document.querySelector('.preview-iframe-wrapper');
const btns = document.querySelectorAll('.device-btn');
btns.forEach(b => b.classList.remove('active'));
// Find button by icon class logic or index (simple way here)
if (device === 'desktop') btns[0].classList.add('active');
if (device === 'tablet') btns[1].classList.add('active');
if (device === 'mobile') btns[2].classList.add('active');
if (device === 'desktop') {
wrapper.style.width = '100%';
} else if (device === 'tablet') {
wrapper.style.width = '768px';
} else if (device === 'mobile') {
wrapper.style.width = '375px';
}
}
function syncColor(id) {
const colorVal = document.getElementById(id).value;
document.getElementById('val_' + id).value = colorVal;
updatePreview();
}
function syncTextLimit(id) {
const textVal = document.getElementById('val_' + id).value;
document.getElementById(id).value = textVal;
updatePreview();
}
function showNotification(msg) {
const n = document.getElementById('notification');
n.innerText = msg;
n.style.display = 'block';
setTimeout(() => n.style.display = 'none', 2000);
}
// ==========================================
// DATA & PREVIEW LOGIC
// ==========================================
function getFormData() {
const direccion = document.getElementById('contacto_direccion').value;
let mapa_url = '';
// Generar mapa automáticamente desde dirección si no se ha pegado un iframe manualmente
if (direccion) {
// Limpiar dirección de caracteres extraños y codificarla
const cleanAddress = direccion.trim().replace(/\s+/g, ' ');
const encodedAddress = encodeURIComponent(cleanAddress);
mapa_url = `https://www.google.com/maps?q=${encodedAddress}&output=embed`;
}
return {
site_name: document.getElementById('site_name').value,
hero_title: document.getElementById('hero_title').value,
hero_description: document.getElementById('hero_description').value,
direccion: direccion,
telefono: document.getElementById('contacto_telefono').value,
email: document.getElementById('contacto_email').value,
capacidad: document.getElementById('capacidad').value || '50',
media: {
logo: document.getElementById('media_logo').value
},
colors: {
primary: document.getElementById('color_primary').value,
secondary: document.getElementById('color_secondary').value,
text: document.getElementById('color_text').value
},
redes_sociales: {
instagram: document.getElementById('redes_instagram').value,
facebook: document.getElementById('redes_facebook').value,
tiktok: document.getElementById('redes_tiktok').value,
linkedin: document.getElementById('redes_linkedin').value,
youtube: document.getElementById('redes_youtube').value,
whatsapp: document.getElementById('redes_whatsapp').value
},
horarios: {
lunes_viernes: document.getElementById('horarios_lunes_viernes').value,
sabados: document.getElementById('horarios_sabados').value,
domingos: document.getElementById('horarios_domingos').value
},
especialidad_culinaria: {
titulo: document.getElementById('especialidad_titulo').value,
descripcion: document.getElementById('especialidad_descripcion').value,
imagen: document.getElementById('especialidad_imagen').value
},
mapa_url: mapa_url,
blocks: window.currentBlocks || [],
menu_url: document.getElementById('menu_url').value,
menu_items: window.currentMenuItems || {}
};
}
function updatePreview() {
const data = getFormData();
const iframe = document.getElementById('preview-iframe');
// Send data to iframe (postMessage) without saving to DB
iframe.contentWindow.postMessage({
type: 'update-content',
content: data
}, '*');
// Change Save button state to indicate unsaved changes (Visual cue)
const saveBtn = document.getElementById('save-btn');
saveBtn.innerText = 'Publicar *';
}
function publishChanges() {
const saveBtn = document.getElementById('save-btn');
const spinner = document.getElementById('saving-spinner');
saveBtn.disabled = true;
saveBtn.innerText = 'Publicando...';
spinner.style.display = 'block';
const data = {
site_id: siteId,
content: getFormData(),
theme: document.getElementById('theme_selector').value,
blocks: window.currentBlocks // For explicit save
};
fetch('/api/customizer/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => {
spinner.style.display = 'none';
saveBtn.disabled = false;
if (res.success) {
saveBtn.innerText = 'Publicado';
showNotification('✅ ¡Cambios publicados correctamente!');
setTimeout(() => saveBtn.innerText = 'Publicar', 2000);
} else {
saveBtn.innerText = 'Error';
alert('Error al guardar: ' + res.error);
}
})
.catch(err => {
spinner.style.display = 'none';
saveBtn.disabled = false;
alert('Error de conexión');
});
}
function confirmChangeTheme(select) {
if (confirm('¿Cambiar de tema? Se perderán los cambios no guardados y se restablecerán los colores.')) {
// We actually verify this via the save/reload mechanic usually,
// but here we might want to just reload the page with new theme or save immediately.
// For simplicity: Save just the theme change and reload.
publishChanges();
setTimeout(() => window.location.reload(), 1000);
} else {
select.value = currentTheme; // Revert
}
}
// ==========================================
// BLOCKS LOGIC (Client Side First)
// ==========================================
window.currentBlocks = []; // Local source of truth
function loadBlocksList() {
// Initial Load from Server
fetch('/api/customizer/get-blocks/' + siteId)
.then(r => r.json())
.then(blocks => {
window.currentBlocks = blocks;
renderBlocksList();
});
}
function renderBlocksList() {
const list = document.getElementById('blocks_list_container');
list.innerHTML = '';
if (window.currentBlocks.length === 0) {
list.innerHTML = '<div style="text-align: center; color: #999; padding: 10px; font-style: italic;">No hay bloques extra.</div>';
return;
}
window.currentBlocks.sort((a, b) => a.order - b.order).forEach((block, index) => {
const item = document.createElement('div');
item.className = 'blocks-list-item';
item.dataset.id = block.id; // For SortableJS
let icon = 'fa-cube';
if (block.type === 'video') icon = 'fa-video';
if (block.type === 'imagen') icon = 'fa-image';
if (block.type === 'texto') icon = 'fa-font';
if (block.type === 'mapa') icon = 'fa-map-location-dot';
item.innerHTML = `
<div class="block-title"><i class="fa-solid ${icon}" style="margin-right:8px; color:#aaa;"></i> ${block.type.toUpperCase()}</div>
<div class="block-actions">
<button class="button button-small" style="color:#d63638; border:none; background:none;" onclick="removeBlockClientSide('${block.id}')" title="Eliminar"><i class="fa-solid fa-trash"></i></button>
</div>
`;
list.appendChild(item);
});
// Re-init Sortable if list re-rendered
initSortable();
}
function addNewBlockClientSide() {
const type = document.getElementById('block_type_selector').value;
if (!type) return;
const tempId = 'new_' + Date.now();
const newBlock = {
id: tempId,
type: type,
content: {},
order: window.currentBlocks.length
};
// Add to local state
window.currentBlocks.push(newBlock);
// Render UI
renderBlocksList();
document.getElementById('add-block-panel').style.display = 'none';
// Update Preview (Send 'add-block' message)
const iframe = document.getElementById('preview-iframe');
iframe.contentWindow.postMessage({
type: 'add-block',
block: newBlock
}, '*');
// Mark as dirty
document.getElementById('save-btn').innerText = 'Publicar *';
showNotification('Bloque añadido (Draft)');
}
function removeBlockClientSide(blockId) {
if (!confirm('¿Eliminar este bloque?')) return;
// Remove from local state
window.currentBlocks = window.currentBlocks.filter(b => b.id !== blockId);
// Render UI
renderBlocksList();
// Update Preview
const iframe = document.getElementById('preview-iframe');
iframe.contentWindow.postMessage({
type: 'remove-block',
block_id: blockId
}, '*');
// Mark as dirty
document.getElementById('save-btn').innerText = 'Publicar *';
}
function initSortable() {
const el = document.getElementById('blocks_list_container');
if (!el) return;
new Sortable(el, {
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: function (evt) {
// Reorder window.currentBlocks based on DOM order
const newOrderIds = Array.from(el.children).map(child => child.dataset.id);
// Sort internal array based on new ID order
const reorderedBlocks = [];
newOrderIds.forEach(id => {
const block = window.currentBlocks.find(b => b.id === id);
if (block) reorderedBlocks.push(block);
});
window.currentBlocks = reorderedBlocks;
// Update indices
window.currentBlocks.forEach((b, i) => b.order = i);
// Send reorder message to iframe
const iframe = document.getElementById('preview-iframe');
iframe.contentWindow.postMessage({
type: 'reorder-blocks',
blockIds: newOrderIds
}, '*');
document.getElementById('save-btn').innerText = 'Publicar *';
}
});
}
// ==========================================
// MENÚ PLATOS MANAGEMENT
// ==========================================
window.currentMenuItems = {};
function loadMenuItems() {
// Cargar desde el objeto inicial renderizado por Jinja2
const initialContent = {{ content| tojson | safe
}};
if (initialContent && initialContent.menu_items && Object.keys(initialContent.menu_items).length > 0) {
window.currentMenuItems = initialContent.menu_items;
} else {
// Platos por defecto si no hay nada guardado
window.currentMenuItems = {
'1': { nombre: 'Plato Especial 1', descripcion: 'Descripción del plato con ingredientes frescos.', precio: '25.00' },
'2': { nombre: 'Plato Especial 2', descripcion: 'Descripción del plato con sabores únicos.', precio: '28.00' },
'3': { nombre: 'Plato Especial 3', descripcion: 'Descripción del plato tradicional.', precio: '30.00' }
};
}
renderMenuItems();
}
function renderMenuItems() {
const list = document.getElementById('menu-items-list');
list.innerHTML = '';
if (Object.keys(window.currentMenuItems).length === 0) {
list.innerHTML = '<p style="text-align: center; color: #999; padding: 20px;">No hay platos. Añade el primero arriba.</p>';
return;
}
Object.keys(window.currentMenuItems).forEach(itemId => {
const item = window.currentMenuItems[itemId];
const card = document.createElement('div');
card.className = 'menu-item-card';
card.innerHTML = `
<div class="menu-item-info">
<h4>${item.nombre || 'Sin nombre'}</h4>
<p>${item.descripcion || 'Sin descripción'}</p>
<p><strong>Precio: $${item.precio || '0.00'}</strong></p>
</div>
<div class="menu-item-actions">
<button class="button button-secondary" onclick="editMenuPlato('${itemId}')">Editar</button>
<button class="button" style="background: #d63638; color: white; border-color: #d63638;" onclick="deleteMenuPlato('${itemId}')">Eliminar</button>
</div>
`;
list.appendChild(card);
});
}
function addMenuPlato() {
const nombre = document.getElementById('new-plato-nombre').value.trim();
const descripcion = document.getElementById('new-plato-descripcion').value.trim();
const precio = document.getElementById('new-plato-precio').value.trim();
if (!nombre) {
alert('Por favor, ingresa un nombre para el plato.');
return;
}
const newId = Date.now().toString();
window.currentMenuItems[newId] = {
nombre: nombre,
descripcion: descripcion || 'Descripción del plato.',
precio: precio || '0.00'
};
// Limpiar formulario
document.getElementById('new-plato-nombre').value = '';
document.getElementById('new-plato-descripcion').value = '';
document.getElementById('new-plato-precio').value = '';
renderMenuItems();
updatePreview();
document.getElementById('save-btn').innerText = 'Publicar *';
}
function editMenuPlato(itemId) {
const item = window.currentMenuItems[itemId];
if (!item) return;
// Llenar formulario con datos existentes
document.getElementById('new-plato-nombre').value = item.nombre || '';
document.getElementById('new-plato-descripcion').value = item.descripcion || '';
document.getElementById('new-plato-precio').value = item.precio || '';
// Eliminar el plato actual (se añadirá de nuevo con los cambios)
deleteMenuPlato(itemId, false);
}
function deleteMenuPlato(itemId, confirmDelete = true) {
if (confirmDelete && !confirm('¿Eliminar este plato?')) return;
delete window.currentMenuItems[itemId];
renderMenuItems();
updatePreview();
document.getElementById('save-btn').innerText = 'Publicar *';
}
function gestionarPlatosMenu() {
loadMenuItems();
document.getElementById('menu-modal').classList.add('active');
}
function closeMenuModal() {
document.getElementById('menu-modal').classList.remove('active');
}
// ==========================================
// WORDPRESS-STYLE INLINE EDITING
// ==========================================
// Escuchar mensajes del iframe (click to edit)
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'gk-edit-field') {
handleInlineEdit(e.data.field, e.data.value, e.data.type_field);
} else if (e.data && e.data.type === 'gk-edit-block') {
handleEditBlock(e.data.block_id);
}
});
function handleEditBlock(blockId) {
const block = window.currentBlocks.find(b => b.id === blockId);
if (!block) return;
const form = document.getElementById('block-edit-form');
form.innerHTML = '';
window.currentEditingBlockId = blockId;
if (block.type === 'texto') {
form.innerHTML = `
<div class="customize-control">
<span class="customize-control-title">Título</span>
<input type="text" id="edit-block-titulo" value="${block.content.titulo || ''}" style="width:100%; margin-bottom:15px;">
</div>
<div class="customize-control">
<span class="customize-control-title">Contenido</span>
<textarea id="edit-block-contenido" style="width:100%; min-height:100px;">${block.content.contenido || ''}</textarea>
</div>
`;
} else if (block.type === 'imagen' || block.type === 'video' || block.type === 'mapa') {
form.innerHTML = `
<div class="customize-control">
<span class="customize-control-title">URL del Recurso</span>
<input type="text" id="edit-block-url" value="${block.content.url || ''}" style="width:100%; margin-bottom:15px;">
</div>
`;
}
document.getElementById('block-edit-modal').classList.add('active');
}
function closeBlockEditModal() {
document.getElementById('block-edit-modal').classList.remove('active');
window.currentEditingBlockId = null;
}
function saveBlockEdit() {
const blockId = window.currentEditingBlockId;
const block = window.currentBlocks.find(b => b.id === blockId);
if (!block) return;
if (block.type === 'texto') {
block.content.titulo = document.getElementById('edit-block-titulo').value;
block.content.contenido = document.getElementById('edit-block-contenido').value;
} else {
block.content.url = document.getElementById('edit-block-url').value;
}
closeBlockEditModal();
updatePreview();
renderBlocksList();
document.getElementById('save-btn').innerText = 'Publicar *';
showNotification('Bloque actualizado');
}
function handleInlineEdit(field, currentValue, fieldType) {
// Encontrar el input correspondiente en el sidebar
let input = null;
// Mapear campos a IDs de inputs
const fieldMap = {
'site_name': 'site_name',
'hero_title': 'hero_title',
'hero_description': 'hero_description',
'direccion': 'contacto_direccion',
'telefono': 'contacto_telefono',
'email': 'contacto_email',
'capacidad': 'capacidad',
'horarios.lunes_viernes': 'horarios_lunes_viernes',
'horarios.sabados': 'horarios_sabados',
'horarios.domingos': 'horarios_domingos',
'especialidad_culinaria.titulo': 'especialidad_titulo',
'especialidad_culinaria.descripcion': 'especialidad_descripcion',
'especialidad_culinaria.imagen': 'especialidad_imagen'
};
const inputId = fieldMap[field];
if (inputId) {
input = document.getElementById(inputId);
}
if (input) {
// Expandir la sección correspondiente
const section = input.closest('.accordion-section');
if (section) {
const title = section.querySelector('.accordion-section-title');
if (title && !title.classList.contains('open')) {
toggleSection(title);
}
}
// Enfocar y seleccionar el input
input.focus();
input.select();
// Scroll al input
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
// Si es un campo anidado (menu_items, blocks), abrir modal
if (field.startsWith('menu_items.')) {
gestionarPlatosMenu();
// Buscar el plato específico y editarlo
const parts = field.split('.');
const itemId = parts[1];
const itemField = parts[2];
setTimeout(() => {
const item = window.currentMenuItems[itemId];
if (item) {
editMenuPlato(itemId);
}
}, 300);
} else if (field.startsWith('blocks.')) {
const blockId = field.split('.')[1];
handleEditBlock(blockId);
} else {
// Modal genérico para otros campos
openEditModal(field, currentValue, fieldType);
}
}
}
function openEditModal(field, currentValue, fieldType) {
// Crear modal si no existe
let modal = document.getElementById('inline-edit-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'inline-edit-modal';
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h3>Editar Campo</h3>
<button class="modal-close" onclick="closeEditModal()">&times;</button>
</div>
<div class="modal-body" style="padding: 20px;">
<div class="customize-control">
<span class="customize-control-title" id="modal-field-label">Campo</span>
<input type="text" id="modal-field-input" style="width: 100%; margin-bottom: 15px;">
<textarea id="modal-field-textarea" style="width: 100%; min-height: 100px; display: none;"></textarea>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button class="button button-secondary" onclick="closeEditModal()">Cancelar</button>
<button class="button button-primary" onclick="saveInlineEdit()">Guardar</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
}
// Configurar modal
const label = document.getElementById('modal-field-label');
const textInput = document.getElementById('modal-field-input');
const textarea = document.getElementById('modal-field-textarea');
label.textContent = field.replace(/\./g, ' → ');
window.currentEditField = field;
window.currentEditFieldType = fieldType;
if (fieldType === 'textarea') {
textInput.style.display = 'none';
textarea.style.display = 'block';
textarea.value = currentValue || '';
textarea.focus();
} else {
textInput.style.display = 'block';
textarea.style.display = 'none';
textInput.value = currentValue || '';
textInput.focus();
textInput.select();
}
modal.classList.add('active');
}
function closeEditModal() {
const modal = document.getElementById('inline-edit-modal');
if (modal) {
modal.classList.remove('active');
window.currentEditField = null;
}
}
function saveInlineEdit() {
const field = window.currentEditField;
if (!field) return;
const textInput = document.getElementById('modal-field-input');
const textarea = document.getElementById('modal-field-textarea');
const value = window.currentEditFieldType === 'textarea'
? textarea.value
: textInput.value;
// Actualizar el valor en el formulario
updateFieldValue(field, value);
updatePreview();
closeEditModal();
document.getElementById('save-btn').innerText = 'Publicar *';
}
function updateFieldValue(field, value) {
// Actualizar el input correspondiente o el objeto de datos
const fieldMap = {
'site_name': 'site_name',
'hero_title': 'hero_title',
'hero_description': 'hero_description',
'direccion': 'contacto_direccion',
'telefono': 'contacto_telefono',
'email': 'contacto_email',
'capacidad': 'capacidad',
'horarios.lunes_viernes': 'horarios_lunes_viernes',
'horarios.sabados': 'horarios_sabados',
'horarios.domingos': 'horarios_domingos',
'especialidad_culinaria.titulo': 'especialidad_titulo',
'especialidad_culinaria.descripcion': 'especialidad_descripcion',
'especialidad_culinaria.imagen': 'especialidad_imagen'
};
const inputId = fieldMap[field];
if (inputId) {
const input = document.getElementById(inputId);
if (input) {
input.value = value;
}
} else if (field.startsWith('menu_items.')) {
// Actualizar menu_items
const parts = field.split('.');
const itemId = parts[1];
const itemField = parts[2];
if (window.currentMenuItems[itemId]) {
window.currentMenuItems[itemId][itemField] = value;
}
} else if (field.startsWith('blocks.')) {
// Actualizar blocks
const parts = field.split('.');
const blockId = parts[1];
const blockField = parts.slice(2).join('.');
const block = window.currentBlocks.find(b => b.id === blockId);
if (block) {
const fieldParts = blockField.split('.');
let obj = block.content;
for (let i = 0; i < fieldParts.length - 1; i++) {
if (!obj[fieldParts[i]]) obj[fieldParts[i]] = {};
obj = obj[fieldParts[i]];
}
obj[fieldParts[fieldParts.length - 1]] = value;
}
}
}
// Cerrar modal al hacer click fuera
document.addEventListener('click', function (e) {
const modal = document.getElementById('inline-edit-modal');
if (modal && modal.classList.contains('active') && e.target === modal) {
closeEditModal();
}
});
// Init
loadMenuItems();
loadBlocksList();
</script>
</body>
</html>