Modularización de GKACHELE SaaS

This commit is contained in:
gkachele
2026-01-17 11:40:17 +01:00
commit b6820848b8
1338 changed files with 339275 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
# 🎯 Cómo Usar el Demo
## ⚠️ IMPORTANTE: Python en WSL
Tu proyecto está en WSL, así que necesitas ejecutar desde ahí.
## 🚀 Opción 1: Desde WSL (Recomendado)
```bash
# En WSL
cd /mnt/c/word/demo
chmod +x start.sh
./start.sh
```
O directamente:
```bash
cd /mnt/c/word/demo
python3 app.py
```
## 🚀 Opción 2: Copiar a WSL
```bash
# En WSL
cp -r /mnt/c/word/demo ~/demo-saas
cd ~/demo-saas
python3 app.py
```
## 📋 Pasos para Probar
1. **Abrir navegador**: http://localhost:5001
2. **Registrarse**:
- Email: test@test.com
- Password: test123
- Plan: Base
- Rubro: Gimnasio
3. **Crear sitio**:
- Nombre: Mi Gimnasio
- Slug: mi-gimnasio
- Título: Bienvenido
- Descripción: Tu gimnasio de confianza
4. **Personalizar** (Customizer):
- Cambia colores
- Cambia texto
- Preview se actualiza en tiempo real
- Guarda cambios
5. **Enviar para aprobación**
6. **Admin** (necesitas ser user_id = 1):
- Ve a `/admin`
- Aprueba el sitio
7. **Ver publicado**: `/site/mi-gimnasio`
## 🔧 Si el puerto 5001 no funciona
Cambia en `app.py` línea 400:
```python
port = int(os.environ.get('PORT', 8000)) # Cambia 5001 por 8000
```
## ✅ Verificar que funciona
```bash
# En WSL
curl http://localhost:5001
# Debe mostrar HTML de la landing
```

View File

@@ -0,0 +1,57 @@
# ✅ DEMO FUNCIONANDO - EJECUTAR AHORA
## 🚀 Comando Simple
**Abre WSL y ejecuta:**
```bash
cd /mnt/c/word/demo
python3 app.py
```
## 📍 Luego abre en tu navegador:
```
http://localhost:5001
```
## ✅ Verificación
El test ya pasó:
- ✅ App importada correctamente
- ✅ Flask funcionando
- ✅ Templates encontrados
- ✅ Base de datos inicializada
## 🎯 Flujo de Prueba
1. **Landing** → http://localhost:5001
2. **Registrarse** → Click "Registrarse"
- Email: test@test.com
- Password: test123
3. **Crear sitio** → Click "Crear Sitio"
- Nombre: Mi Gimnasio
- Slug: mi-gimnasio
4. **Customizer** → Se abre automáticamente
- Cambia colores, texto
- Preview se actualiza en tiempo real
5. **Enviar** → Click "Enviar para Aprobación"
6. **Admin** → http://localhost:5001/admin
- (Necesitas ser user_id = 1, el primer usuario registrado)
7. **Ver publicado** → http://localhost:5001/site/mi-gimnasio
## 🔧 Si el puerto 5001 está ocupado
Edita `app.py` línea 399:
```python
port = int(os.environ.get('PORT', 8000)) # Cambia a 8000
```
## ✨ Características Funcionando
- ✅ Multi-tenant (DB por cliente)
- ✅ Customizer (sidebar + preview)
- ✅ Actualización en tiempo real
- ✅ Sistema de solicitudes
- ✅ Admin panel
- ✅ Sitios públicos

View File

@@ -0,0 +1,69 @@
# 🚀 Iniciar Demo en Windows
## Opción 1: Usar Python desde WSL
Si tienes Python en WSL (que es donde tienes tu proyecto):
```bash
# En WSL
cd ~/mi-landing/pagebuilder-saas-prod
# O copia la carpeta demo a WSL
cd /mnt/c/word/demo
python3 app.py
```
## Opción 2: Usar Python de Windows
1. **Verificar Python instalado:**
```powershell
py --version
# o
python --version
```
2. **Instalar dependencias:**
```powershell
cd demo
py -m pip install Flask Werkzeug
```
3. **Ejecutar:**
```powershell
py app.py
```
## Opción 3: Usar el script start.bat
Doble click en `start.bat` (instala dependencias y ejecuta)
## 🔧 Si no funciona
### Verificar puerto
```powershell
netstat -ano | findstr :5001
```
### Cambiar puerto
Edita `app.py` línea 400, cambia:
```python
port = int(os.environ.get('PORT', 5001))
```
Por:
```python
port = int(os.environ.get('PORT', 8000))
```
### Ver errores
Ejecuta directamente:
```powershell
cd demo
py app.py
```
Y verás los errores en la consola.
## 📍 URLs
- Landing: http://localhost:5001
- Admin: http://localhost:5001/admin
- Dashboard: http://localhost:5001/dashboard (requiere login)

View File

@@ -0,0 +1,90 @@
# 🚀 Demo Funcional - Instrucciones
## ✅ Lo que tienes
1. **Backend Flask completo** con:
- Multi-tenant (DB por cliente)
- Customizer (sidebar + preview)
- Sistema de solicitudes
- Admin panel
2. **Flujo completo**:
- Registro → Dashboard → Crear sitio → Customizer → Enviar → Admin aprueba → Publicado
## 🎯 Cómo probarlo
### 1. Instalar dependencias
```bash
cd demo
pip install Flask Werkzeug
```
### 2. Ejecutar
```bash
python app.py
```
### 3. Abrir navegador
```
http://localhost:5000
```
## 📋 Pasos para probar
1. **Registrarse** (`/register`)
- Email: test@test.com
- Password: test123
- Plan: Base
- Rubro: Gimnasio
2. **Crear sitio** (`/dashboard/create`)
- Nombre: Mi Gimnasio
- Slug: mi-gimnasio
- Título: Bienvenido
- Descripción: Tu gimnasio de confianza
3. **Personalizar** (`/customizer/{site_id}`)
- Cambia colores, texto, tipografía
- Preview se actualiza en tiempo real
- Guarda cambios
4. **Enviar para aprobación**
- Click en "Enviar para Aprobación"
- Status cambia a "pending"
5. **Admin** (`/admin`)
- Registra otro usuario (será user_id = 2)
- O modifica la DB para que el primer usuario sea admin (user_id = 1)
- Ve solicitudes pendientes
- Aprueba sitio
6. **Ver sitio publicado** (`/site/{slug}`)
- Una vez aprobado, el sitio está público
## 🔧 Para hacer admin
En `app.py` línea 200, cambia:
```python
if 'user_id' not in session or session['user_id'] != 1:
```
Por:
```python
if 'user_id' not in session:
```
Así cualquier usuario puede ser admin (solo para demo).
## ✨ Características del Demo
- ✅ Multi-tenant: Cada cliente tiene su DB
- ✅ Customizer: Sidebar izquierdo + Preview derecha
- ✅ Actualización en tiempo real
- ✅ Sistema de solicitudes
- ✅ Admin panel
- ✅ Sitios públicos
## 🐛 Si no funciona
1. Verifica que Flask esté instalado: `pip list | findstr Flask`
2. Verifica puerto 5000 libre: `netstat -ano | findstr :5000`
3. Revisa logs en consola
4. Asegúrate de estar en la carpeta `demo/`

View File

@@ -0,0 +1,47 @@
═══════════════════════════════════════════════════════════
✅ DEMO FUNCIONANDO - VERSIÓN SIMPLE
═══════════════════════════════════════════════════════════
🚀 EJECUTAR:
Desde WSL:
cd /mnt/c/word/demo
python3 app_simple.py
Luego abre en navegador:
http://localhost:5001
═══════════════════════════════════════════════════════════
✨ CARACTERÍSTICAS:
✅ Landing page
✅ Registro/Login
✅ Dashboard cliente
✅ Crear sitio
✅ Customizer (sidebar + preview en tiempo real)
✅ Enviar para aprobación
✅ Admin panel
✅ Sitios públicos
═══════════════════════════════════════════════════════════
📋 FLUJO:
1. Registrarse → /register
2. Crear sitio → /create
3. Personalizar → /customizer/{id}
4. Enviar → Click "Enviar"
5. Admin → /admin (aprobar)
6. Ver → /site/{slug}
═══════════════════════════════════════════════════════════
🔧 SI NO FUNCIONA:
1. Verifica Python: python3 --version
2. Instala Flask: pip3 install Flask Werkzeug
3. Cambia puerto en app_simple.py línea final:
port=8000 (en lugar de 5001)
═══════════════════════════════════════════════════════════

View File

@@ -0,0 +1,38 @@
# 🚀 Demo SaaS PageBuilder
## Inicio Rápido
```bash
cd demo
pip install -r requirements.txt
python app.py
```
Abre: http://localhost:5000
## Flujo Demo
1. **Registrarse**`/register`
2. **Crear sitio**`/dashboard/create`
3. **Personalizar**`/customizer/{site_id}` (sidebar + preview)
4. **Enviar** → Cliente envía para aprobación
5. **Admin**`/admin` (aprobar sitios)
6. **Publicado**`/site/{slug}`
## Credenciales Admin
- User ID: 1 (primer usuario registrado)
- Accede a `/admin` con user_id = 1
## Estructura
```
demo/
├── app.py # Backend Flask
├── database/ # SQLite DBs
│ ├── main.db # DB principal
│ └── sites/ # DB por cliente
├── sites/ # Sitios compilados
├── themes/ # Templates
└── templates/ # HTML templates
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,594 @@
"""
Demo SaaS - Versión SIMPLE que FUNCIONA
Sistema profesional automatizado - GKACHELE™
"""
from flask import Flask, render_template, render_template_string, request, jsonify, session, redirect
import sqlite3
import os
import json
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__, template_folder='templates', static_folder='static')
app.secret_key = 'demo-2025'
# DB simple
DB_PATH = 'demo.db'
def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY, email TEXT UNIQUE, password TEXT, plan TEXT, rubro TEXT
)''')
c.execute('''CREATE TABLE IF NOT EXISTS sites (
id INTEGER PRIMARY KEY, user_id INTEGER, slug TEXT UNIQUE,
content TEXT, status TEXT DEFAULT 'draft'
)''')
conn.commit()
conn.close()
init_db()
# Landing profesional (inspirada en tu landing real)
LANDING_HTML = '''
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PageBuilder SaaS | Crea tu sitio web automatizado</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; color: #333; }
/* Nav */
.nav-sticky { position: fixed; top: 0; width: 100%; background: rgba(255,255,255,0.95);
backdrop-filter: blur(10px); z-index: 1000; padding: 15px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.nav-container { max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; padding: 0 20px; }
.nav-logo a { font-size: 1.5rem; font-weight: 700; color: #667eea; text-decoration: none; }
.nav-menu { display: flex; gap: 30px; }
.nav-link { color: #333; text-decoration: none; font-weight: 500; transition: color 0.3s; }
.nav-link:hover { color: #667eea; }
.nav-social { display: flex; gap: 15px; }
.nav-social a { color: #667eea; font-size: 1.2rem; }
/* Hero */
.hero-section { min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex; align-items: center; justify-content: center; text-align: center; color: white;
position: relative; padding-top: 80px; }
.hero-content { max-width: 800px; padding: 40px 20px; }
.hero-title { font-size: 4rem; font-weight: 700; margin-bottom: 20px; }
.hero-subtitle { font-size: 1.5rem; margin-bottom: 15px; opacity: 0.9; }
.hero-description { font-size: 1.1rem; margin-bottom: 40px; opacity: 0.8; }
.hero-buttons { display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; }
.btn-hero { padding: 15px 40px; background: white; color: #667eea; text-decoration: none;
border-radius: 50px; font-weight: 600; transition: transform 0.3s, box-shadow 0.3s; }
.btn-hero:hover { transform: translateY(-2px); box-shadow: 0 10px 30px rgba(0,0,0,0.2); }
.btn-hero.secondary { background: transparent; border: 2px solid white; color: white; }
/* Plans */
.plans-section { padding: 100px 20px; background: #f8f9fa; }
.container-large { max-width: 1200px; margin: 0 auto; }
.section-title { text-align: center; font-size: 2.5rem; margin-bottom: 60px; color: #333; }
.plans-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; }
.plan-card { background: white; padding: 40px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.1);
transition: transform 0.3s; }
.plan-card:hover { transform: translateY(-5px); }
.plan-card h3 { font-size: 2rem; margin-bottom: 10px; color: #667eea; }
.plan-card .price { font-size: 3rem; font-weight: 700; color: #333; margin: 20px 0; }
.plan-card ul { list-style: none; margin: 30px 0; }
.plan-card li { padding: 10px 0; border-bottom: 1px solid #eee; }
.plan-card li:before { content: ""; margin-right: 10px; }
.btn-plan { width: 100%; padding: 15px; background: #667eea; color: white; border: none;
border-radius: 8px; font-size: 1.1rem; font-weight: 600; cursor: pointer; margin-top: 20px; }
.btn-plan:hover { background: #5568d3; }
/* Contact */
.contact-section { padding: 100px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
.contact-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; margin-top: 50px; }
.contact-card { background: rgba(255,255,255,0.1); padding: 30px; border-radius: 15px; text-align: center;
backdrop-filter: blur(10px); }
.contact-card i { font-size: 2.5rem; margin-bottom: 15px; }
/* WhatsApp Float */
.whatsapp-float { position: fixed; bottom: 30px; right: 30px; background: #25D366; color: white;
width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font-size: 1.5rem; box-shadow: 0 5px 20px rgba(37,211,102,0.4);
z-index: 1000; text-decoration: none; transition: transform 0.3s; }
.whatsapp-float:hover { transform: scale(1.1); }
/* Footer */
footer { background: #1a1a1a; color: white; padding: 40px 20px; text-align: center; }
@media (max-width: 768px) {
.hero-title { font-size: 2.5rem; }
.nav-menu { display: none; }
}
</style>
</head>
<body>
<!-- Nav -->
<nav class="nav-sticky">
<div class="nav-container">
<div class="nav-logo"><a href="/">PageBuilder SaaS</a></div>
<div class="nav-menu">
<a href="#planes" class="nav-link">Planes</a>
<a href="#contacto" class="nav-link">Contacto</a>
</div>
<div class="nav-social">
<a href="/login" class="nav-link">Login</a>
<a href="/register" class="nav-link" style="background: #667eea; color: white; padding: 8px 20px; border-radius: 20px;">Registrarse</a>
</div>
</div>
</nav>
<!-- Hero -->
<section class="hero-section">
<div class="hero-content">
<h1 class="hero-title">🎨 PageBuilder SaaS</h1>
<p class="hero-subtitle">Crea tu sitio web automatizado</p>
<p class="hero-description">Sistema profesional y automatizado. Cada cliente con su admin, templates y base de datos.</p>
<div class="hero-buttons">
<a href="/register" class="btn-hero">Comenzar Gratis</a>
<a href="#planes" class="btn-hero secondary">Ver Planes</a>
</div>
</div>
</section>
<!-- Plans -->
<section id="planes" class="plans-section">
<div class="container-large">
<h2 class="section-title">Nuestros Planes</h2>
<div class="plans-grid">
<div class="plan-card">
<h3>Base</h3>
<div class="price">$50</div>
<ul>
<li>Sitio web de una página</li>
<li>Diseño profesional</li>
<li>Responsive móvil</li>
<li>Panel de administración</li>
<li>Base de datos propia</li>
</ul>
<button class="btn-plan" onclick="window.location.href='/register?plan=base'">Elegir Base</button>
</div>
<div class="plan-card">
<h3>Pro</h3>
<div class="price">$100</div>
<ul>
<li>Sitio multipágina</li>
<li>Animaciones y efectos</li>
<li>Galería de imágenes</li>
<li>Formularios avanzados</li>
<li>Customizer completo</li>
</ul>
<button class="btn-plan" onclick="window.location.href='/register?plan=pro'">Elegir Pro</button>
</div>
<div class="plan-card">
<h3>Premium</h3>
<div class="price">$200</div>
<ul>
<li>Sitio completo escalable</li>
<li>SEO optimizado</li>
<li>Panel admin avanzado</li>
<li>Integraciones</li>
<li>Soporte prioritario</li>
</ul>
<button class="btn-plan" onclick="window.location.href='/register?plan=premium'">Elegir Premium</button>
</div>
</div>
</div>
</section>
<!-- Contact -->
<section id="contacto" class="contact-section">
<div class="container-large">
<h2 class="section-title" style="color: white;">Contacto</h2>
<div class="contact-grid">
<div class="contact-card">
<i class="fas fa-envelope"></i>
<h3>Email</h3>
<p>contacto@pagebuilder.com</p>
</div>
<div class="contact-card">
<i class="fas fa-phone"></i>
<h3>Teléfono</h3>
<p>+54 9 11 2345 6789</p>
</div>
<div class="contact-card">
<i class="fab fa-whatsapp"></i>
<h3>WhatsApp</h3>
<p>Chatea con nosotros</p>
</div>
</div>
</div>
</section>
<!-- WhatsApp Float -->
<a href="https://wa.me/5491123456789" target="_blank" class="whatsapp-float">
<i class="fab fa-whatsapp"></i>
</a>
<!-- Footer -->
<footer>
<p>&copy; 2025 PageBuilder SaaS. Todos los derechos reservados.</p>
<p style="margin-top: 10px; opacity: 0.7; font-size: 0.9rem;">Sistema profesional y automatizado - GKACHELE™</p>
</footer>
<script>
// Smooth scroll
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
</script>
</body>
</html>
'''
@app.route('/')
def home():
# Usar tu landing real EXACTA
return render_template('landing_real.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
data = request.get_json()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
try:
c.execute('INSERT INTO users (email, password, plan, rubro) VALUES (?, ?, ?, ?)',
(data['email'], generate_password_hash(data['password']),
data.get('plan', 'base'), data.get('rubro', 'gimnasio')))
user_id = c.lastrowid
conn.commit()
session['user_id'] = user_id
return jsonify({'success': True, 'user_id': user_id})
except:
return jsonify({'error': 'Email existe'}), 400
finally:
conn.close()
return render_template_string('''
<!DOCTYPE html>
<html><head><title>Registro</title>
<style>body { font-family: Arial; padding: 40px; max-width: 400px; margin: 0 auto; }
input { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; }
button { width: 100%; padding: 12px; background: #667eea; color: white; border: none; border-radius: 5px; }
</style></head>
<body>
<h1>Registro</h1>
<form id="form">
<input type="email" id="email" placeholder="Email" required>
<input type="password" id="password" placeholder="Password" required>
<button type="submit">Registrarse</button>
</form>
<script>
document.getElementById('form').onsubmit = async (e) => {
e.preventDefault();
const res = await fetch('/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: document.getElementById('email').value,
password: document.getElementById('password').value})
});
const data = await res.json();
if (data.success) window.location.href = '/dashboard';
else alert(data.error);
};
</script>
</body></html>
''')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
data = request.get_json()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT id, password FROM users WHERE email = ?', (data['email'],))
user = c.fetchone()
conn.close()
if user and check_password_hash(user[1], data['password']):
session['user_id'] = user[0]
return jsonify({'success': True})
return jsonify({'error': 'Credenciales inválidas'}), 401
return render_template_string('''
<!DOCTYPE html>
<html><head><title>Login</title>
<style>body { font-family: Arial; padding: 40px; max-width: 400px; margin: 0 auto; }
input { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; }
button { width: 100%; padding: 12px; background: #667eea; color: white; border: none; border-radius: 5px; }
</style></head>
<body>
<h1>Login</h1>
<form id="form">
<input type="email" id="email" placeholder="Email" required>
<input type="password" id="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
<script>
document.getElementById('form').onsubmit = async (e) => {
e.preventDefault();
const res = await fetch('/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: document.getElementById('email').value,
password: document.getElementById('password').value})
});
const data = await res.json();
if (data.success) window.location.href = '/dashboard';
else alert(data.error);
};
</script>
</body></html>
''')
@app.route('/dashboard')
def dashboard():
if 'user_id' not in session:
return redirect('/login')
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT id, slug, status FROM sites WHERE user_id = ?', (session['user_id'],))
sites = c.fetchall()
conn.close()
sites_html = ''.join([f'<div style="padding: 15px; background: #f5f5f5; margin: 10px 0; border-radius: 5px;"><h3>{s[1]}</h3><p>Status: {s[2]}</p><a href="/customizer/{s[0]}" style="padding: 8px 15px; background: #667eea; color: white; text-decoration: none; border-radius: 3px;">Editar</a></div>' for s in sites])
return render_template_string(f'''
<!DOCTYPE html>
<html><head><title>Dashboard</title>
<style>body {{ font-family: Arial; padding: 20px; max-width: 800px; margin: 0 auto; }}
.header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }}
.btn {{ padding: 10px 20px; background: #667eea; color: white; text-decoration: none; border-radius: 5px; }}
</style></head>
<body>
<div class="header">
<h1>📊 Dashboard</h1>
<a href="/create" class="btn"> Crear Sitio</a>
</div>
{sites_html if sites else '<p>No tienes sitios. <a href="/create">Crear uno</a></p>'}
</body></html>
''')
@app.route('/create', methods=['GET', 'POST'])
def create():
if 'user_id' not in session:
return redirect('/login')
if request.method == 'POST':
data = request.get_json()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
content = json.dumps({
'site_name': data.get('site_name', 'Mi Sitio'),
'hero_title': data.get('hero_title', 'Bienvenido'),
'colors': {'primary': '#ff4d4d', 'secondary': '#1a1a1a'}
})
c.execute('INSERT INTO sites (user_id, slug, content) VALUES (?, ?, ?)',
(session['user_id'], data['slug'], content))
site_id = c.lastrowid
conn.commit()
conn.close()
return jsonify({'success': True, 'site_id': site_id})
return render_template_string('''
<!DOCTYPE html>
<html><head><title>Crear Sitio</title>
<style>body { font-family: Arial; padding: 40px; max-width: 600px; margin: 0 auto; }
input, textarea { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; }
button { width: 100%; padding: 12px; background: #667eea; color: white; border: none; border-radius: 5px; }
</style></head>
<body>
<h1> Crear Sitio</h1>
<form id="form">
<input type="text" id="site_name" placeholder="Nombre" required>
<input type="text" id="slug" placeholder="Slug (URL)" required>
<input type="text" id="hero_title" placeholder="Título" required>
<button type="submit">Crear</button>
</form>
<script>
document.getElementById('form').onsubmit = async (e) => {
e.preventDefault();
const res = await fetch('/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
site_name: document.getElementById('site_name').value,
slug: document.getElementById('slug').value,
hero_title: document.getElementById('hero_title').value
})
});
const data = await res.json();
if (data.success) window.location.href = `/customizer/${data.site_id}`;
};
</script>
</body></html>
''')
@app.route('/customizer/<int:site_id>')
def customizer(site_id):
if 'user_id' not in session:
return redirect('/login')
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT content FROM sites WHERE id = ? AND user_id = ?', (site_id, session['user_id']))
site = c.fetchone()
conn.close()
if not site:
return "No encontrado", 404
content = json.loads(site[0])
return render_template_string(f'''
<!DOCTYPE html>
<html><head><title>Customizer</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: Arial; display: flex; height: 100vh; }}
.sidebar {{ width: 300px; background: #f5f5f5; padding: 20px; overflow-y: auto; }}
.preview {{ flex: 1; padding: 40px; background: white; overflow-y: auto; }}
input, select {{ width: 100%; padding: 8px; margin: 10px 0; border: 1px solid #ddd; border-radius: 3px; }}
button {{ padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px; }}
</style>
</head>
<body>
<div class="sidebar">
<h2>⚙️ Personalizar</h2>
<input type="text" id="hero_title" value="{content.get('hero_title', '')}" placeholder="Título">
<input type="color" id="color_primary" value="{content.get('colors', {}).get('primary', '#ff4d4d')}">
<button onclick="save()">💾 Guardar</button>
<button onclick="submit()" style="background: #ff4d4d;">📤 Enviar</button>
</div>
<div class="preview">
<div id="preview">
<h1 id="preview_title" style="color: {content.get('colors', {}).get('primary', '#ff4d4d')};">
{content.get('hero_title', 'Título')}
</h1>
</div>
</div>
<script>
const siteId = {site_id};
document.getElementById('hero_title').addEventListener('input', update);
document.getElementById('color_primary').addEventListener('change', update);
function update() {{
const title = document.getElementById('hero_title').value;
const color = document.getElementById('color_primary').value;
document.getElementById('preview_title').textContent = title;
document.getElementById('preview_title').style.color = color;
}}
function save() {{
const content = {{
hero_title: document.getElementById('hero_title').value,
colors: {{primary: document.getElementById('color_primary').value}}
}};
fetch('/api/save/' + siteId, {{
method: 'POST',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{content: content}})
}}).then(() => alert('✅ Guardado'));
}}
function submit() {{
fetch('/api/submit/' + siteId, {{method: 'POST'}})
.then(() => {{alert('✅ Enviado'); window.location.href = '/dashboard';}});
}}
</script>
</body></html>
''')
@app.route('/api/save/<int:site_id>', methods=['POST'])
def save(site_id):
if 'user_id' not in session:
return jsonify({'error': 'No autorizado'}), 401
data = request.get_json()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('UPDATE sites SET content = ? WHERE id = ? AND user_id = ?',
(json.dumps(data['content']), site_id, session['user_id']))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/submit/<int:site_id>', methods=['POST'])
def submit(site_id):
if 'user_id' not in session:
return jsonify({'error': 'No autorizado'}), 401
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('UPDATE sites SET status = ? WHERE id = ? AND user_id = ?',
('pending', site_id, session['user_id']))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/admin')
def admin():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT id, slug, status FROM sites WHERE status = ?', ('pending',))
requests = c.fetchall()
c.execute('SELECT id, slug, status FROM sites')
sites = c.fetchall()
conn.close()
return render_template_string(f'''
<!DOCTYPE html>
<html><head><title>Admin</title>
<style>body {{ font-family: Arial; padding: 20px; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ padding: 12px; border: 1px solid #ddd; }}
th {{ background: #667eea; color: white; }}
button {{ padding: 5px 15px; background: #4caf50; color: white; border: none; border-radius: 3px; cursor: pointer; }}
</style></head>
<body>
<h1>🔧 Admin</h1>
<h2>Solicitudes Pendientes</h2>
<table>
<tr><th>ID</th><th>Slug</th><th>Acción</th></tr>
{''.join([f'<tr><td>{r[0]}</td><td>{r[1]}</td><td><button onclick="approve({r[0]})">✅ Aprobar</button></td></tr>' for r in requests])}
</table>
<h2>Todos los Sitios</h2>
<table>
<tr><th>ID</th><th>Slug</th><th>Status</th></tr>
{''.join([f'<tr><td>{s[0]}</td><td>{s[1]}</td><td>{s[2]}</td></tr>' for s in sites])}
</table>
<script>
function approve(id) {{
fetch('/api/approve/' + id, {{method: 'POST'}})
.then(() => {{alert('✅ Aprobado'); location.reload();}});
}}
</script>
</body></html>
''')
@app.route('/api/approve/<int:site_id>', methods=['POST'])
def approve(site_id):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('UPDATE sites SET status = ? WHERE id = ?', ('published', site_id))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/site/<slug>')
def site(slug):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT content FROM sites WHERE slug = ? AND status = ?', (slug, 'published'))
site = c.fetchone()
conn.close()
if not site:
return "Sitio no encontrado", 404
content = json.loads(site[0])
return render_template_string(f'''
<!DOCTYPE html>
<html><head><title>{content.get('site_name', 'Sitio')}</title>
<style>body {{ font-family: Arial; padding: 40px; max-width: 800px; margin: 0 auto; }}
h1 {{ color: {content.get('colors', {}).get('primary', '#ff4d4d')}; }}
</style></head>
<body>
<h1>{content.get('hero_title', 'Título')}</h1>
<p>{content.get('site_name', '')}</p>
</body></html>
''')
if __name__ == '__main__':
print("🚀 Demo SaaS SIMPLE iniciado")
print("📍 http://localhost:5001")
app.run(debug=True, host='0.0.0.0', port=5001)

Binary file not shown.

View File

@@ -0,0 +1,66 @@
"""
GKACHELE™ - Script para limpiar base de datos
© 2025 GKACHELE™. Todos los derechos reservados.
Uso: python limpiar_db.py
"""
import sqlite3
import os
BASE_DIR = os.path.dirname(__file__)
DATABASE_DIR = os.path.join(BASE_DIR, 'database')
MAIN_DB = os.path.join(DATABASE_DIR, 'main.db')
def limpiar_db():
"""Limpiar todas las tablas de la base de datos"""
if not os.path.exists(MAIN_DB):
print(f"❌ No se encontró la base de datos en: {MAIN_DB}")
return
try:
conn = sqlite3.connect(MAIN_DB)
c = conn.cursor()
# Mostrar datos actuales
c.execute('SELECT COUNT(*) FROM users')
user_count = c.fetchone()[0]
c.execute('SELECT COUNT(*) FROM sites')
site_count = c.fetchone()[0]
print("=" * 80)
print("📊 ESTADO ACTUAL DE LA BASE DE DATOS")
print("=" * 80)
print(f"Usuarios: {user_count}")
print(f"Sitios: {site_count}")
print("=" * 80)
respuesta = input("\n⚠️ ¿Estás seguro de que quieres limpiar TODA la base de datos? (escribe 'SI' para confirmar): ")
if respuesta.upper() != 'SI':
print("❌ Operación cancelada")
conn.close()
return
# Limpiar en orden (respetando foreign keys)
print("\n🗑️ Limpiando base de datos...")
c.execute('DELETE FROM widgets')
c.execute('DELETE FROM menus')
c.execute('DELETE FROM media')
c.execute('DELETE FROM content')
c.execute('DELETE FROM settings')
c.execute('DELETE FROM requests')
c.execute('DELETE FROM sites')
c.execute('DELETE FROM users')
conn.commit()
conn.close()
print("✅ Base de datos limpiada exitosamente")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
limpiar_db()

View File

@@ -0,0 +1,2 @@
Flask==2.3.3
Werkzeug==2.3.7

View File

@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")"
echo "🚀 Iniciando Demo SaaS..."
echo ""
python3 app.py

View File

@@ -0,0 +1,8 @@
@echo off
echo Instalando dependencias...
pip install Flask Werkzeug --quiet
echo.
echo Iniciando servidor en puerto 5001...
echo.
python app.py
pause

View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Script para iniciar el demo desde WSL
echo "🚀 Iniciando Demo SaaS..."
echo ""
# Verificar Python
if ! command -v python3 &> /dev/null; then
echo "❌ Python3 no encontrado"
exit 1
fi
# Instalar dependencias si no están
if ! python3 -c "import flask" 2>/dev/null; then
echo "📦 Instalando dependencias..."
pip3 install Flask Werkzeug --quiet
fi
echo ""
echo "✅ Todo listo"
echo "📍 Servidor en: http://localhost:5001"
echo ""
echo "Presiona Ctrl+C para detener"
echo ""
# Ejecutar
python3 app.py

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Admin - Demo</title>
<style>
* { 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 {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #667eea;
color: white;
}
.btn {
padding: 5px 15px;
background: #4caf50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="header">
<h1>🔧 Panel Admin</h1>
</div>
<h2>Solicitudes Pendientes</h2>
<table>
<tr>
<th>ID</th>
<th>Slug</th>
<th>Email Cliente</th>
<th>Fecha</th>
<th>Acción</th>
</tr>
{% for req in requests %}
<tr>
<td>{{ req.id }}</td>
<td>{{ req.slug }}</td>
<td>{{ req.email }}</td>
<td>{{ req.created_at }}</td>
<td>
<button class="btn" onclick="approve({{ req.id }})">✅ Aprobar</button>
</td>
</tr>
{% endfor %}
{% if not requests %}
<tr><td colspan="5">No hay solicitudes pendientes</td></tr>
{% endif %}
</table>
<h2>Todos los Sitios</h2>
<table>
<tr>
<th>ID</th>
<th>Slug</th>
<th>Tema</th>
<th>Status</th>
<th>Ver</th>
</tr>
{% for site in sites %}
<tr>
<td>{{ site.id }}</td>
<td>{{ site.slug }}</td>
<td>{{ site.theme }}</td>
<td>{{ site.status }}</td>
<td>
{% if site.status == 'published' %}
<a href="/site/{{ site.slug }}" target="_blank">Ver</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
<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();
}
});
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,311 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Admin - Cliente</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f0f0f1;
padding: 20px;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #1d2327;
font-size: 24px;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.tab {
padding: 10px 20px;
background: transparent;
border: none;
cursor: pointer;
border-bottom: 2px solid transparent;
font-size: 14px;
color: #50575e;
}
.tab.active {
color: #2271b1;
border-bottom-color: #2271b1;
}
.tab-content {
display: none;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.tab-content.active {
display: block;
}
.btn {
padding: 8px 16px;
background: #2271b1;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
display: inline-block;
}
.btn:hover { background: #135e96; }
.btn-danger { background: #d63638; }
.btn-danger:hover { background: #b32d2e; }
.btn-success { background: #00a32a; }
.btn-success:hover { background: #008a20; }
.upload-area {
border: 2px dashed #c3c4c7;
border-radius: 4px;
padding: 40px;
text-align: center;
margin: 20px 0;
background: #f6f7f7;
}
.upload-area:hover {
border-color: #2271b1;
background: #f0f6fc;
}
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.media-item {
position: relative;
border: 1px solid #c3c4c7;
border-radius: 4px;
overflow: hidden;
background: #fff;
}
.media-item img {
width: 100%;
height: 150px;
object-fit: cover;
}
.media-item .actions {
padding: 8px;
display: flex;
gap: 5px;
background: #f6f7f7;
}
.media-item .actions button {
flex: 1;
padding: 5px;
font-size: 12px;
}
input[type="file"] {
display: none;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #1d2327;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #8c8f94;
border-radius: 4px;
font-size: 14px;
}
.form-group input:focus,
.form-group textarea:focus {
border-color: #2271b1;
outline: none;
box-shadow: 0 0 0 1px #2271b1;
}
</style>
</head>
<body>
<div class="header">
<h1>⚙️ Panel de Administración</h1>
<div>
<a href="/dashboard" class="btn">← Volver al Dashboard</a>
<a href="/logout" class="btn btn-danger">Salir</a>
</div>
</div>
<div class="tabs">
<button class="tab active" onclick="showTab('media')">📷 Media</button>
<button class="tab" onclick="showTab('settings')">⚙️ Configuración</button>
<button class="tab" onclick="showTab('sites')">🌐 Mis Sitios</button>
</div>
<!-- Tab Media -->
<div id="tab-media" class="tab-content active">
<h2>Gestionar Imágenes</h2>
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
<p style="font-size: 18px; color: #50575e; margin-bottom: 10px;">📤 Arrastra imágenes aquí o haz clic para subir</p>
<p style="font-size: 12px; color: #8c8f94;">Formatos: JPG, PNG, GIF (máx. 5MB)</p>
<input type="file" id="fileInput" multiple accept="image/*" onchange="uploadFiles(this.files)">
</div>
<div id="mediaGrid" class="media-grid">
<!-- Las imágenes se cargarán aquí -->
</div>
</div>
<!-- Tab Configuración -->
<div id="tab-settings" class="tab-content">
<h2>Configuración de Cuenta</h2>
<form id="settingsForm">
<div class="form-group">
<label>Email</label>
<input type="email" id="user_email" value="{{ user_email }}" readonly>
</div>
<div class="form-group">
<label>Plan</label>
<input type="text" id="user_plan" value="{{ user_plan }}" readonly>
</div>
<div class="form-group">
<label>Cambiar Contraseña</label>
<input type="password" id="new_password" placeholder="Nueva contraseña">
</div>
<button type="submit" class="btn btn-success">💾 Guardar Cambios</button>
</form>
</div>
<!-- Tab Sitios -->
<div id="tab-sites" class="tab-content">
<h2>Mis Sitios</h2>
<div style="display: grid; gap: 15px; margin-top: 20px;">
{% for site in sites %}
<div style="background: #f6f7f7; padding: 15px; border-radius: 4px; border: 1px solid #c3c4c7;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h3 style="margin-bottom: 5px;">{{ site.slug }}</h3>
<p style="color: #50575e; font-size: 14px;">Tema: {{ site.theme }} | Estado: <span style="padding: 3px 8px; background: {% if site.status == 'published' %}#00a32a{% elif site.status == 'pending' %}#dba617{% else %}#d63638{% endif %}; color: white; border-radius: 3px; font-size: 12px;">{{ site.status }}</span></p>
</div>
<div>
<a href="/customizer/{{ site.id }}" class="btn">✏️ Editar</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
function showTab(tabName) {
// Ocultar todos los tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab').forEach(btn => {
btn.classList.remove('active');
});
// Mostrar el tab seleccionado
document.getElementById('tab-' + tabName).classList.add('active');
event.target.classList.add('active');
}
function uploadFiles(files) {
const formData = new FormData();
for (let file of files) {
formData.append('files', file);
}
fetch('/api/admin/upload', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('✅ Imágenes subidas correctamente');
loadMedia();
} else {
alert('❌ Error: ' + data.error);
}
});
}
function loadMedia() {
fetch('/api/admin/media')
.then(r => r.json())
.then(data => {
const grid = document.getElementById('mediaGrid');
grid.innerHTML = '';
data.media.forEach(item => {
const div = document.createElement('div');
div.className = 'media-item';
div.innerHTML = `
<img src="/uploads/${item.filename}" alt="${item.filename}">
<div class="actions">
<button class="btn" onclick="copyUrl('${item.filename}')">📋 URL</button>
<button class="btn btn-danger" onclick="deleteMedia(${item.id})">🗑️</button>
</div>
`;
grid.appendChild(div);
});
});
}
function copyUrl(filename) {
const url = window.location.origin + '/uploads/' + filename;
navigator.clipboard.writeText(url);
alert('✅ URL copiada: ' + url);
}
function deleteMedia(id) {
if (confirm('¿Eliminar esta imagen?')) {
fetch(`/api/admin/media/${id}`, {method: 'DELETE'})
.then(r => r.json())
.then(data => {
if (data.success) {
loadMedia();
}
});
}
}
// Cargar media al iniciar
loadMedia();
// Guardar configuración
document.getElementById('settingsForm').addEventListener('submit', (e) => {
e.preventDefault();
const password = document.getElementById('new_password').value;
if (password) {
fetch('/api/admin/settings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({password: password})
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('✅ Contraseña actualizada');
document.getElementById('new_password').value = '';
}
});
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Crear Sitio - Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
}
h1 { margin-bottom: 20px; }
input, select {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.btn {
padding: 12px 30px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.btn:hover { background: #5568d3; }
</style>
</head>
<body>
<div class="container">
<h1> Crear Nuevo Sitio</h1>
<form id="createForm">
<input type="text" id="site_name" placeholder="Nombre del sitio" required>
<input type="text" id="slug" placeholder="Slug (URL)" required>
<input type="text" id="hero_title" placeholder="Título principal" required>
<textarea id="hero_description" placeholder="Descripción" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 15px;"></textarea>
<select id="theme">
<option value="default">Tema Default</option>
<option value="modern">Tema Moderno</option>
</select>
<button type="submit" class="btn">Crear Sitio</button>
</form>
<a href="/dashboard" style="display: inline-block; margin-top: 15px; color: #667eea;">← Volver</a>
</div>
<script>
document.getElementById('createForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
site_name: document.getElementById('site_name').value,
slug: document.getElementById('slug').value,
hero_title: document.getElementById('hero_title').value,
hero_description: document.getElementById('hero_description').value,
theme: document.getElementById('theme').value
};
const res = await fetch('/dashboard/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
window.location.href = `/customizer/${result.site_id}`;
} else {
alert(result.error || 'Error al crear sitio');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,467 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Personalizar - {{ site_name or 'Sitio' }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
display: flex;
height: 100vh;
overflow: hidden;
background: #f0f0f1;
}
/* Sidebar de personalización */
.sidebar {
width: 350px;
background: #fff;
border-right: 1px solid #c3c4c7;
overflow-y: auto;
display: flex;
flex-direction: column;
box-shadow: 2px 0 5px rgba(0,0,0,0.05);
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid #c3c4c7;
background: #f6f7f7;
}
.sidebar-header h2 {
font-size: 20px;
color: #1d2327;
font-weight: 600;
margin: 0;
}
.sidebar-content {
flex: 1;
padding: 20px;
}
.control-section {
margin-bottom: 30px;
border-bottom: 1px solid #c3c4c7;
padding-bottom: 20px;
}
.control-section:last-child {
border-bottom: none;
}
.control-section h3 {
font-size: 13px;
text-transform: uppercase;
color: #50575e;
font-weight: 600;
margin-bottom: 15px;
letter-spacing: 0.5px;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
font-size: 13px;
font-weight: 600;
color: #1d2327;
margin-bottom: 8px;
}
.control-group input[type="text"],
.control-group textarea,
.control-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #8c8f94;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
.control-group input[type="text"]:focus,
.control-group textarea:focus,
.control-group select:focus {
outline: none;
border-color: #2271b1;
box-shadow: 0 0 0 1px #2271b1;
}
.control-group textarea {
resize: vertical;
min-height: 80px;
}
.color-picker-group {
display: flex;
align-items: center;
gap: 10px;
}
.color-picker-group input[type="color"] {
width: 50px;
height: 40px;
border: 1px solid #8c8f94;
border-radius: 4px;
cursor: pointer;
padding: 2px;
}
.color-picker-group input[type="text"] {
flex: 1;
}
.preview-container {
flex: 1;
background: #fff;
position: relative;
display: flex;
flex-direction: column;
}
.preview-header {
padding: 15px 20px;
background: #f6f7f7;
border-bottom: 1px solid #c3c4c7;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-header h3 {
font-size: 14px;
color: #1d2327;
font-weight: 600;
}
.preview-actions {
display: flex;
gap: 10px;
}
.preview {
flex: 1;
overflow: hidden;
position: relative;
}
.preview iframe {
width: 100%;
height: 100%;
border: none;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #2271b1;
color: #fff;
}
.btn-primary:hover {
background: #135e96;
}
.btn-success {
background: #00a32a;
color: #fff;
}
.btn-success:hover {
background: #008a20;
}
.btn-danger {
background: #d63638;
color: #fff;
}
.btn-danger:hover {
background: #b32d2e;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid #c3c4c7;
background: #f6f7f7;
display: flex;
flex-direction: column;
gap: 10px;
}
.btn-block {
width: 100%;
text-align: center;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #50575e;
font-size: 14px;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
background: #00a32a;
color: #fff;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 10000;
display: none;
}
.notification.show {
display: block;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<h2>⚙️ Personalizar Sitio</h2>
</div>
<div class="sidebar-content">
<!-- Contenido -->
<div class="control-section">
<h3>Contenido</h3>
<div class="control-group">
<label>Nombre del Sitio</label>
<input type="text" id="site_name" placeholder="Mi Restaurante" value="{{ content.site_name or '' }}">
</div>
<div class="control-group">
<label>Título Principal</label>
<input type="text" id="hero_title" placeholder="Bienvenido" value="{{ content.hero_title or '' }}">
</div>
<div class="control-group">
<label>Descripción</label>
<textarea id="hero_description" placeholder="Descripción de tu restaurante...">{{ content.hero_description or '' }}</textarea>
</div>
</div>
<!-- Colores -->
<div class="control-section">
<h3>Colores</h3>
<div class="control-group">
<label>Color Primario</label>
<div class="color-picker-group">
<input type="color" id="color_primary" value="{{ content.colors.primary if content.colors else '#d32f2f' }}">
<input type="text" id="color_primary_text" value="{{ content.colors.primary if content.colors else '#d32f2f' }}" readonly>
</div>
</div>
<div class="control-group">
<label>Color Secundario</label>
<div class="color-picker-group">
<input type="color" id="color_secondary" value="{{ content.colors.secondary if content.colors else '#ff6f00' }}">
<input type="text" id="color_secondary_text" value="{{ content.colors.secondary if content.colors else '#ff6f00' }}" readonly>
</div>
</div>
{% if theme == 'restaurante-moderno' or theme == 'restaurante-elegante' %}
<div class="control-group">
<label>Color Acento</label>
<div class="color-picker-group">
<input type="color" id="color_accent" value="{{ content.colors.accent if content.colors and content.colors.accent else '#ff8f00' }}">
<input type="text" id="color_accent_text" value="{{ content.colors.accent if content.colors and content.colors.accent else '#ff8f00' }}" readonly>
</div>
</div>
{% endif %}
<div class="control-group">
<label>Color de Texto</label>
<div class="color-picker-group">
<input type="color" id="color_text" value="{{ content.colors.text if content.colors else '#2c2c2c' }}">
<input type="text" id="color_text_text" value="{{ content.colors.text if content.colors else '#2c2c2c' }}" readonly>
</div>
</div>
</div>
<!-- Tipografía -->
<div class="control-section">
<h3>Tipografía</h3>
<div class="control-group">
<label>Familia de Fuente</label>
<select id="font_family">
<option value="Roboto" {% if content.typography and content.typography.font_family == 'Roboto' %}selected{% endif %}>Roboto</option>
<option value="Georgia" {% if content.typography and content.typography.font_family == 'Georgia' %}selected{% endif %}>Georgia</option>
<option value="Playfair Display" {% if content.typography and content.typography.font_family == 'Playfair Display' %}selected{% endif %}>Playfair Display</option>
<option value="Cormorant Garamond" {% if content.typography and content.typography.font_family == 'Cormorant Garamond' %}selected{% endif %}>Cormorant Garamond</option>
<option value="Arial" {% if content.typography and content.typography.font_family == 'Arial' %}selected{% endif %}>Arial</option>
</select>
</div>
</div>
<!-- Menús -->
<div class="control-section">
<h3>Menús</h3>
<div class="control-group">
<label>Gestionar Menús</label>
<button class="btn" style="background: #f0f0f1; color: #1d2327; width: 100%; margin-top: 5px;" onclick="gestionarMenus()">📋 Gestionar Menús</button>
<p style="font-size: 11px; color: #50575e; margin-top: 5px;">Configura menús para header, footer y sidebar</p>
</div>
</div>
</div>
<div class="sidebar-footer">
<button class="btn btn-primary btn-block" onclick="saveChanges()">💾 Guardar Cambios</button>
<button class="btn btn-success btn-block" onclick="submitSite()">📤 Enviar para Aprobación</button>
<a href="/dashboard" class="btn btn-block" style="background: #50575e; color: #fff; text-align: center;">← Volver al Dashboard</a>
</div>
</div>
<div class="preview-container">
<div class="preview-header">
<h3>Vista Previa</h3>
<div class="preview-actions">
<button class="btn" style="background: #f0f0f1; color: #1d2327;" onclick="refreshPreview()">🔄 Actualizar</button>
</div>
</div>
<div class="preview">
<div class="loading" id="loading">Cargando vista previa...</div>
<iframe id="preview-iframe" style="width: 100%; height: 100%; border: none;" src="/api/customizer/preview-frame/{{ site_id }}" onload="document.getElementById('loading').style.display='none'"></iframe>
</div>
</div>
<div class="notification" id="notification">✅ Cambios guardados</div>
<script>
const siteId = {{ site_id }};
const theme = '{{ theme }}';
// Sincronizar color picker con input de texto
function syncColorInputs() {
document.querySelectorAll('input[type="color"]').forEach(colorInput => {
const textInput = document.getElementById(colorInput.id + '_text');
if (textInput) {
colorInput.addEventListener('input', () => {
textInput.value = colorInput.value;
updatePreview();
});
}
});
}
syncColorInputs();
// Actualizar preview cuando cambian los campos
function updatePreview() {
const content = {
site_name: document.getElementById('site_name').value,
hero_title: document.getElementById('hero_title').value,
hero_description: document.getElementById('hero_description').value,
colors: {
primary: document.getElementById('color_primary').value,
secondary: document.getElementById('color_secondary').value,
text: document.getElementById('color_text').value
},
typography: {
font_family: document.getElementById('font_family').value
}
};
// Agregar accent si existe
const accentInput = document.getElementById('color_accent');
if (accentInput) {
content.colors.accent = accentInput.value;
}
// Guardar cambios y recargar iframe
fetch('/api/customizer/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({site_id: siteId, content: content})
})
.then(() => {
refreshPreview();
});
}
function refreshPreview() {
document.getElementById('loading').style.display = 'block';
document.getElementById('preview-iframe').src = '/api/customizer/preview-frame/' + siteId + '?t=' + Date.now();
}
function gestionarMenus() {
alert('📋 Gestión de menús próximamente disponible.\n\nPor ahora los menús se crean automáticamente al registrar.\nPuedes editarlos desde el código o esperar a la próxima actualización.');
}
// Escuchar cambios en los campos
document.querySelectorAll('#site_name, #hero_title, #hero_description, #color_primary, #color_secondary, #color_text, #font_family').forEach(el => {
el.addEventListener('input', updatePreview);
el.addEventListener('change', updatePreview);
});
const accentInput = document.getElementById('color_accent');
if (accentInput) {
accentInput.addEventListener('input', updatePreview);
accentInput.addEventListener('change', updatePreview);
}
function saveChanges() {
updatePreview();
showNotification('✅ Cambios guardados');
}
function submitSite() {
if (confirm('¿Enviar sitio para aprobación? Una vez enviado, esperarás la aprobación del administrador.')) {
fetch(`/dashboard/submit/${siteId}`, {method: 'POST'})
.then(r => r.json())
.then(data => {
if (data.success) {
showNotification('✅ Sitio enviado para aprobación');
setTimeout(() => {
window.location.href = '/dashboard';
}, 1500);
}
});
}
}
function showNotification(message) {
const notif = document.getElementById('notification');
notif.textContent = message;
notif.classList.add('show');
setTimeout(() => {
notif.classList.remove('show');
}, 3000);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Dashboard - Demo</title>
<style>
* { 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;
display: flex;
justify-content: space-between;
align-items: center;
}
.btn {
padding: 10px 20px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 5px;
}
.sites {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.site-card {
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.status {
display: inline-block;
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
margin-top: 10px;
}
.status.draft { background: #ffc107; }
.status.pending { background: #ff9800; }
.status.published { background: #4caf50; }
</style>
</head>
<body>
<div class="header">
<h1>📊 Mi Panel de Control</h1>
<div>
<a href="/dashboard/admin" class="btn" style="background: #28a745;">⚙️ Admin</a>
<a href="/dashboard/create" class="btn"> Crear Sitio</a>
<a href="/logout" class="btn" style="background: #ff4d4d;">Salir</a>
</div>
</div>
<div class="sites">
{% for site in sites %}
<div class="site-card">
<h3>{{ site.slug }}</h3>
<p>Tema: {{ site.theme }}</p>
<span class="status {{ site.status }}">{{ site.status }}</span>
<div style="margin-top: 15px;">
<a href="/customizer/{{ site.id }}" class="btn">✏️ Editar</a>
{% if site.status == 'draft' %}
<button onclick="submitSite({{ site.id }})" class="btn" style="background: #ff9800; border: none; cursor: pointer;">📤 Enviar</button>
{% endif %}
</div>
</div>
{% endfor %}
{% if not sites %}
<div class="site-card">
<p>No tienes sitios aún. <a href="/dashboard/create">Crear uno</a></p>
</div>
{% endif %}
</div>
<script>
function submitSite(siteId) {
if (confirm('¿Enviar sitio para aprobación?')) {
fetch(`/dashboard/submit/${siteId}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'}
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('✅ Sitio enviado para aprobación');
location.reload();
} else {
alert('❌ Error: ' + (data.error || 'Error al enviar'));
}
})
.catch(err => {
console.error('Error:', err);
alert('❌ Error de conexión');
});
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>PageBuilder SaaS - Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
text-align: center;
max-width: 500px;
}
h1 { color: #333; margin-bottom: 20px; }
p { color: #666; margin-bottom: 30px; }
.btn {
display: inline-block;
padding: 12px 30px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 5px;
}
.btn:hover { background: #5568d3; }
</style>
</head>
<body>
<div class="container">
<h1>🎨 PageBuilder SaaS</h1>
<p>Crea tu sitio web en minutos</p>
<a href="/register" class="btn">Registrarse</a>
<a href="/login" class="btn">Iniciar Sesión</a>
<a href="/admin" class="btn" style="background: #ff4d4d;">Admin</a>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Login - Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
max-width: 400px;
width: 100%;
}
h1 { margin-bottom: 20px; color: #333; }
input {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.btn {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.btn:hover { background: #5568d3; }
a { color: #667eea; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<h1>🔐 Iniciar Sesión</h1>
<form id="loginForm">
<input type="email" id="email" placeholder="Email" required>
<input type="password" id="password" placeholder="Contraseña" required>
<button type="submit" class="btn">Iniciar Sesión</button>
</form>
<p style="text-align: center; margin-top: 15px;">
<a href="/register">¿No tienes cuenta? Regístrate</a>
</p>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
email: document.getElementById('email').value,
password: document.getElementById('password').value
};
const res = await fetch('/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
let result;
try {
result = await res.json();
} catch (e) {
const text = await res.text();
console.error('❌ Servidor devolvió HTML:', text.substring(0, 200));
alert('❌ Error del servidor. Revisa la consola.');
return;
}
if (result.success) {
window.location.href = result.redirect || '/dashboard';
} else {
alert('❌ ' + (result.error || 'Error al iniciar sesión'));
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{ content.site_name or 'Mi Sitio' }}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: {{ content.typography.font_family if content.typography else 'Arial' }};
background: white;
padding: 40px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
color: {{ content.colors.primary if content.colors else '#ff4d4d' }};
margin-bottom: 20px;
}
p {
color: {{ content.colors.text if content.colors else '#333' }};
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container">
<h1>{{ content.hero_title or 'Título' }}</h1>
<p>{{ content.hero_description or 'Descripción' }}</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Registro - Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
max-width: 400px;
width: 100%;
}
h1 { margin-bottom: 20px; color: #333; }
input, select {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.btn {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.btn:hover { background: #5568d3; }
a { color: #667eea; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<h1>📝 Registrarse</h1>
<form id="registerForm">
<input type="email" id="email" placeholder="Email" required>
<input type="password" id="password" placeholder="Contraseña" required>
<select id="plan">
<option value="base" {% if plan == 'base' %}selected{% endif %}>Base</option>
<option value="pro" {% if plan == 'pro' %}selected{% endif %}>Pro</option>
<option value="premium" {% if plan == 'premium' %}selected{% endif %}>Premium</option>
</select>
<select id="rubro">
<option value="gimnasio" {% if rubro == 'gimnasio' or rubro == 'gimnasios' %}selected{% endif %}>Gimnasio</option>
<option value="restaurante" {% if rubro == 'restaurante' %}selected{% endif %}>Restaurante</option>
<option value="danza" {% if rubro == 'danza' %}selected{% endif %}>Danza</option>
<option value="cosmeticos" {% if rubro == 'cosmeticos' %}selected{% endif %}>Cosméticos</option>
<option value="despachos" {% if rubro == 'despachos' %}selected{% endif %}>Despachos</option>
<option value="educacion" {% if rubro == 'educacion' %}selected{% endif %}>Educación</option>
<option value="tienda" {% if rubro == 'tienda' %}selected{% endif %}>Tienda</option>
</select>
<button type="submit" class="btn">Registrarse</button>
</form>
<p style="text-align: center; margin-top: 15px;">
<a href="/login">¿Ya tienes cuenta? Inicia sesión</a>
</p>
</div>
<script>
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const plan = document.getElementById('plan').value;
const rubro = document.getElementById('rubro').value;
if (!email || !password) {
alert('❌ Por favor completa email y contraseña');
return;
}
const data = {
email: email,
password: password,
plan: plan,
rubro: rubro
};
console.log('Enviando registro:', data);
try {
const res = await fetch('/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
console.log('Respuesta del servidor:', res.status, res.statusText);
let result;
try {
result = await res.json();
} catch (e) {
// Si no es JSON, el servidor devolvió HTML (error 500)
const text = await res.text();
console.error('❌ Servidor devolvió HTML en vez de JSON:', text.substring(0, 200));
alert('❌ Error del servidor. Revisa la consola para más detalles.');
return;
}
console.log('Resultado:', result);
if (result.success) {
// Registro exitoso - mostrar mensaje y redirigir al login
alert(result.message || '✅ Registro exitoso. Por favor inicia sesión.');
window.location.href = result.redirect || '/login';
} else {
alert('❌ Error: ' + (result.error || 'Error al registrarse'));
}
} catch (error) {
console.error('Error en registro:', error);
alert('❌ Error de conexión. Verifica la consola para más detalles.');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python3
"""Test rápido para verificar que todo funciona"""
import sys
import os
# Agregar directorio actual al path
sys.path.insert(0, os.path.dirname(__file__))
try:
from app import app
print("✅ App importada correctamente")
print("✅ Flask funcionando")
print(f"✅ Templates en: {app.template_folder}")
print("\n🚀 Para iniciar:")
print(" python3 app.py")
print("\n📍 URL: http://localhost:5001")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -0,0 +1,22 @@
</main>
<footer class="gk-footer" style="background: var(--text); color: var(--white); text-align: center; padding: 40px 20px; margin-top: 60px;">
<div style="max-width: 1200px; margin: 0 auto;">
{% if menus.footer %}
<nav style="margin-bottom: 20px;">
<ul style="display: flex; justify-content: center; list-style: none; gap: 20px; flex-wrap: wrap;">
{% for menu_item in menus.footer %}
<li><a href="{{ menu_item.url }}" style="color: var(--white); text-decoration: none;">{{ menu_item.title }}</a></li>
{% endfor %}
</ul>
</nav>
{% endif %}
<p style="margin: 0; opacity: 0.8;">
© 2025 GKACHELE™. Todos los derechos reservados.<br>
Desarrollado desde noviembre 2025 por GKACHELE<br>
Código propiedad de GKACHELE © 2025 - Prohibida su reproducción sin autorización
</p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,112 @@
<!--
GKACHELE™ Template System
© 2025 GKACHELE™. Todos los derechos reservados.
Desarrollado desde noviembre 2025 por GKACHELE
Código propiedad de GKACHELE © 2025 - Prohibida su reproducción sin autorización
-->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if site_name %}{{ site_name }}{% else %}GKACHELE Site{% endif %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family={{ typography.font_family|replace(' ', '+') }}:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: {{ colors.primary or '#d32f2f' }};
--secondary: {{ colors.secondary or '#ff6f00' }};
--text: {{ colors.text or '#2c2c2c' }};
--bg-light: #f9f9f9;
--white: #ffffff;
}
body {
font-family: '{{ typography.font_family or 'Roboto' }}', sans-serif;
color: var(--text);
line-height: 1.6;
}
/* Header / Navigation */
.gk-header {
background: var(--white);
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
position: fixed;
width: 100%;
top: 0;
z-index: 1000;
}
.gk-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 40px;
max-width: 1200px;
margin: 0 auto;
}
.gk-logo {
font-size: 28px;
color: var(--primary);
font-weight: 700;
text-decoration: none;
}
.gk-menu {
display: flex;
list-style: none;
gap: 30px;
}
.gk-menu a {
text-decoration: none;
color: var(--text);
font-weight: 500;
transition: color 0.3s;
}
.gk-menu a:hover {
color: var(--primary);
}
.gk-menu .current-menu-item a {
color: var(--primary);
}
/* Main Content */
.gk-main {
margin-top: 80px;
min-height: calc(100vh - 160px);
}
@media (max-width: 768px) {
.gk-menu {
display: none;
}
}
</style>
</head>
<body>
<header class="gk-header">
<nav class="gk-nav">
<a href="/" class="gk-logo">{{ site_name or 'GKACHELE Site' }}</a>
{% if menus %}
<ul class="gk-menu">
{% for menu_item in menus.header %}
<li class="{% if menu_item.current %}current-menu-item{% endif %}">
<a href="{{ menu_item.url }}">{{ menu_item.title }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</nav>
</header>
<main class="gk-main">

View File

@@ -0,0 +1,33 @@
<!--
GKACHELE™ Sidebar Widget Area
© 2025 GKACHELE™. Todos los derechos reservados.
-->
<aside class="gk-sidebar" style="width: 300px; padding: 20px; background: var(--bg-light);">
{% if widgets %}
{% for widget in widgets %}
<div class="gk-widget" style="margin-bottom: 30px; padding: 20px; background: var(--white); border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.05);">
{% if widget.title %}
<h3 style="color: var(--primary); margin-bottom: 15px; font-size: 18px;">{{ widget.title }}</h3>
{% endif %}
<div class="gk-widget-content">
{{ widget.content|safe }}
</div>
</div>
{% endfor %}
{% endif %}
{% if menus.sidebar %}
<div class="gk-widget gk-menu-widget" style="margin-bottom: 30px; padding: 20px; background: var(--white); border-radius: 5px;">
<h3 style="color: var(--primary); margin-bottom: 15px; font-size: 18px;">Menú</h3>
<ul style="list-style: none; padding: 0;">
{% for menu_item in menus.sidebar %}
<li style="margin-bottom: 10px;">
<a href="{{ menu_item.url }}" style="color: var(--text); text-decoration: none; transition: color 0.3s;">
{{ menu_item.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</aside>

View File

@@ -0,0 +1,29 @@
{
"name": "Gimnasio Claro",
"rubro": "gimnasios",
"description": "Tema claro y moderno para gimnasios",
"sections": [
"hero",
"planes_fitness",
"horarios",
"entrenadores",
"instalaciones",
"contacto"
],
"colors": {
"primary": "#ff4d4d",
"secondary": "#1a1a1a",
"accent": "#ff6b6b"
},
"typography": {
"font_family": "Roboto",
"headings": "Roboto"
},
"features": {
"planes_fitness": true,
"horarios": true,
"entrenadores": true,
"instalaciones": true,
"precios": true
}
}

View File

@@ -0,0 +1,32 @@
{
"name": "Restaurante Elegante",
"rubro": "restaurante",
"description": "Tema sofisticado y elegante para restaurantes de alta cocina",
"sections": [
"hero",
"menu",
"horarios",
"reservas",
"especialidad_culinaria",
"galeria",
"contacto"
],
"colors": {
"primary": "#8b4513",
"secondary": "#d4af37",
"accent": "#f5deb3",
"text": "#2c2c2c"
},
"typography": {
"font_family": "Georgia",
"headings": "Cormorant Garamond"
},
"features": {
"menu_url": true,
"horarios": true,
"reservas": true,
"capacidad": true,
"especialidad_culinaria": true,
"galeria": true
}
}

View File

@@ -0,0 +1,649 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name or 'Restaurante Elegante' }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Georgia:wght@400;700&family=Cormorant+Garamond:wght@400;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #8b4513;
--secondary: #d4af37;
--accent: #f5deb3;
--text-dark: #2c2c2c;
--text-light: #666;
--bg-cream: #faf8f3;
--white: #ffffff;
}
body {
font-family: 'Georgia', serif;
color: var(--text-dark);
line-height: 1.8;
background: var(--bg-cream);
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Cormorant Garamond', serif;
font-weight: 700;
line-height: 1.3;
letter-spacing: 1px;
}
.section {
padding: 100px 20px;
position: relative;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
/* Header Elegante */
header {
background: var(--white);
box-shadow: 0 2px 20px rgba(0,0,0,0.08);
position: fixed;
width: 100%;
top: 0;
z-index: 1000;
border-bottom: 3px solid var(--secondary);
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25px 50px;
}
.logo {
font-family: 'Cormorant Garamond', serif;
font-size: 32px;
color: var(--primary);
font-weight: 700;
letter-spacing: 2px;
}
.nav-links {
display: flex;
list-style: none;
gap: 40px;
}
.nav-links a {
text-decoration: none;
color: var(--text-dark);
font-weight: 500;
font-size: 15px;
letter-spacing: 0.5px;
transition: color 0.3s;
position: relative;
}
.nav-links a::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 0;
height: 2px;
background: var(--secondary);
transition: width 0.3s;
}
.nav-links a:hover::after {
width: 100%;
}
.nav-links a:hover {
color: var(--primary);
}
/* Hero Elegante */
.hero {
background: linear-gradient(rgba(139, 69, 19, 0.85), rgba(139, 69, 19, 0.85)),
url('https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1920') center/cover;
color: var(--white);
text-align: center;
padding: 200px 20px 150px;
margin-top: 90px;
}
.hero h1 {
font-size: 72px;
margin-bottom: 30px;
color: var(--white);
text-shadow: 2px 2px 10px rgba(0,0,0,0.3);
letter-spacing: 3px;
}
.hero p {
font-size: 24px;
margin-bottom: 40px;
opacity: 0.95;
font-style: italic;
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
.btn {
display: inline-block;
padding: 18px 50px;
background: var(--secondary);
color: var(--white);
text-decoration: none;
border-radius: 0;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
transition: all 0.3s;
border: 2px solid var(--secondary);
}
.btn:hover {
background: transparent;
color: var(--secondary);
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(212, 175, 55, 0.3);
}
/* Menu Elegante */
.menu {
background: var(--white);
}
.section-title {
text-align: center;
font-size: 48px;
color: var(--primary);
margin-bottom: 80px;
position: relative;
padding-bottom: 20px;
}
.section-title::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 3px;
background: var(--secondary);
}
.menu-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 40px;
margin-top: 60px;
}
.menu-item {
background: var(--bg-cream);
padding: 40px;
border: 1px solid #e8e5df;
transition: all 0.3s;
position: relative;
}
.menu-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 0;
background: var(--secondary);
transition: height 0.3s;
}
.menu-item:hover::before {
height: 100%;
}
.menu-item:hover {
transform: translateY(-5px);
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
border-color: var(--secondary);
}
.menu-item h3 {
color: var(--primary);
margin-bottom: 15px;
font-size: 28px;
}
.menu-item p {
color: var(--text-light);
margin-bottom: 20px;
font-style: italic;
line-height: 1.8;
}
.menu-price {
font-size: 28px;
font-weight: 700;
color: var(--secondary);
font-family: 'Cormorant Garamond', serif;
}
/* Horarios Elegantes */
.horarios {
background: var(--primary);
color: var(--white);
}
.horarios .section-title {
color: var(--white);
}
.horarios .section-title::after {
background: var(--secondary);
}
.horarios-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 40px;
text-align: center;
}
.horario-item {
background: rgba(255,255,255,0.1);
padding: 40px;
border: 1px solid rgba(212, 175, 55, 0.3);
backdrop-filter: blur(10px);
}
.horario-item h3 {
color: var(--secondary);
margin-bottom: 20px;
font-size: 24px;
}
.horario-item p {
font-size: 20px;
opacity: 0.9;
}
/* Reservas Elegantes */
.reservas {
background: var(--white);
}
.reservas-content {
max-width: 700px;
margin: 0 auto;
text-align: center;
}
.reservas-form {
display: grid;
gap: 25px;
margin-top: 50px;
text-align: left;
}
.form-group label {
display: block;
margin-bottom: 10px;
color: var(--primary);
font-weight: 600;
font-size: 15px;
letter-spacing: 0.5px;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 15px;
border: 2px solid #e8e5df;
border-radius: 0;
font-family: 'Georgia', serif;
font-size: 16px;
transition: border-color 0.3s;
background: var(--bg-cream);
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--secondary);
}
.btn-primary {
background: var(--primary);
color: var(--white);
border: 2px solid var(--primary);
padding: 18px 50px;
font-size: 16px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s;
}
.btn-primary:hover {
background: transparent;
color: var(--primary);
}
/* Especialidad Elegante */
.especialidad {
background: var(--bg-cream);
}
.especialidad-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
align-items: center;
}
.especialidad-text h2 {
color: var(--primary);
margin-bottom: 30px;
font-size: 42px;
}
.especialidad-text p {
font-size: 18px;
line-height: 2;
color: var(--text-light);
font-style: italic;
}
.especialidad-image img {
width: 100%;
border-radius: 0;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
border: 10px solid var(--white);
}
/* Contacto Elegante */
.contacto {
background: var(--white);
}
.contacto-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
}
.contacto-info h3 {
color: var(--primary);
margin-bottom: 30px;
font-size: 32px;
}
.contacto-info p {
color: var(--text-light);
margin-bottom: 20px;
font-size: 17px;
line-height: 2;
}
.contacto-info strong {
color: var(--primary);
font-weight: 700;
}
.redes-sociales {
margin-top: 40px;
}
.redes-sociales a {
display: inline-block;
margin-right: 20px;
color: var(--primary);
font-size: 28px;
transition: transform 0.3s, color 0.3s;
}
.redes-sociales a:hover {
transform: scale(1.2);
color: var(--secondary);
}
/* Footer Elegante */
footer {
background: var(--primary);
color: var(--white);
text-align: center;
padding: 50px 20px;
border-top: 3px solid var(--secondary);
}
footer p {
font-size: 16px;
opacity: 0.9;
letter-spacing: 1px;
}
/* Responsive */
@media (max-width: 768px) {
.hero h1 {
font-size: 42px;
}
.nav-links {
display: none;
}
.especialidad-content,
.contacto-content {
grid-template-columns: 1fr;
}
.section {
padding: 60px 20px;
}
}
</style>
</head>
<body>
<!-- Header -->
<header>
<nav>
<div class="logo">{{ site_name or 'Restaurante Elegante' }}</div>
<ul class="nav-links">
<li><a href="#inicio">Inicio</a></li>
<li><a href="#menu">Menú</a></li>
<li><a href="#horarios">Horarios</a></li>
<li><a href="#reservas">Reservas</a></li>
<li><a href="#contacto">Contacto</a></li>
</ul>
</nav>
</header>
<!-- Hero Section -->
<section id="inicio" class="section hero">
<div class="container">
<h1>{{ hero_title or 'Experiencia Culinaria Excepcional' }}</h1>
<p>{{ hero_description or 'Donde la tradición se encuentra con la innovación' }}</p>
<a href="#reservas" class="btn">Reservar Mesa</a>
</div>
</section>
<!-- Menu Section -->
<section id="menu" class="section menu">
<div class="container">
<h2 class="section-title">Nuestro Menú Exquisito</h2>
<div class="menu-grid">
<div class="menu-item">
<h3>Plato Especial del Chef</h3>
<p>Una creación única que combina los mejores ingredientes con técnicas culinarias refinadas.</p>
<div class="menu-price">€35.00</div>
</div>
<div class="menu-item">
<h3>Especialidad de la Casa</h3>
<p>Nuestra receta tradicional, perfeccionada a lo largo de generaciones.</p>
<div class="menu-price">€42.00</div>
</div>
<div class="menu-item">
<h3>Degustación Premium</h3>
<p>Una experiencia gastronómica completa con los mejores sabores de nuestra cocina.</p>
<div class="menu-price">€65.00</div>
</div>
<div class="menu-item">
<h3>Postre Artesanal</h3>
<p>Dulces elaborados con ingredientes selectos y presentación impecable.</p>
<div class="menu-price">€18.00</div>
</div>
</div>
{% if menu_url %}
<div style="text-align: center; margin-top: 60px;">
<a href="{{ menu_url }}" class="btn" target="_blank">Ver Menú Completo</a>
</div>
{% endif %}
</div>
</section>
<!-- Horarios Section -->
<section id="horarios" class="section horarios">
<div class="container">
<h2 class="section-title">Horarios de Atención</h2>
<div class="horarios-content">
<div class="horario-item">
<h3>Lunes - Viernes</h3>
<p>{{ horarios.lunes_viernes or '13:00 - 23:00' }}</p>
</div>
<div class="horario-item">
<h3>Sábados</h3>
<p>{{ horarios.sabados or '13:00 - 00:00' }}</p>
</div>
<div class="horario-item">
<h3>Domingos</h3>
<p>{{ horarios.domingos or '13:00 - 22:00' }}</p>
</div>
</div>
</div>
</section>
<!-- Reservas Section -->
<section id="reservas" class="section reservas">
<div class="container">
<h2 class="section-title">Reserva tu Mesa</h2>
<div class="reservas-content">
<p style="color: var(--text-light); margin-bottom: 40px; font-size: 18px; font-style: italic;">
Complete el formulario y nos pondremos en contacto para confirmar su reserva.
</p>
<form class="reservas-form">
<div class="form-group">
<label>Nombre Completo</label>
<input type="text" name="nombre" required>
</div>
<div class="form-group">
<label>Teléfono</label>
<input type="tel" name="telefono" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" required>
</div>
<div class="form-group">
<label>Fecha</label>
<input type="date" name="fecha" required>
</div>
<div class="form-group">
<label>Hora</label>
<input type="time" name="hora" required>
</div>
<div class="form-group">
<label>Número de Personas</label>
<select name="personas" required>
<option value="1">1 persona</option>
<option value="2">2 personas</option>
<option value="3">3 personas</option>
<option value="4">4 personas</option>
<option value="5+">5 o más personas</option>
</select>
</div>
<div class="form-group">
<label>Mensaje (opcional)</label>
<textarea name="mensaje" rows="4"></textarea>
</div>
<button type="submit" class="btn-primary">Enviar Reserva</button>
</form>
</div>
</div>
</section>
<!-- Especialidad Culinaria Section -->
<section class="section especialidad">
<div class="container">
<div class="especialidad-content">
<div class="especialidad-text">
<h2>{{ especialidad_culinaria.titulo or 'Nuestra Especialidad' }}</h2>
<p>{{ especialidad_culinaria.descripcion or 'Cada plato es una obra de arte, preparado con ingredientes de la más alta calidad y técnicas culinarias refinadas que honran la tradición gastronómica.' }}</p>
</div>
<div class="especialidad-image">
<img src="{{ especialidad_culinaria.imagen or 'https://via.placeholder.com/600x500?text=Especialidad' }}" alt="Especialidad Culinaria">
</div>
</div>
</div>
</section>
<!-- Contacto Section -->
<section id="contacto" class="section contacto">
<div class="container">
<h2 class="section-title">Contacto</h2>
<div class="contacto-content">
<div class="contacto-info">
<h3>Información de Contacto</h3>
<p><strong>Dirección:</strong> {{ direccion or 'Avenida Principal 456, Ciudad' }}</p>
<p><strong>Teléfono:</strong> {{ telefono or '+34 987 654 321' }}</p>
<p><strong>Email:</strong> {{ email or 'contacto@restaurante.com' }}</p>
{% if capacidad %}
<p><strong>Capacidad:</strong> {{ capacidad }} personas</p>
{% endif %}
<div class="redes-sociales">
{% if redes_sociales.facebook %}
<a href="{{ redes_sociales.facebook }}" target="_blank">📘</a>
{% endif %}
{% if redes_sociales.instagram %}
<a href="{{ redes_sociales.instagram }}" target="_blank">📷</a>
{% endif %}
{% if redes_sociales.twitter %}
<a href="{{ redes_sociales.twitter }}" target="_blank">🐦</a>
{% endif %}
{% if redes_sociales.whatsapp %}
<a href="https://wa.me/{{ redes_sociales.whatsapp }}" target="_blank">💬</a>
{% endif %}
</div>
</div>
<div class="contacto-info">
<h3>Ubicación</h3>
{% if mapa_url %}
<iframe src="{{ mapa_url }}" width="100%" height="350" style="border:0; border: 2px solid #e8e5df;" allowfullscreen="" loading="lazy"></iframe>
{% else %}
<p style="color: var(--text-light); font-style: italic;">Mapa de ubicación disponible próximamente</p>
{% endif %}
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer>
<div class="container">
<p>&copy; 2025 {{ site_name or 'Restaurante Elegante' }}. Todos los derechos reservados.</p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,29 @@
{
"name": "Restaurante Moderno",
"rubro": "restaurante",
"description": "Tema elegante para restaurantes",
"sections": [
"hero",
"menu",
"horarios",
"reservas",
"especialidad_culinaria",
"contacto"
],
"colors": {
"primary": "#d32f2f",
"secondary": "#ff6f00",
"accent": "#ff8f00"
},
"typography": {
"font_family": "Roboto",
"headings": "Playfair Display"
},
"features": {
"menu_url": true,
"horarios": true,
"reservas": true,
"capacidad": true,
"especialidad_culinaria": true
}
}

View File

@@ -0,0 +1,44 @@
/* Estilos adicionales para el tema Restaurante Moderno */
/* Estos estilos complementan el template.html */
/* Animaciones suaves */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section {
animation: fadeInUp 0.6s ease-out;
}
/* Efectos hover mejorados */
.menu-item,
.horario-item {
transition: all 0.3s ease;
}
/* Scroll suave */
html {
scroll-behavior: smooth;
}
/* Mejoras responsive */
@media (max-width: 480px) {
.hero h1 {
font-size: 32px;
}
.section-title {
font-size: 32px;
}
.menu-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,562 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name or 'Restaurante' }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Playfair+Display:wght@400;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #d32f2f;
--secondary: #ff6f00;
--accent: #ff8f00;
--text-dark: #2c2c2c;
--text-light: #666;
--bg-light: #f9f9f9;
--white: #ffffff;
}
body {
font-family: 'Roboto', sans-serif;
color: var(--text-dark);
line-height: 1.6;
overflow-x: hidden;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Playfair Display', serif;
font-weight: 600;
line-height: 1.2;
}
.section {
padding: 80px 20px;
position: relative;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
/* Header / Navigation */
header {
background: var(--white);
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
position: fixed;
width: 100%;
top: 0;
z-index: 1000;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 40px;
}
.logo {
font-family: 'Playfair Display', serif;
font-size: 28px;
color: var(--primary);
font-weight: 700;
}
.nav-links {
display: flex;
list-style: none;
gap: 30px;
}
.nav-links a {
text-decoration: none;
color: var(--text-dark);
font-weight: 500;
transition: color 0.3s;
}
.nav-links a:hover {
color: var(--primary);
}
/* Hero Section */
.hero {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: var(--white);
text-align: center;
padding: 150px 20px 100px;
margin-top: 80px;
}
.hero h1 {
font-size: 64px;
margin-bottom: 20px;
color: var(--white);
}
.hero p {
font-size: 20px;
margin-bottom: 30px;
opacity: 0.95;
}
.btn {
display: inline-block;
padding: 15px 40px;
background: var(--accent);
color: var(--white);
text-decoration: none;
border-radius: 5px;
font-weight: 500;
transition: transform 0.3s, box-shadow 0.3s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
}
/* Menu Section */
.menu {
background: var(--white);
}
.section-title {
text-align: center;
font-size: 42px;
color: var(--primary);
margin-bottom: 60px;
}
.menu-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 40px;
}
.menu-item {
background: var(--bg-light);
padding: 30px;
border-radius: 10px;
transition: transform 0.3s;
}
.menu-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.menu-item h3 {
color: var(--primary);
margin-bottom: 10px;
font-size: 24px;
}
.menu-item p {
color: var(--text-light);
margin-bottom: 15px;
}
.menu-price {
font-size: 22px;
font-weight: 700;
color: var(--secondary);
}
/* Horarios Section */
.horarios {
background: var(--bg-light);
}
.horarios-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
text-align: center;
}
.horario-item {
background: var(--white);
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.horario-item h3 {
color: var(--primary);
margin-bottom: 15px;
}
.horario-item p {
color: var(--text-light);
font-size: 18px;
}
/* Reservas Section */
.reservas {
background: var(--white);
}
.reservas-content {
max-width: 600px;
margin: 0 auto;
text-align: center;
}
.reservas-form {
display: grid;
gap: 20px;
margin-top: 40px;
}
.form-group {
text-align: left;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--text-dark);
font-weight: 500;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-family: 'Roboto', sans-serif;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary);
}
.btn-primary {
background: var(--primary);
color: var(--white);
border: none;
padding: 15px 40px;
font-size: 18px;
cursor: pointer;
border-radius: 5px;
transition: background 0.3s;
}
.btn-primary:hover {
background: var(--secondary);
}
/* Especialidad Culinaria Section */
.especialidad {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: var(--white);
}
.especialidad-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 50px;
align-items: center;
}
.especialidad-text h2 {
color: var(--white);
margin-bottom: 20px;
}
.especialidad-text p {
font-size: 18px;
opacity: 0.95;
line-height: 1.8;
}
.especialidad-image {
text-align: center;
}
.especialidad-image img {
max-width: 100%;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
/* Contacto Section */
.contacto {
background: var(--bg-light);
}
.contacto-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 50px;
}
.contacto-info h3 {
color: var(--primary);
margin-bottom: 20px;
}
.contacto-info p {
color: var(--text-light);
margin-bottom: 15px;
font-size: 16px;
}
.contacto-info strong {
color: var(--text-dark);
}
.redes-sociales {
margin-top: 30px;
}
.redes-sociales a {
display: inline-block;
margin-right: 15px;
color: var(--primary);
font-size: 24px;
transition: transform 0.3s;
}
.redes-sociales a:hover {
transform: scale(1.2);
}
/* Footer */
footer {
background: var(--text-dark);
color: var(--white);
text-align: center;
padding: 40px 20px;
}
/* Responsive */
@media (max-width: 768px) {
.hero h1 {
font-size: 42px;
}
.nav-links {
display: none;
}
.especialidad-content,
.contacto-content {
grid-template-columns: 1fr;
}
.section {
padding: 60px 20px;
}
}
</style>
</head>
<body>
<!-- Header -->
<header>
<nav>
<div class="logo">{{ site_name or 'Restaurante' }}</div>
<ul class="nav-links">
<li><a href="#inicio">Inicio</a></li>
<li><a href="#menu">Menú</a></li>
<li><a href="#horarios">Horarios</a></li>
<li><a href="#reservas">Reservas</a></li>
<li><a href="#contacto">Contacto</a></li>
</ul>
</nav>
</header>
<!-- Hero Section -->
<section id="inicio" class="section hero">
<div class="container">
<h1>{{ hero_title or 'Bienvenido a Nuestro Restaurante' }}</h1>
<p>{{ hero_description or 'Sabores auténticos que despiertan tus sentidos' }}</p>
<a href="#reservas" class="btn">Reservar Mesa</a>
</div>
</section>
<!-- Menu Section -->
<section id="menu" class="section menu">
<div class="container">
<h2 class="section-title">Nuestro Menú</h2>
<div class="menu-grid">
<div class="menu-item">
<h3>Plato Especial 1</h3>
<p>Descripción del plato con ingredientes frescos y sabores únicos.</p>
<div class="menu-price">$25.00</div>
</div>
<div class="menu-item">
<h3>Plato Especial 2</h3>
<p>Descripción del plato con ingredientes frescos y sabores únicos.</p>
<div class="menu-price">$28.00</div>
</div>
<div class="menu-item">
<h3>Plato Especial 3</h3>
<p>Descripción del plato con ingredientes frescos y sabores únicos.</p>
<div class="menu-price">$30.00</div>
</div>
<div class="menu-item">
<h3>Plato Especial 4</h3>
<p>Descripción del plato con ingredientes frescos y sabores únicos.</p>
<div class="menu-price">$32.00</div>
</div>
</div>
{% if menu_url %}
<div style="text-align: center; margin-top: 40px;">
<a href="{{ menu_url }}" class="btn" target="_blank">Ver Menú Completo</a>
</div>
{% endif %}
</div>
</section>
<!-- Horarios Section -->
<section id="horarios" class="section horarios">
<div class="container">
<h2 class="section-title">Horarios de Atención</h2>
<div class="horarios-content">
<div class="horario-item">
<h3>Lunes - Viernes</h3>
<p>{{ horarios.lunes_viernes or '12:00 PM - 10:00 PM' }}</p>
</div>
<div class="horario-item">
<h3>Sábados</h3>
<p>{{ horarios.sabados or '12:00 PM - 11:00 PM' }}</p>
</div>
<div class="horario-item">
<h3>Domingos</h3>
<p>{{ horarios.domingos or '12:00 PM - 9:00 PM' }}</p>
</div>
</div>
</div>
</section>
<!-- Reservas Section -->
<section id="reservas" class="section reservas">
<div class="container">
<h2 class="section-title">Reserva tu Mesa</h2>
<div class="reservas-content">
<p style="color: var(--text-light); margin-bottom: 30px;">
Completa el formulario y nos pondremos en contacto contigo para confirmar tu reserva.
</p>
<form class="reservas-form">
<div class="form-group">
<label>Nombre Completo</label>
<input type="text" name="nombre" required>
</div>
<div class="form-group">
<label>Teléfono</label>
<input type="tel" name="telefono" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" required>
</div>
<div class="form-group">
<label>Fecha</label>
<input type="date" name="fecha" required>
</div>
<div class="form-group">
<label>Hora</label>
<input type="time" name="hora" required>
</div>
<div class="form-group">
<label>Número de Personas</label>
<select name="personas" required>
<option value="1">1 persona</option>
<option value="2">2 personas</option>
<option value="3">3 personas</option>
<option value="4">4 personas</option>
<option value="5+">5 o más personas</option>
</select>
</div>
<div class="form-group">
<label>Mensaje (opcional)</label>
<textarea name="mensaje" rows="4"></textarea>
</div>
<button type="submit" class="btn-primary">Enviar Reserva</button>
</form>
</div>
</div>
</section>
<!-- Especialidad Culinaria Section -->
<section class="section especialidad">
<div class="container">
<div class="especialidad-content">
<div class="especialidad-text">
<h2>{{ especialidad_culinaria.titulo or 'Nuestra Especialidad' }}</h2>
<p>{{ especialidad_culinaria.descripcion or 'Cada plato es una obra de arte culinaria, preparado con ingredientes frescos y técnicas tradicionales que honran la autenticidad de nuestros sabores.' }}</p>
</div>
<div class="especialidad-image">
<img src="{{ especialidad_culinaria.imagen or 'https://via.placeholder.com/500x400?text=Especialidad' }}" alt="Especialidad Culinaria">
</div>
</div>
</div>
</section>
<!-- Contacto Section -->
<section id="contacto" class="section contacto">
<div class="container">
<h2 class="section-title">Contacto</h2>
<div class="contacto-content">
<div class="contacto-info">
<h3>Información de Contacto</h3>
<p><strong>Dirección:</strong> {{ direccion or 'Calle Principal 123, Ciudad' }}</p>
<p><strong>Teléfono:</strong> {{ telefono or '+34 123 456 789' }}</p>
<p><strong>Email:</strong> {{ email or 'contacto@restaurante.com' }}</p>
{% if capacidad %}
<p><strong>Capacidad:</strong> {{ capacidad }} personas</p>
{% endif %}
<div class="redes-sociales">
{% if redes_sociales.facebook %}
<a href="{{ redes_sociales.facebook }}" target="_blank">📘</a>
{% endif %}
{% if redes_sociales.instagram %}
<a href="{{ redes_sociales.instagram }}" target="_blank">📷</a>
{% endif %}
{% if redes_sociales.twitter %}
<a href="{{ redes_sociales.twitter }}" target="_blank">🐦</a>
{% endif %}
{% if redes_sociales.whatsapp %}
<a href="https://wa.me/{{ redes_sociales.whatsapp }}" target="_blank">💬</a>
{% endif %}
</div>
</div>
<div class="contacto-info">
<h3>Ubicación</h3>
{% if mapa_url %}
<iframe src="{{ mapa_url }}" width="100%" height="300" style="border:0; border-radius: 10px;" allowfullscreen="" loading="lazy"></iframe>
{% else %}
<p style="color: var(--text-light);">Mapa de ubicación disponible próximamente</p>
{% endif %}
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer>
<div class="container">
<p>&copy; 2025 {{ site_name or 'Restaurante' }}. Todos los derechos reservados.</p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,78 @@
"""
GKACHELE™ - Script para ver usuarios registrados
© 2025 GKACHELE™. Todos los derechos reservados.
Uso: python ver_usuarios.py
"""
import sqlite3
import os
from datetime import datetime
# Ruta de la base de datos
BASE_DIR = os.path.dirname(__file__)
DATABASE_DIR = os.path.join(BASE_DIR, 'database')
MAIN_DB = os.path.join(DATABASE_DIR, 'main.db')
def ver_usuarios():
"""Ver todos los usuarios registrados"""
if not os.path.exists(MAIN_DB):
print(f"❌ No se encontró la base de datos en: {MAIN_DB}")
return
try:
conn = sqlite3.connect(MAIN_DB)
c = conn.cursor()
# Obtener usuarios
c.execute('SELECT id, email, plan, rubro, created_at FROM users ORDER BY id')
users = c.fetchall()
if not users:
print("📭 No hay usuarios registrados aún")
return
print("=" * 80)
print("👥 USUARIOS REGISTRADOS")
print("=" * 80)
print(f"{'ID':<5} {'Email':<35} {'Plan':<10} {'Rubro':<15} {'Fecha Registro':<20}")
print("-" * 80)
for user in users:
user_id, email, plan, rubro, created_at = user
# Formatear fecha
fecha = created_at if created_at else 'N/A'
print(f"{user_id:<5} {email:<35} {plan:<10} {rubro:<15} {fecha:<20}")
print("-" * 80)
print(f"Total: {len(users)} usuario(s)")
# Obtener sitios por usuario
print("\n" + "=" * 80)
print("🌐 SITIOS POR USUARIO")
print("=" * 80)
for user in users:
user_id, email, plan, rubro, created_at = user
c.execute('SELECT id, slug, theme, status, created_at FROM sites WHERE user_id = ? ORDER BY id', (user_id,))
sites = c.fetchall()
print(f"\n👤 Usuario ID {user_id} ({email}):")
if sites:
print(f" {'ID':<5} {'Slug':<25} {'Tema':<20} {'Estado':<12} {'Fecha':<20}")
print(" " + "-" * 82)
for site in sites:
site_id, slug, theme, status, site_created = site
fecha = site_created if site_created else 'N/A'
print(f" {site_id:<5} {slug:<25} {theme:<20} {status:<12} {fecha:<20}")
else:
print(" ⚠️ No tiene sitios creados")
conn.close()
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
ver_usuarios()