Cleanup: Remoción masiva de restos de WordPress y consolidación de archivos GKACHELE™

This commit is contained in:
komkida91
2026-01-27 18:00:16 +01:00
parent d9aad67066
commit 7083aa3893
874 changed files with 137670 additions and 137443 deletions

View File

@@ -0,0 +1,7 @@
---
description: Gestión de funcionalidades del Plan Base
---
1. Los temas permitidos son únicamente los marcados como `plan: base` en `config.json`.
2. El Customizer permite cambios básicos de colores y texto.
3. El rubro por defecto es `restaurante`.

View File

@@ -0,0 +1,7 @@
---
description: Gestión de funcionalidades del Plan Premium
---
1. Acceso total a todas las plantillas (`base`, `pro`, `premium`).
2. Funcionalidades exclusivas como Custom Domains y soporte prioritario.
3. Capacidad de usar el Customizer Premium completo sin restricciones.

View File

@@ -0,0 +1,7 @@
---
description: Gestión de funcionalidades del Plan Pro
---
1. Incluye acceso a plantillas marcadas como `plan: pro` y `plan: base`.
2. Habilita bloques avanzados de video e imágenes.
3. El soporte para personalización de tipografía está activado.

View File

@@ -20,7 +20,7 @@ def init_db():
password TEXT NOT NULL, password TEXT NOT NULL,
role TEXT DEFAULT 'subscriber', role TEXT DEFAULT 'subscriber',
plan TEXT DEFAULT 'base', plan TEXT DEFAULT 'base',
rubro TEXT DEFAULT 'gimnasio', rubro TEXT DEFAULT 'restaurante',
status TEXT DEFAULT 'active', status TEXT DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''') )''')

View File

@@ -11,10 +11,21 @@ customizer_bp = Blueprint('customizer', __name__)
@customizer_bp.route('/api/themes') @customizer_bp.route('/api/themes')
def list_themes(): def list_themes():
"""Listar todos los templates disponibles""" """Listar todos los templates disponibles filtrados por plan"""
from utils.theme_engine import get_themes_by_rubro from utils.theme_engine import get_themes_by_rubro
rubro = request.args.get('rubro', None) rubro = request.args.get('rubro', None)
themes = get_themes_by_rubro(rubro) if rubro else scan_available_themes() user_id = session.get('user_id')
user_plan = 'base'
if user_id:
conn = sqlite3.connect(MAIN_DB)
c = conn.cursor()
c.execute('SELECT plan FROM users WHERE id = ?', (user_id,))
res = c.fetchone()
conn.close()
if res: user_plan = res[0]
themes = get_themes_by_rubro(rubro, user_plan) if rubro else scan_available_themes()
return jsonify({'success': True, 'themes': themes, 'total': len(themes)}) return jsonify({'success': True, 'themes': themes, 'total': len(themes)})
@customizer_bp.route('/customizer/<int:site_id>') @customizer_bp.route('/customizer/<int:site_id>')
@@ -42,7 +53,16 @@ def customizer_view(site_id):
theme_template = f.read() theme_template = f.read()
theme_config = get_theme_config(theme) theme_config = get_theme_config(theme)
available_themes = scan_available_themes()
# 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()
user_plan = user_data[0] if user_data else 'base'
user_rubro = user_data[1] if user_data else 'restaurante'
from utils.theme_engine import get_themes_by_rubro
available_themes = get_themes_by_rubro(user_rubro, user_plan)
return render_template('customizer.html', return render_template('customizer.html',
site_id=site_id, site_id=site_id,
@@ -51,7 +71,8 @@ def customizer_view(site_id):
content=content, content=content,
theme_template=theme_template, theme_template=theme_template,
theme_config=theme_config, theme_config=theme_config,
available_themes=available_themes) available_themes=available_themes,
user_plan=user_plan)
@customizer_bp.route('/api/customizer/save', methods=['POST']) @customizer_bp.route('/api/customizer/save', methods=['POST'])
def save_customizer(): def save_customizer():

View File

@@ -1,4 +1,5 @@
from flask import Blueprint, render_template, send_from_directory, sqlite3 from flask import Blueprint, render_template, send_from_directory
import sqlite3
import json import json
import os import os
from config import MAIN_DB, UPLOADS_DIR from config import MAIN_DB, UPLOADS_DIR
@@ -14,7 +15,6 @@ def landing():
@public_bp.route('/site/<slug>') @public_bp.route('/site/<slug>')
def public_site(slug): def public_site(slug):
"""Sitio público del cliente""" """Sitio público del cliente"""
import sqlite3
conn = sqlite3.connect(MAIN_DB) conn = sqlite3.connect(MAIN_DB)
c = conn.cursor() c = conn.cursor()
c.execute('SELECT id, theme, content_json, status, user_id FROM sites WHERE slug = ?', (slug,)) c.execute('SELECT id, theme, content_json, status, user_id FROM sites WHERE slug = ?', (slug,))

View File

@@ -1,11 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Administración del Sitio</title> <title>Dashboard - Administración del Sitio</title>
<style> <style>
* { box-sizing: border-box; } * {
box-sizing: border-box;
}
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
margin: 0; margin: 0;
@@ -24,12 +28,14 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.sidebar-header { .sidebar-header {
padding: 10px 0 10px 20px; padding: 10px 0 10px 20px;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
color: #fff; color: #fff;
} }
.menu-item { .menu-item {
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
@@ -39,12 +45,19 @@
border-left: 4px solid transparent; border-left: 4px solid transparent;
cursor: pointer; cursor: pointer;
} }
.menu-item:hover, .menu-item.active {
.menu-item:hover,
.menu-item.active {
background: #2c3338; background: #2c3338;
color: #72aee6; color: #72aee6;
border-left-color: #72aee6; border-left-color: #72aee6;
} }
.menu-item i { margin-right: 8px; width: 16px; text-align: center; }
.menu-item i {
margin-right: 8px;
width: 16px;
text-align: center;
}
/* Main Content */ /* Main Content */
.main-content { .main-content {
@@ -65,17 +78,30 @@
justify-content: space-between; justify-content: space-between;
font-size: 13px; font-size: 13px;
} }
.top-bar a { color: #fff; text-decoration: none; margin-left: 20px; }
.top-bar a:hover { color: #72aee6; } .top-bar a {
color: #fff;
text-decoration: none;
margin-left: 20px;
}
.top-bar a:hover {
color: #72aee6;
}
/* Content Area */ /* Content Area */
.wp-content { .gk-content {
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
flex-grow: 1; flex-grow: 1;
} }
h1 { font-size: 23px; font-weight: 400; margin: 0 0 20px 0; padding: 0; } h1 {
font-size: 23px;
font-weight: 400;
margin: 0 0 20px 0;
padding: 0;
}
.card { .card {
background: #fff; background: #fff;
@@ -83,7 +109,7 @@
padding: 20px; padding: 20px;
margin-bottom: 20px; margin-bottom: 20px;
max-width: 800px; max-width: 800px;
box-shadow: 0 1px 1px rgba(0,0,0,.04); box-shadow: 0 1px 1px rgba(0, 0, 0, .04);
} }
.welcome-panel { .welcome-panel {
@@ -107,11 +133,28 @@
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
} }
.btn-primary:hover { background: #135e96; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; } .btn-primary:hover {
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #f0f0f1; } background: #135e96;
th { font-weight: 600; } }
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th,
td {
text-align: left;
padding: 10px;
border-bottom: 1px solid #f0f0f1;
}
th {
font-weight: 600;
}
.status-badge { .status-badge {
background: #f0f0f1; background: #f0f0f1;
color: #646970; color: #646970;
@@ -120,18 +163,27 @@
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
} }
.status-badge.published { background: #edfaef; color: #008a20; }
.status-badge.pending { background: #fff8e5; color: #996800; } .status-badge.published {
background: #edfaef;
color: #008a20;
}
.status-badge.pending {
background: #fff8e5;
color: #996800;
}
</style> </style>
<!-- FontAwesome simplificado para iconos --> <!-- FontAwesome simplificado para iconos -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head> </head>
<body> <body>
<!-- Sidebar --> <!-- Sidebar -->
<div class="sidebar"> <div class="sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<i class="fab fa-wordpress-simple"></i> GKACHELE <i class="fas fa-cube"></i> GKACHELE
</div> </div>
<a href="#" class="menu-item active"><i class="fas fa-tachometer-alt"></i> Escritorio</a> <a href="#" class="menu-item active"><i class="fas fa-tachometer-alt"></i> Escritorio</a>
<a href="#" class="menu-item"><i class="fas fa-thumbtack"></i> Entradas</a> <a href="#" class="menu-item"><i class="fas fa-thumbtack"></i> Entradas</a>
@@ -156,16 +208,18 @@
</div> </div>
</div> </div>
<!-- WP Content --> <!-- GK Content -->
<div class="wp-content"> <div class="gk-content">
<h1>Escritorio</h1> <h1>Escritorio</h1>
<div class="welcome-panel"> <div class="welcome-panel">
<div> <div>
<h2 style="margin-top: 0;">¡Te damos la bienvenida a tu panel!</h2> <h2 style="margin-top: 0;">¡Te damos la bienvenida a tu panel!</h2>
<p style="color: #646970;">Aquí puedes gestionar todos tus sitios y contenidos de forma profesional.</p> <p style="color: #646970;">Aquí puedes gestionar todos tus sitios y contenidos de forma profesional.
</p>
</div> </div>
<button class="btn-primary" onclick="window.location.href='/dashboard/create'">+ Crear Nuevo Sitio</button> <button class="btn-primary" onclick="window.location.href='/dashboard/create'">+ Crear Nuevo
Sitio</button>
</div> </div>
<div class="card"> <div class="card">
@@ -189,7 +243,8 @@
<td>{{ site.theme }}</td> <td>{{ site.theme }}</td>
<td><span class="status-badge {{ site.status }}">{{ site.status|upper }}</span></td> <td><span class="status-badge {{ site.status }}">{{ site.status|upper }}</span></td>
<td> <td>
<a href="/customizer/{{ site.id }}" class="btn-primary" style="padding: 4px 8px; font-size: 11px;">Personalizar</a> <a href="/customizer/{{ site.id }}" class="btn-primary"
style="padding: 4px 8px; font-size: 11px;">Personalizar</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -216,4 +271,5 @@
</div> </div>
</body> </body>
</html> </html>

View File

@@ -9,25 +9,24 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<style> <style>
/* /*
* WORDPRESS CUSTOMIZER STYLE CLONE * GKACHELE™ CUSTOMIZER STYLE CLONE
* Colors: * Colors:
* - Admin Bar: #23282d * - Admin Bar: #1d2327
* - Sidebar BG: #f0f0f1 * - Sidebar BG: #f0f0f1
* - Active Item: #fff * - Active Item: #fff
* - Primary Button: #2271b1 (Hover: #135e96) * - Primary Button: #2271b1 (Hover: #135e96)
* - Text: #2c3338 * - Text: #2c3338
*/ */
:root { :root {
--wp-admin-bar: #23282d; --gk-admin-bar: #1d2327;
--wp-sidebar-bg: #f0f0f1; --gk-sidebar-bg: #f0f0f1;
--wp-white: #ffffff; --gk-white: #ffffff;
--wp-border: #dcdcde; --gk-border: #dcdcde;
--wp-text: #2c3338; --gk-text: #2c3338;
--wp-blue: #2271b1; --gk-blue: #2271b1;
--wp-blue-hover: #135e96; --gk-blue-hover: #135e96;
--wp-red: #d63638; --gk-red: #d63638;
--transition: 0.2s ease; --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
* { * {
@@ -36,30 +35,55 @@
box-sizing: border-box; box-sizing: border-box;
} }
/* GK STYLE CLONE - Premium UI */
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 13px; color: var(--gk-text);
color: var(--wp-text);
background: #f1f1f1; background: #f1f1f1;
}
.gk-full-overlay {
display: flex;
height: 100vh; height: 100vh;
}
.gk-sidebar {
width: 300px;
background: var(--gk-sidebar-bg);
border-right: 1px solid var(--gk-border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; 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) */ /* ACTIONS BAR (Left Side Controls) */
.wp-full-overlay { .gk-full-overlay {
display: flex; display: flex;
flex: 1; flex: 1;
height: 100%; height: 100%;
} }
.wp-full-overlay-sidebar { .gk-full-overlay-sidebar {
width: 300px; width: 300px;
/* Estándar WP */
min-width: 300px; min-width: 300px;
background: var(--wp-sidebar-bg); background: var(--gk-sidebar-bg);
border-right: 1px solid var(--wp-border); border-right: 1px solid var(--gk-border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
@@ -226,6 +250,7 @@
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
-webkit-appearance: none; -webkit-appearance: none;
appearance: none;
border-radius: 3px; border-radius: 3px;
white-space: nowrap; white-space: nowrap;
box-sizing: border-box; box-sizing: border-box;
@@ -261,10 +286,10 @@
} }
/* FOOTER ACTIONS */ /* FOOTER ACTIONS */
.wp-full-overlay-footer { .gk-full-overlay-footer {
height: 46px; height: 46px;
background: #fff; background: #fff;
border-top: 1px solid var(--wp-border); border-top: 1px solid var(--gk-border);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
@@ -293,7 +318,7 @@
} }
/* PREVIEW AREA */ /* PREVIEW AREA */
.wp-full-overlay-main { .gk-full-overlay-main {
flex: 1; flex: 1;
position: relative; position: relative;
background: #f0f0f1; background: #f0f0f1;
@@ -438,7 +463,7 @@
.modal-header h3 { .modal-header h3 {
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
color: var(--wp-text); color: var(--gk-text);
} }
.modal-close { .modal-close {
@@ -489,7 +514,7 @@
.menu-item-info h4 { .menu-item-info h4 {
margin: 0 0 5px 0; margin: 0 0 5px 0;
font-size: 16px; font-size: 16px;
color: var(--wp-text); color: var(--gk-text);
} }
.menu-item-info p { .menu-item-info p {
@@ -545,36 +570,36 @@
<body> <body>
<div class="wp-full-overlay"> <div class="gk-full-overlay">
<!-- SIDEBAR CONTROLS --> <!-- SIDEBAR CONTROLS -->
<div class="wp-full-overlay-sidebar"> <div class="gk-sidebar">
<div class="customize-controls-close"> <div class="gk-header">
<h2>Personalizar</h2> <div class="gk-logo">GKACHELE™</div>
<a href="/dashboard" class="close-btn" title="Cerrar"><i class="fa-solid fa-xmark"></i></a> <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>
<div class="customize-pane-child"> <div class="customize-pane-child">
<!-- TEMPLATE SELECTOR -->
<!-- TEMPLATE SELECTOR --> <!-- TEMPLATE SELECTOR -->
<div class="accordion-section"> <div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)"> <div class="accordion-section-title" onclick="toggleSection(this)">
<span><i class="fa-solid fa-paintbrush" style="margin-right:8px; color:#555;"></i> Diseño & <span><i class="fa-solid fa-paintbrush" style="margin-right:8px; color:var(--gk-blue);"></i>
Template</span> Plantilla & Plan</span>
</div> </div>
<div class="accordion-section-content"> <div class="accordion-section-content">
<div class="customize-control"> <div class="customize-control">
<span class="customize-control-title">Tema Activo</span> <span class="customize-control-title">Template Actual</span>
<select id="theme_selector" onchange="confirmChangeTheme(this)"> <select id="theme_selector" onchange="confirmChangeTheme(this)">
{% for theme_id, theme_info in available_themes.items() %} {% for theme_id, theme_info in available_themes.items() %}
<option value="{{ theme_id }}" {% if theme==theme_id %}selected{% endif %}> <option value="{{ theme_id }}" {% if theme==theme_id %}selected{% endif %}>
{{ theme_info.name }} {{ theme_info.name }} (Plan {{ theme_info.plan|upper }})
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<span class="customize-control-description">Cambiar el tema restablecerá colores por <span class="customize-control-description">Estás en el plan <strong>{{ user_plan|upper
defecto.</span> }}</strong>.</span>
</div> </div>
</div> </div>
</div> </div>
@@ -730,7 +755,8 @@
<!-- HORARIOS --> <!-- HORARIOS -->
<div class="accordion-section"> <div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)"> <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> <span><i class="fa-solid fa-clock" style="margin-right:8px; color:#555;"></i> Horarios de
Atención</span>
</div> </div>
<div class="accordion-section-content"> <div class="accordion-section-content">
<div class="customize-control"> <div class="customize-control">
@@ -757,7 +783,8 @@
<!-- ESPECIALIDAD CULINARIA --> <!-- ESPECIALIDAD CULINARIA -->
<div class="accordion-section"> <div class="accordion-section">
<div class="accordion-section-title" onclick="toggleSection(this)"> <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> <span><i class="fa-solid fa-star" style="margin-right:8px; color:#555;"></i> Especialidad
Culinaria</span>
</div> </div>
<div class="accordion-section-content"> <div class="accordion-section-content">
<div class="customize-control"> <div class="customize-control">
@@ -790,9 +817,10 @@
<div class="customize-control"> <div class="customize-control">
<span class="customize-control-title">Capacidad del Restaurante</span> <span class="customize-control-title">Capacidad del Restaurante</span>
<input type="number" id="capacidad" <input type="number" id="capacidad"
value="{{ content.capacidad if content.capacidad else '50' }}" value="{{ content.capacidad if content.capacidad else '50' }}" placeholder="50" min="1"
placeholder="50" min="1" oninput="updatePreview()"> oninput="updatePreview()">
<span class="customize-control-description">Número de personas que puede albergar el restaurante.</span> <span class="customize-control-description">Número de personas que puede albergar el
restaurante.</span>
</div> </div>
</div> </div>
</div> </div>
@@ -824,8 +852,7 @@
<option value="video">Video</option> <option value="video">Video</option>
<option value="mapa">Mapa</option> <option value="mapa">Mapa</option>
</select> </select>
<button class="button button-primary" <button class="button button-primary" onclick="addNewBlockClientSide()">Insertar</button>
onclick="addNewBlockClientSide()">Insertar</button>
<button class="button button-link" <button class="button button-link"
style="color:red; border:none; background:none; cursor:pointer; font-size:12px;" style="color:red; border:none; background:none; cursor:pointer; font-size:12px;"
onclick="document.getElementById('add-block-panel').style.display='none'">Cancelar</button> onclick="document.getElementById('add-block-panel').style.display='none'">Cancelar</button>
@@ -842,8 +869,7 @@
<div class="accordion-section-content"> <div class="accordion-section-content">
<div class="customize-control"> <div class="customize-control">
<span class="customize-control-title">Enlace PDF Menú</span> <span class="customize-control-title">Enlace PDF Menú</span>
<input type="url" id="menu_url" <input type="url" id="menu_url" value="{{ content.menu_url if content.menu_url else '' }}"
value="{{ content.menu_url if content.menu_url else '' }}"
oninput="updatePreview()"> oninput="updatePreview()">
</div> </div>
<div class="customize-control"> <div class="customize-control">
@@ -898,7 +924,8 @@
<div class="menu-item-form"> <div class="menu-item-form">
<span class="customize-control-title">Añadir Nuevo Plato</span> <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;"> <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> <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;"> <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> <button class="button button-primary" onclick="addMenuPlato()">Añadir Plato</button>
</div> </div>
@@ -908,6 +935,23 @@
</div> </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> <script>
const siteId = {{ site_id }}; const siteId = {{ site_id }};
let currentTheme = '{{ theme }}'; let currentTheme = '{{ theme }}';
@@ -977,9 +1021,11 @@
const direccion = document.getElementById('contacto_direccion').value; const direccion = document.getElementById('contacto_direccion').value;
let mapa_url = ''; let mapa_url = '';
// Generar mapa automáticamente desde dirección // Generar mapa automáticamente desde dirección si no se ha pegado un iframe manualmente
if (direccion) { if (direccion) {
const encodedAddress = encodeURIComponent(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`; mapa_url = `https://www.google.com/maps?q=${encodedAddress}&output=embed`;
} }
@@ -1232,16 +1278,18 @@
window.currentMenuItems = {}; window.currentMenuItems = {};
function loadMenuItems() { function loadMenuItems() {
// Cargar desde content si existe // Cargar desde el objeto inicial renderizado por Jinja2
const data = getFormData(); const initialContent = {{ content| tojson | safe
if (data.menu_items && Object.keys(data.menu_items).length > 0) { }};
window.currentMenuItems = data.menu_items;
if (initialContent && initialContent.menu_items && Object.keys(initialContent.menu_items).length > 0) {
window.currentMenuItems = initialContent.menu_items;
} else { } else {
// Inicializar con algunos platos por defecto // Platos por defecto si no hay nada guardado
window.currentMenuItems = { window.currentMenuItems = {
'1': { nombre: 'Plato Especial 1', descripcion: 'Descripción del plato con ingredientes frescos y sabores únicos.', precio: '25.00' }, '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 ingredientes frescos y sabores únicos.', precio: '28.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 con ingredientes frescos y sabores únicos.', precio: '30.00' } '3': { nombre: 'Plato Especial 3', descripcion: 'Descripción del plato tradicional.', precio: '30.00' }
}; };
} }
renderMenuItems(); renderMenuItems();
@@ -1333,27 +1381,13 @@
document.getElementById('menu-modal').classList.remove('active'); document.getElementById('menu-modal').classList.remove('active');
} }
// Cargar menu_items iniciales desde content
function loadInitialMenuItems() {
const content = {{ content|tojson|safe }};
if (content && content.menu_items && Object.keys(content.menu_items).length > 0) {
window.currentMenuItems = content.menu_items;
} else {
// Inicializar con algunos platos por defecto
window.currentMenuItems = {
'1': { nombre: 'Plato Especial 1', descripcion: 'Descripción del plato con ingredientes frescos y sabores únicos.', precio: '25.00' },
'2': { nombre: 'Plato Especial 2', descripcion: 'Descripción del plato con ingredientes frescos y sabores únicos.', precio: '28.00' },
'3': { nombre: 'Plato Especial 3', descripcion: 'Descripción del plato con ingredientes frescos y sabores únicos.', precio: '30.00' }
};
}
}
// ========================================== // ==========================================
// WORDPRESS-STYLE INLINE EDITING // WORDPRESS-STYLE INLINE EDITING
// ========================================== // ==========================================
// Escuchar mensajes del iframe (click to edit) // Escuchar mensajes del iframe (click to edit)
window.addEventListener('message', function(e) { window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'gk-edit-field') { if (e.data && e.data.type === 'gk-edit-field') {
handleInlineEdit(e.data.field, e.data.value, e.data.type_field); handleInlineEdit(e.data.field, e.data.value, e.data.type_field);
} else if (e.data && e.data.type === 'gk-edit-block') { } else if (e.data && e.data.type === 'gk-edit-block') {
@@ -1362,26 +1396,58 @@
}); });
function handleEditBlock(blockId) { function handleEditBlock(blockId) {
// Expandir sección de bloques const block = window.currentBlocks.find(b => b.id === blockId);
const blocksSection = document.querySelector('[onclick*="Bloques"]'); if (!block) return;
if (blocksSection) {
const title = blocksSection.closest('.accordion-section')?.querySelector('.accordion-section-title'); const form = document.getElementById('block-edit-form');
if (title && !title.classList.contains('open')) { form.innerHTML = '';
toggleSection(title); 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>
`;
} }
// Buscar el bloque en la lista y destacarlo document.getElementById('block-edit-modal').classList.add('active');
const blockItem = document.querySelector(`[data-id="${blockId}"]`);
if (blockItem) {
blockItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
blockItem.style.background = '#fff3cd';
setTimeout(() => {
blockItem.style.background = '';
}, 2000);
} }
showNotification('Bloque seleccionado. Edítalo desde la lista de bloques.'); 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) { function handleInlineEdit(field, currentValue, fieldType) {
@@ -1441,8 +1507,8 @@
} }
}, 300); }, 300);
} else if (field.startsWith('blocks.')) { } else if (field.startsWith('blocks.')) {
// Para bloques, mostrar opciones de edición const blockId = field.split('.')[1];
showNotification('Edita el bloque desde la lista de bloques en el sidebar'); handleEditBlock(blockId);
} else { } else {
// Modal genérico para otros campos // Modal genérico para otros campos
openEditModal(field, currentValue, fieldType); openEditModal(field, currentValue, fieldType);
@@ -1580,7 +1646,7 @@
} }
// Cerrar modal al hacer click fuera // Cerrar modal al hacer click fuera
document.addEventListener('click', function(e) { document.addEventListener('click', function (e) {
const modal = document.getElementById('inline-edit-modal'); const modal = document.getElementById('inline-edit-modal');
if (modal && modal.classList.contains('active') && e.target === modal) { if (modal && modal.classList.contains('active') && e.target === modal) {
closeEditModal(); closeEditModal();
@@ -1588,7 +1654,7 @@
}); });
// Init // Init
loadInitialMenuItems(); loadMenuItems();
loadBlocksList(); loadBlocksList();
</script> </script>

View File

@@ -1,6 +1,7 @@
{ {
"name": "Restaurante Asiático", "name": "Restaurante Asiático",
"rubro": "restaurante", "rubro": "restaurante",
"plan": "premium",
"description": "Tema moderno y elegante para restaurantes asiáticos", "description": "Tema moderno y elegante para restaurantes asiáticos",
"sections": [ "sections": [
"hero", "hero",

View File

@@ -1,6 +1,7 @@
{ {
"name": "Restaurante Elegante", "name": "Restaurante Elegante",
"rubro": "restaurante", "rubro": "restaurante",
"plan": "pro",
"description": "Tema sofisticado y elegante para restaurantes de alta cocina", "description": "Tema sofisticado y elegante para restaurantes de alta cocina",
"sections": [ "sections": [
"hero", "hero",

View File

@@ -1,6 +1,7 @@
{ {
"name": "Restaurante Moderno", "name": "Restaurante Moderno",
"rubro": "restaurante", "rubro": "restaurante",
"plan": "base",
"description": "Tema elegante para restaurantes", "description": "Tema elegante para restaurantes",
"sections": [ "sections": [
"hero", "hero",

View File

@@ -30,6 +30,7 @@ def scan_available_themes():
'name': config.get('name', theme_dir), 'name': config.get('name', theme_dir),
'description': config.get('description', ''), 'description': config.get('description', ''),
'rubro': config.get('rubro', 'general'), 'rubro': config.get('rubro', 'general'),
'plan': config.get('plan', 'base'),
'sections': config.get('sections', []), 'sections': config.get('sections', []),
'colors': config.get('colors', {}), 'colors': config.get('colors', {}),
'typography': config.get('typography', {}), 'typography': config.get('typography', {}),
@@ -55,10 +56,19 @@ def get_theme_config(theme_id):
print(f"⚠️ Error cargando config de {theme_id}: {e}") print(f"⚠️ Error cargando config de {theme_id}: {e}")
return None return None
def get_themes_by_rubro(rubro): def get_themes_by_rubro(rubro, user_plan='base'):
"""Obtener templates filtrados por rubro""" """Obtener templates filtrados por rubro y plan del usuario"""
all_themes = scan_available_themes() all_themes = scan_available_themes()
return {k: v for k, v in all_themes.items() if v.get('rubro') == rubro or v.get('rubro') == 'general'} plan_priority = {'base': 1, 'pro': 2, 'premium': 3}
user_level = plan_priority.get(user_plan, 1)
filtered_themes = {}
for k, v in all_themes.items():
theme_level = plan_priority.get(v.get('plan', 'base'), 1)
# Solo mostrar temas del rubro (o general) que estén dentro del plan del usuario
if (v.get('rubro') == rubro or v.get('rubro') == 'general') and theme_level <= user_level:
filtered_themes[k] = v
return filtered_themes
def get_site_menus(site_id, user_id): def get_site_menus(site_id, user_id):
"""Obtener menús del sitio organizados por ubicación""" """Obtener menús del sitio organizados por ubicación"""
@@ -175,5 +185,20 @@ def render_gkachele_template(theme, content, site_id=None, user_id=None):
return template.render(**template_data) return template.render(**template_data)
full_template = header + theme_template + sidebar + footer full_template = header + theme_template + sidebar + footer
template = Template(full_template)
# Inject WhatsApp Button if configured
whatsapp = content.get('redes_sociales', {}).get('whatsapp')
if whatsapp:
wa_html = f'''
<a href="https://wa.me/{whatsapp}" class="whatsapp-float" target="_blank" style="position:fixed; width:60px; height:60px; bottom:40px; right:40px; background-color:#25d366; color:#FFF; border-radius:50px; text-align:center; font-size:30px; box-shadow: 2px 2px 3px #999; z-index:10000; display:flex; align-items:center; justify-content:center; text-decoration:none;">
<i class="fab fa-whatsapp"></i>
</a>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
'''
if is_full_page:
theme_template = theme_template.replace('</body>', wa_html + '</body>')
else:
footer = footer.replace('</body>', wa_html + '</body>')
template = Template(theme_template if is_full_page else header + theme_template + sidebar + footer)
return template.render(**template_data) return template.render(**template_data)

35
deploy_modular.sh Normal file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# GKACHELE™ SaaS - Modular Deployment Script
# Despliega la nueva estructura (routes, utils, config) a la Raspberry Pi
# Configuración
RASPBERRY_USER="pi"
RASPBERRY_HOST="192.168.1.134"
RASPBERRY_PORT="2222"
RASPBERRY_PASS="Gdk1983gdk45@"
RASPBERRY_PATH="/home/pi/gkachele-saas"
LOCAL_PATH="/mnt/c/word/demo"
echo "🚀 Iniciando despliegue modular de GKACHELE™..."
# 1. Asegurar directorios en la Raspberry
sshpass -p "$RASPBERRY_PASS" ssh -p $RASPBERRY_PORT -o StrictHostKeyChecking=no $RASPBERRY_USER@$RASPBERRY_HOST "mkdir -p $RASPBERRY_PATH/routes $RASPBERRY_PATH/utils $RASPBERRY_PATH/templates $RASPBERRY_PATH/themes"
# 2. Copiar archivos core
echo "📦 Copiando archivos base..."
sshpass -p "$RASPBERRY_PASS" scp -P $RASPBERRY_PORT -o StrictHostKeyChecking=no $LOCAL_PATH/app.py $LOCAL_PATH/config.py $LOCAL_PATH/database.py $RASPBERRY_USER@$RASPBERRY_HOST:$RASPBERRY_PATH/
# 3. Copiar rutas y utilidades (la nueva modularización)
echo "📦 Copiando módulos (routes & utils)..."
sshpass -p "$RASPBERRY_PASS" scp -P $RASPBERRY_PORT -o StrictHostKeyChecking=no -r $LOCAL_PATH/routes/* $RASPBERRY_USER@$RASPBERRY_HOST:$RASPBERRY_PATH/routes/
sshpass -p "$RASPBERRY_PASS" scp -P $RASPBERRY_PORT -o StrictHostKeyChecking=no -r $LOCAL_PATH/utils/* $RASPBERRY_USER@$RASPBERRY_HOST:$RASPBERRY_PATH/utils/
# 4. Copiar templates (por si hubo cambios)
echo "📦 Copiando templates..."
sshpass -p "$RASPBERRY_PASS" scp -P $RASPBERRY_PORT -o StrictHostKeyChecking=no -r $LOCAL_PATH/templates/* $RASPBERRY_USER@$RASPBERRY_HOST:$RASPBERRY_PATH/templates/
# 5. Reiniciar el servicio
echo "🔄 Reiniciando servicio GKACHELE™..."
sshpass -p "$RASPBERRY_PASS" ssh -p $RASPBERRY_PORT -o StrictHostKeyChecking=no $RASPBERRY_USER@$RASPBERRY_HOST "sudo systemctl restart gkachele-saas || (sudo pkill -f 'python3 app.py' && cd $RASPBERRY_PATH && nohup python3 app.py > /tmp/app_modular.log 2>&1 &)"
echo "✅ Despliegue completado con éxito."

Some files were not shown because too many files have changed in this diff Show More