feat: Add Dockerfile and initial Docker setup files

This commit is contained in:
komkida91
2026-01-31 16:04:55 +01:00
parent 70c533e755
commit 59812e547e
31 changed files with 7720 additions and 1776 deletions

View File

@@ -1,21 +1,20 @@
FROM python:3.11-slim
# Usa una imagen oficial de Python como base
FROM python:3.9-slim-buster
# Establece el directorio de trabajo dentro del contenedor
WORKDIR /app
# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
git \
&& rm -rf /var/lib/apt/lists/*
# Copiar requirements e instalar
COPY demo/requirements.txt .
# Copia el archivo de requisitos e instálalos
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar el resto del código
# Copia el resto de la aplicación al directorio de trabajo
COPY . .
# Exponer el puerto
EXPOSE 5000
# Expone el puerto en el que corre la aplicación Flask (definido en config.py)
EXPOSE 5001
# Comando para arrancar
CMD ["python", "demo/app.py"]
# Comando para correr la aplicación
# Asegúrate de que app.py esté en el directorio raíz de WORKDIR (/app)
# Y que las variables de entorno si son necesarias para SECRET_KEY y PORT se pasen al docker run o compose
CMD ["python", "app.py"]

Binary file not shown.

Binary file not shown.

188
demo/customizer.html Normal file
View File

@@ -0,0 +1,188 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GKACHELE Customizer</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
body { font-family: system-ui, Arial; margin:0; background:#f0f0f1; }
.wrap { display:flex; height:100vh; }
.sidebar { width:340px; background:#fff; border-right:1px solid #ddd; padding:12px; overflow:auto }
.preview { flex:1; display:flex; flex-direction:column }
.preview-header { padding:12px; background:#fff; border-bottom:1px solid #ddd }
.preview-body { padding:20px; overflow:auto; }
.btn { padding:8px 12px; border-radius:4px; border:0; cursor:pointer }
.btn-primary { background:#2271b1; color:#fff }
.btn-secondary { background:#f0f0f1 }
.block-item { padding:10px; border:1px solid #e5e5e5; margin-bottom:8px; border-radius:4px; display:flex; justify-content:space-between }
</style>
</head>
<body>
<div class="wrap">
<div class="sidebar">
<h3>GKACHELE™ Customizer</h3>
<div>
<h4>Ajustes</h4>
<label>Nombre sitio<br><input id="siteName" placeholder="Mi Sitio"></label>
<label>Color primario<br><input type="color" id="colorPrimary" value="#2271b1"></label>
</div>
<hr>
<div>
<h4>Añadir Bloque</h4>
<button class="btn btn-primary" onclick="addBlock('heading')">Encabezado</button>
<button class="btn btn-primary" onclick="addBlock('paragraph')">Párrafo</button>
<button class="btn btn-primary" onclick="addBlock('image')">Imagen</button>
</div>
<hr>
<div>
<h4>Bloques</h4>
<div id="blocksList"></div>
</div>
</div>
<div class="preview">
<div class="preview-header">
<span id="previewTitle">Vista previa</span>
<div style="float:right">
<button class="btn btn-secondary" onclick="discardChanges()">Descartar</button>
<button class="btn btn-primary" onclick="saveChanges()">Guardar y Publicar</button>
</div>
</div>
<div class="preview-body">
<div id="previewBlocks"></div>
</div>
</div>
</div>
<script>
// Simple customizer adapted to GKACHELE backend
let state = {
siteId: (new URLSearchParams(window.location.search)).get('site_id') || null,
blocks: [],
settings: { siteName:'Mi Sitio', colorPrimary:'#2271b1' },
hasChanges: false,
};
document.addEventListener('DOMContentLoaded', async () => {
// populate from backend if editing a real site
await loadFromBackendIfAvailable();
// fallback: localStorage
loadFromLocalStorage();
renderUI();
});
function loadFromLocalStorage(){
if (!state.siteId) {
const saved = localStorage.getItem('gkachele_customizer_demo');
if (saved) {
const d = JSON.parse(saved);
state.blocks = d.blocks||state.blocks;
state.settings = {...state.settings, ...(d.settings||{})};
}
}
}
async function loadFromBackendIfAvailable(){
if (!state.siteId) return;
try{
const r = await fetch(`/api/customizer/get-content/${state.siteId}`);
const j = await r.json();
if (j && j.success) {
const c = j.content || {};
state.blocks = c.blocks || [];
state.settings = {...state.settings, ...(c.settings||{})};
state.hasChanges = false;
updateSettingsUI();
}
}catch(e){ console.warn('Backend load failed', e); }
}
function updateSettingsUI(){
document.getElementById('siteName').value = state.settings.siteName||'';
document.getElementById('colorPrimary').value = state.settings.colorPrimary||'#2271b1';
}
function renderUI(){
updateSettingsUI();
renderBlocksList();
renderPreview();
}
function addBlock(type){
const b = { id:'b_'+Date.now(), type:type, data: getDefault(type) };
state.blocks.push(b);
state.hasChanges = true;
renderUI();
}
function getDefault(type){
if (type==='heading') return {text:'Título'};
if (type==='paragraph') return {text:'Párrafo de ejemplo'};
if (type==='image') return {url:'https://via.placeholder.com/600x300'};
return {};
}
function renderBlocksList(){
const el = document.getElementById('blocksList');
if (!state.blocks.length) { el.innerHTML='<div style="color:#888">No hay bloques</div>'; return; }
el.innerHTML = state.blocks.map((b,i)=>`<div class="block-item"><div>${b.type}</div><div><button onclick="editBlock(${i})">✏️</button> <button onclick="deleteBlock(${i})">🗑️</button></div></div>`).join('');
}
function renderPreview(){
const el = document.getElementById('previewBlocks');
if (!state.blocks.length) { el.innerHTML='<div style="color:#999">Añade bloques para ver la preview</div>'; return; }
el.innerHTML = state.blocks.map(b=>renderBlockHtml(b)).join('');
document.getElementById('previewTitle').textContent = state.settings.siteName || 'Vista previa';
}
function renderBlockHtml(b){
if (b.type==='heading') return `<h2>${escapeHtml(b.data.text)}</h2>`;
if (b.type==='paragraph') return `<p>${escapeHtml(b.data.text)}</p>`;
if (b.type==='image') return `<img src="${escapeHtml(b.data.url)}" style="max-width:100%;height:auto"/>`;
return `<div>${b.type}</div>`;
}
function editBlock(idx){
const b = state.blocks[idx];
const value = prompt('Editar contenido', b.type==='image'?b.data.url:b.data.text);
if (value!==null){
if (b.type==='image') b.data.url = value; else b.data.text = value;
state.hasChanges = true; renderUI();
}
}
function deleteBlock(idx){ if (confirm('Eliminar bloque?')) { state.blocks.splice(idx,1); state.hasChanges=true; renderUI(); } }
function escapeHtml(s){ if (!s) return ''; return s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;'); }
function discardChanges(){ if (!state.hasChanges){ alert('No hay cambios'); return;} if (confirm('Descartar cambios?')){ state.blocks=[]; state.settings={siteName:'Mi Sitio',colorPrimary:'#2271b1'}; localStorage.removeItem('gkachele_customizer_demo'); renderUI(); state.hasChanges=false;} }
function updateSaveIndicator(status){ console.log('save:',status); }
function saveChanges(){
// collect settings from UI
state.settings.siteName = document.getElementById('siteName').value;
state.settings.colorPrimary = document.getElementById('colorPrimary').value;
const content = { blocks: state.blocks, settings: state.settings };
// save local demo
if (!state.siteId) localStorage.setItem('gkachele_customizer_demo', JSON.stringify(content));
// send to backend using API expected by server: { site_id, content }
if (!state.siteId) { alert('Guardado local (demo). Para guardar en SaaS abre el customizer con ?site_id=ID'); state.hasChanges=false; return; }
updateSaveIndicator('saving');
fetch('/api/customizer/save', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ site_id: state.siteId, content: content })
}).then(r=>r.json()).then(j=>{
if (j && j.success) { state.hasChanges=false; updateSaveIndicator('saved'); alert('Guardado en servidor'); }
else { updateSaveIndicator('error'); alert('Error guardando'); }
}).catch(e=>{ updateSaveIndicator('error'); alert('Error guardando: '+e); });
}
</script>
</body>
</html>

View File

@@ -150,6 +150,6 @@ def init_db():
conn.commit()
conn.close()
print("Base de datos GKACHELE inicializada correctamente.")
print("Base de datos GKACHELE inicializada correctamente.")
except Exception as e:
print(f" Error inicializando DB: {e}")
print(f" Error inicializando DB: {e}")

Binary file not shown.

View File

@@ -1,2 +1,8 @@
blinker==1.9.0
click==8.3.1
colorama==0.4.6
Flask==2.3.3
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
Werkzeug==2.3.7

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -9,6 +9,19 @@ from utils.auth_decorators import login_required
customizer_bp = Blueprint('customizer', __name__)
@customizer_bp.route('/customizer')
def customizer_demo():
"""Ruta de conveniencia: si se pasa ?site_id=ID delega a customizer_view, si no muestra demo"""
sid = request.args.get('site_id')
if sid:
try:
return customizer_view(int(sid))
except Exception:
pass
# Render demo template with empty content
return render_template('customizer.html', site_id='demo', slug='demo', theme=None, content={}, theme_template=None, theme_config={}, available_themes={}, user_plan='base')
@customizer_bp.route('/api/themes')
def list_themes():
"""Listar todos los templates disponibles filtrados por plan"""
@@ -35,12 +48,13 @@ def customizer_view(site_id):
c = conn.cursor()
c.execute('SELECT user_id, slug, theme, content_json FROM sites WHERE id = ?', (site_id,))
site = c.fetchone()
conn.close()
if not site:
conn.close()
return "Sitio no encontrado", 404
if 'user_id' in session and site[0] != session['user_id']:
conn.close()
return "No autorizado", 403
content = json.loads(site[3]) if site[3] else {}
@@ -55,9 +69,10 @@ def customizer_view(site_id):
theme_config = get_theme_config(theme)
# Obtener plan del usuario para filtrar templates
c = conn.cursor()
c.execute('SELECT plan, rubro FROM users WHERE id = ?', (site[0],))
user_data = c.fetchone()
conn.close()
user_plan = user_data[0] if user_data else 'base'
user_rubro = user_data[1] if user_data else 'restaurante'
@@ -91,6 +106,43 @@ def save_customizer():
conn.close()
return jsonify({'success': True})
@customizer_bp.route('/api/customizer/get-blocks/<int:site_id>', methods=['GET'])
def get_blocks(site_id):
"""Retorna los bloques de un sitio"""
conn = sqlite3.connect(MAIN_DB)
c = conn.cursor()
c.execute('SELECT content_json FROM sites WHERE id = ?', (site_id,))
result = c.fetchone()
conn.close()
if not result or not result[0]:
return jsonify([])
try:
content = json.loads(result[0])
return jsonify(content.get('blocks', []))
except:
return jsonify([])
@customizer_bp.route('/api/customizer/get-content/<int:site_id>', methods=['GET'])
def get_content(site_id):
"""Retorna el contenido completo (blocks + settings) de un sitio"""
conn = sqlite3.connect(MAIN_DB)
c = conn.cursor()
c.execute('SELECT content_json FROM sites WHERE id = ?', (site_id,))
result = c.fetchone()
conn.close()
if not result or not result[0]:
return jsonify({'success': True, 'content': {}})
try:
content = json.loads(result[0])
return jsonify({'success': True, 'content': content})
except Exception:
return jsonify({'success': True, 'content': {}})
@customizer_bp.route('/api/customizer/add-block', methods=['POST'])
def add_block():
data = request.get_json()
@@ -102,6 +154,10 @@ def add_block():
c.execute('SELECT content_json FROM sites WHERE id = ?', (site_id,))
result = c.fetchone()
if not result:
conn.close()
return jsonify({'success': False, 'error': 'Sitio no encontrado'}), 404
content = json.loads(result[0]) if result[0] else {}
if 'blocks' not in content: content['blocks'] = []

View File

@@ -0,0 +1,25 @@
from flask import Blueprint, request, jsonify
import json
customizer_bp = Blueprint('customizer_api', __name__)
@customizer_bp.route('/api/customizer/get-blocks/<int:site_id>', methods=['GET'])
def get_blocks(site_id):
"""Retorna los bloques de un sitio"""
import sqlite3
from config import MAIN_DB
conn = sqlite3.connect(MAIN_DB)
c = conn.cursor()
c.execute('SELECT content_json FROM sites WHERE id = ?', (site_id,))
result = c.fetchone()
conn.close()
if not result or not result[0]:
return jsonify([])
try:
content = json.loads(result[0])
return jsonify(content.get('blocks', []))
except:
return jsonify([])

13
demo/static/test.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Customizer</title>
</head>
<body>
<h1>Abre la Consola (F12) y verás los errores del Customizer</h1>
<iframe src="http://localhost:5001/customizer/1" width="100%" height="800px"></iframe>
</body>
</html>

View File

@@ -1,36 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Admin - Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.header {
background: white;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
table {
width: 100%;
background: white;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #667eea;
color: white;
}
.btn {
padding: 5px 15px;
background: #4caf50;
@@ -41,11 +53,12 @@
}
</style>
</head>
<body>
<div class="header">
<h1>🔧 Panel Admin</h1>
</div>
<h2>Solicitudes Pendientes</h2>
<table>
<tr>
@@ -67,10 +80,12 @@
</tr>
{% endfor %}
{% if not requests %}
<tr><td colspan="5">No hay solicitudes pendientes</td></tr>
<tr>
<td colspan="5">No hay solicitudes pendientes</td>
</tr>
{% endif %}
</table>
<h2>👥 Usuarios Registrados</h2>
<table>
<tr>
@@ -104,10 +119,12 @@
</tr>
{% endfor %}
{% if not users %}
<tr><td colspan="9">No hay usuarios registrados</td></tr>
<tr>
<td colspan="9">No hay usuarios registrados</td>
</tr>
{% endif %}
</table>
<h2>🌐 Todos los Sitios</h2>
<table>
<tr>
@@ -131,48 +148,50 @@
</tr>
{% endfor %}
</table>
<style>
.btn-danger {
background: #d63638;
color: white;
}
.btn-danger:hover {
background: #b32d2e;
}
</style>
<script>
function approve(requestId) {
if (confirm('¿Aprobar este sitio?')) {
fetch(`/admin/approve/${requestId}`, {method: 'POST'})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('✅ Sitio aprobado');
location.reload();
}
});
fetch(`/admin/approve/${requestId}`, { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data.success) {
alert('✅ Sitio aprobado');
location.reload();
}
});
}
}
function deleteUser(userId, email) {
if (confirm(`⚠️ ¿Eliminar usuario ${userId} (${email})?\n\nEsto eliminará TODOS sus datos:\n- Sitios\n- Menús\n- Widgets\n- Media\n- Solicitudes\n\nEsta acción NO se puede deshacer.`)) {
fetch(`/admin/users/delete/${userId}`, {method: 'POST'})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('✅ Usuario eliminado exitosamente');
location.reload();
} else {
alert('❌ Error: ' + (data.error || 'Error al eliminar'));
}
})
.catch(err => {
alert('❌ Error: ' + err);
});
fetch(`/admin/users/delete/${userId}`, { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data.success) {
alert('✅ Usuario eliminado exitosamente');
location.reload();
} else {
alert('❌ Error: ' + (data.error || 'Error al eliminar'));
}
})
.catch(err => {
alert('❌ Error: ' + err);
});
}
}
</script>
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,597 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GKACHELE Builder - Premium V3</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--bg-sidebar: #ffffff;
--bg-canvas: #f3f4f6;
--border: #e5e7eb;
--text-main: #111827;
--text-muted: #6b7280;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-canvas);
height: 100vh;
overflow: hidden;
display: flex;
}
/* SIDEBAR */
.sidebar {
width: 380px;
background: var(--bg-sidebar);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.04);
z-index: 20;
}
.sidebar-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
font-weight: 800;
font-size: 18px;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #2563eb, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: flex;
align-items: center;
gap: 8px;
}
.logo i {
-webkit-text-fill-color: #2563eb;
}
.back-link {
color: var(--text-muted);
text-decoration: none;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
transition: 0.2s;
}
.back-link:hover {
color: var(--text-main);
}
.nav-sections {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.section-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
margin-bottom: 8px;
background: #fff;
border: 1px solid var(--border);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.section-item:hover {
border-color: var(--primary);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.08);
transform: translateY(-1px);
}
.section-info {
display: flex;
align-items: center;
gap: 12px;
}
.section-icon {
width: 36px;
height: 36px;
background: #f0f9ff;
color: #0369a1;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.section-title h3 {
font-size: 14px;
font-weight: 600;
color: var(--text-main);
margin-bottom: 2px;
}
.section-title p {
font-size: 12px;
color: var(--text-muted);
}
/* ACTIVE PANEL OVERLAY */
.panel-drawer {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 380px;
background: #fff;
transform: translateX(-100%);
transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 30;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
box-shadow: 10px 0 40px rgba(0, 0, 0, 0.1);
}
.panel-drawer.active {
transform: translateX(0);
}
.drawer-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 15px;
}
.drawer-close {
border: none;
background: #f3f4f6;
width: 32px;
height: 32px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #4b5563;
transition: 0.2s;
}
.drawer-close:hover {
background: #e5e7eb;
color: #111;
}
.drawer-title {
font-weight: 700;
font-size: 16px;
}
.drawer-content {
padding: 24px;
overflow-y: auto;
flex: 1;
}
/* FORM CONTROLS */
.form-group {
margin-bottom: 24px;
}
.label {
display: block;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.input,
.textarea,
.select {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-family: inherit;
font-size: 14px;
transition: 0.2s;
background: #fff;
}
.input:focus,
.textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
outline: none;
}
/* MENU CARD DESIGN */
.dish-card {
background: #fff;
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
display: flex;
gap: 15px;
transition: 0.2s;
}
.dish-card:hover {
border-color: #cbd5e1;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.dish-img {
width: 60px;
height: 60px;
background: #eee;
border-radius: 8px;
object-fit: cover;
}
.dish-info {
flex: 1;
}
.dish-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.dish-name {
font-weight: 600;
font-size: 14px;
}
.dish-price {
font-weight: 700;
color: var(--primary);
font-size: 14px;
}
.dish-desc {
font-size: 12px;
color: #6b7280;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.btn-add {
width: 100%;
padding: 12px;
background: #f0f9ff;
color: #0369a1;
border: 1px dashed #bae6fd;
border-radius: 12px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
margin-bottom: 20px;
transition: 0.2s;
}
.btn-add:hover {
background: #e0f2fe;
border-color: #7dd3fc;
}
/* PREVIEW CANVAS */
.canvas-area {
flex: 1;
background-color: #e5e5e5;
background-image: radial-gradient(#d4d4d4 1px, transparent 1px);
background-size: 20px 20px;
padding: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.device-bar {
background: #fff;
padding: 6px;
border-radius: 50px;
display: flex;
gap: 8px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.device-btn {
width: 40px;
height: 40px;
border: none;
background: transparent;
border-radius: 50%;
color: #6b7280;
cursor: pointer;
transition: 0.2s;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.device-btn.active {
background: var(--bg-canvas);
color: var(--text-main);
}
.device-btn:hover {
color: var(--primary);
}
.preview-frame-wrapper {
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
width: 100%;
height: 100%;
max-width: 1200px;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<!-- SIDEBAR NAVIGATION -->
<div class="sidebar">
<div class="sidebar-header">
<div class="logo"><i class="fa-solid fa-cube"></i> GKACHELE™</div>
<a href="#" class="back-link"><i class="fa-solid fa-arrow-left"></i> Salir</a>
</div>
<div class="nav-sections">
<div class="section-item" onclick="openDrawer('identity')">
<div class="section-info">
<div class="section-icon"><i class="fa-solid fa-store"></i></div>
<div class="section-title">
<h3>Identidad del Sitio</h3>
<p>Logo, Nombre, Slogan</p>
</div>
</div>
<i class="fa-solid fa-chevron-right" style="color:#d1d5db; font-size: 12px;"></i>
</div>
<div class="section-item" onclick="openDrawer('menus')">
<div class="section-info">
<div class="section-icon" style="background:#fff7ed; color:#c2410c;"><i
class="fa-solid fa-utensils"></i></div>
<div class="section-title">
<h3>Menú & Platos</h3>
<p>Gestionar carta digital</p>
</div>
</div>
<i class="fa-solid fa-chevron-right" style="color:#d1d5db; font-size: 12px;"></i>
</div>
<div class="section-item" onclick="openDrawer('style')">
<div class="section-info">
<div class="section-icon" style="background:#fdf4ff; color:#a21caf;"><i
class="fa-solid fa-palette"></i></div>
<div class="section-title">
<h3>Estilo & Marca</h3>
<p>Colores, Tipografía</p>
</div>
</div>
<i class="fa-solid fa-chevron-right" style="color:#d1d5db; font-size: 12px;"></i>
</div>
</div>
<div style="padding: 24px; border-top: 1px solid var(--border);">
<button
style="width:100%; padding: 14px; background: var(--text-main); color: #fff; border:none; border-radius: 10px; font-weight: 600; cursor: pointer;">Publicar
Cambios</button>
</div>
</div>
<!-- DRAWERS (HIDDEN PANELS) -->
<!-- MENU DRAWER -->
<div id="menus" class="panel-drawer">
<div class="drawer-header">
<button class="drawer-close" onclick="closeDrawers()"><i class="fa-solid fa-arrow-left"></i></button>
<span class="drawer-title">Gestionar Menú</span>
</div>
<div class="drawer-content">
<button class="btn-add"><i class="fa-solid fa-plus"></i> Añadir Nuevo Plato</button>
<div class="dish-card">
<img src="https://images.unsplash.com/photo-1546069901-ba9599a7e63c?auto=format&fit=crop&w=200&h=200"
class="dish-img">
<div class="dish-info">
<div class="dish-header">
<span class="dish-name">Bowl Saludable</span>
<span class="dish-price">$12.50</span>
</div>
<p class="dish-desc">Quinoa, aguacate, tomate cherry, huevo pouche y aderezo de sésamo.</p>
</div>
</div>
<div class="dish-card">
<img src="https://images.unsplash.com/photo-1482049016688-2d3e1b311543?auto=format&fit=crop&w=200&h=200"
class="dish-img">
<div class="dish-info">
<div class="dish-header">
<span class="dish-name">Tostada de Aguacate</span>
<span class="dish-price">$8.00</span>
</div>
<p class="dish-desc">Pan de masa madre, aguacate triturado, semillas de girasol.</p>
</div>
</div>
</div>
</div>
<!-- IDENTITY DRAWER -->
<div id="identity" class="panel-drawer">
<div class="drawer-header">
<button class="drawer-close" onclick="closeDrawers()"><i class="fa-solid fa-arrow-left"></i></button>
<span class="drawer-title">Identidad</span>
</div>
<div class="drawer-content">
<div class="form-group">
<label class="label">Nombre del Negocio</label>
<input type="text" class="input" id="inputName" value="Green Bowl Madrid"
oninput="updatePreviewText('.logo', this.value)">
</div>
<div class="form-group">
<label class="label">Descripción Corta</label>
<textarea class="textarea" rows="3" id="inputDesc"
oninput="updatePreviewText('.hero p', this.value)">Comida saludable y fresca en el corazón de la ciudad.</textarea>
</div>
<div class="form-group">
<label class="label">Título Hero</label>
<input type="text" class="input" id="inputHero" value="Sabor Natural"
oninput="updatePreviewText('.hero h1', this.value)">
</div>
</div>
</div>
<!-- PREVIEW AREA -->
<div class="canvas-area">
<div class="device-bar">
<button class="device-btn active" onclick="setDevice('100%')"><i class="fa-solid fa-desktop"></i></button>
<button class="device-btn" onclick="setDevice('768px')"><i
class="fa-solid fa-tablet-screen-button"></i></button>
<button class="device-btn" onclick="setDevice('390px')"><i
class="fa-solid fa-mobile-screen-button"></i></button>
</div>
<div class="preview-frame-wrapper" style="max-width: 100%">
<!-- MOCK FRAME CONTENT FOR DEMO -->
<iframe id="previewFrame" srcdoc='
<html>
<head>
<style>
body { margin: 0; font-family: "Helvetica Neue", sans-serif; }
header { padding: 20px 40px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; }
.logo { font-weight: bold; font-size: 20px; }
.hero { padding: 80px 40px; text-align: center; background: #f9f9f9; }
h1 { font-size: 48px; margin: 0 0 20px 0; }
p { font-size: 18px; color: #666; max-width: 600px; margin: 0 auto; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 30px; padding: 40px; max-width: 1200px; margin: 0 auto; }
.card { border: 1px solid #eee; border-radius: 8px; overflow: hidden; }
.card img { width: 100%; height: 200px; object-fit: cover; }
.card-body { padding: 20px; }
.price { float: right; font-weight: bold; color: #2563eb; }
</style>
</head>
<body>
<header>
<div class="logo">Green Bowl Madrid</div>
<nav>
<a href="#" style="margin-left: 20px; text-decoration: none; color: #333;">Menú</a>
<a href="#" style="margin-left: 20px; text-decoration: none; color: #333;">Reservas</a>
<a href="#" style="margin-left: 20px; text-decoration: none; color: #333;">Contacto</a>
</nav>
</header>
<div class="hero">
<h1>Sabor Natural</h1>
<p>Los mejores ingredientes orgánicos seleccionados para ti cada mañana.</p>
</div>
<div class="grid">
<div class="card">
<img src="https://images.unsplash.com/photo-1546069901-ba9599a7e63c?auto=format&fit=crop&w=500&q=60">
<div class="card-body">
<span class="price">$12.50</span>
<h3>Bowl Saludable</h3>
<p style="font-size: 14px">Quinoa, aguacate, tomate cherry...</p>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1482049016688-2d3e1b311543?auto=format&fit=crop&w=500&q=60">
<div class="card-body">
<span class="price">$8.00</span>
<h3>Tostada de Aguacate</h3>
<p style="font-size: 14px">Pan de masa madre tostado...</p>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1512621776951-a57141f2eefd?auto=format&fit=crop&w=500&q=60">
<div class="card-body">
<span class="price">$10.00</span>
<h3>Ensalada César</h3>
<p style="font-size: 14px">Lechuga romana, crutones, parmesano...</p>
</div>
</div>
</div>
</body>
</html>
'></iframe>
</div>
</div>
<script>
function openDrawer(id) {
document.querySelectorAll(".panel-drawer").forEach(d => d.classList.remove("active"));
const target = document.getElementById(id);
if (target) target.classList.add("active");
}
function closeDrawers() {
document.querySelectorAll(".panel-drawer").forEach(d => d.classList.remove("active"));
}
function setDevice(size) {
const wrapper = document.querySelector(".preview-frame-wrapper");
wrapper.style.maxWidth = size;
document.querySelectorAll(".device-btn").forEach(b => b.classList.remove("active"));
event.currentTarget.classList.add("active");
}
// REAL-TIME PREVIEW LOGIC
function updatePreviewText(selector, value) {
const iframe = document.getElementById('previewFrame');
const doc = iframe.contentDocument || iframe.contentWindow.document;
const el = doc.querySelector(selector);
if (el) el.innerText = value;
}
// Initialize preview editability after load
document.getElementById('previewFrame').onload = function () {
// Optional: Add click-to-edit logic inside iframe if needed later
};
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -164,6 +164,7 @@ def render_gkachele_template(theme, content, site_id=None, user_id=None):
template_data = {
'site_name': content.get('site_name', 'GKACHELE Site'),
'hero_title': content.get('hero_title', 'Bienvenido'),
'hero_description': content.get('hero_description', ''),
'colors': content.get('colors', {}),
'typography': content.get('typography', {}),
'horarios': content.get('horarios', {}),
@@ -171,6 +172,14 @@ def render_gkachele_template(theme, content, site_id=None, user_id=None):
'blocks': content.get('blocks', []),
'menus': menus,
'widgets': widgets,
'especialidad_culinaria': content.get('especialidad_culinaria', {}),
'menu_items': content.get('menu_items', {}),
'menu_url': content.get('menu_url', ''),
'capacidad': content.get('capacidad', '50'),
'direccion': content.get('direccion', ''),
'telefono': content.get('telefono', ''),
'email': content.get('email', ''),
'mapa_url': content.get('mapa_url', ''),
**content
}