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,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)