595 lines
24 KiB
Python
595 lines
24 KiB
Python
"""
|
||
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>© 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)
|