3 Commits

3 changed files with 173 additions and 4 deletions

View File

@@ -86,6 +86,18 @@
- Revert: - Revert:
- `git revert f6d8ab1` - `git revert f6d8ab1`
### API Elementor save/publish
- Commit: `b6fb4da`
- Objetivo: agregar endpoint dedicado `/api/elementor/save` para guardar builder con opcion de publicar.
- Revert:
- `git revert b6fb4da`
### Builder persistencia y feedback de publicacion
- Commit: `c2ee81d`
- Objetivo: mantener bloques cargados al entrar, normalizar bloques sin id y mostrar estado de guardado/publicacion en topbar.
- Revert:
- `git revert c2ee81d`
## URL local canonica (unificada) ## URL local canonica (unificada)
- Base local: `http://127.0.0.1:5001` - Base local: `http://127.0.0.1:5001`
- Builder local: `http://127.0.0.1:5001/elementor/1` - Builder local: `http://127.0.0.1:5001/elementor/1`

106
elementor/routes.py Normal file
View File

@@ -0,0 +1,106 @@
from flask import Blueprint, render_template, session, request, jsonify
import json
from db import get_db
from utils.theme_engine import get_theme_config
elementor_bp = Blueprint(
'elementor',
__name__,
template_folder='templates',
static_folder='static',
static_url_path='/elementor/static'
)
def _render_builder(site_id, builder_mode='default', **_kwargs):
conn = get_db()
c = conn.cursor()
c.execute('SELECT user_id, slug, theme, content_json FROM sites WHERE id = ?', (site_id,))
site = c.fetchone()
if not site:
conn.close()
return 'Sitio no encontrado', 404
if 'user_id' in session and site[0] != session['user_id']:
conn.close()
return 'No autorizado', 403
content = json.loads(site[3]) if site[3] else {}
if not isinstance(content, dict):
content = {}
theme = site[2]
theme_config = get_theme_config(theme)
c.execute('SELECT plan, rubro FROM users WHERE id = ?', (site[0],))
user_data = c.fetchone()
conn.close()
user_plan = user_data[0] if user_data else 'base'
user_rubro = user_data[1] if user_data else 'restaurante'
return render_template(
'elementor_builder.html',
site_id=site_id,
slug=site[1],
theme=theme,
content=content,
theme_config=theme_config,
user_plan=user_plan,
rubro=user_rubro,
builder_mode=builder_mode
)
@elementor_bp.route('/elementor/<int:site_id>')
def elementor_view(site_id):
return _render_builder(site_id, builder_mode='default')
@elementor_bp.route('/ub24/<int:site_id>')
def ub24_view(site_id):
return _render_builder(site_id, builder_mode='ub24')
@elementor_bp.route('/api/elementor/save', methods=['POST'])
def save_elementor():
data = request.get_json(silent=True) or {}
site_id = data.get('site_id')
content = data.get('content')
publish = bool(data.get('publish'))
if not site_id or not isinstance(content, dict):
return jsonify({'success': False, 'error': 'Payload invalido'}), 400
conn = get_db()
c = conn.cursor()
c.execute('SELECT user_id, content_json FROM sites WHERE id = ?', (site_id,))
row = c.fetchone()
if not row:
conn.close()
return jsonify({'success': False, 'error': 'Sitio no encontrado'}), 404
owner_id = row[0]
if 'user_id' in session and session['user_id'] != owner_id:
conn.close()
return jsonify({'success': False, 'error': 'No autorizado'}), 403
current_content = {}
try:
if row[1]:
current_content = json.loads(row[1]) or {}
except Exception:
current_content = {}
merged = dict(current_content)
merged.update(content)
if publish:
c.execute('UPDATE sites SET content_json = ?, status = ? WHERE id = ?', (json.dumps(merged), 'published', site_id))
else:
c.execute('UPDATE sites SET content_json = ? WHERE id = ?', (json.dumps(merged), site_id))
conn.commit()
conn.close()
return jsonify({'success': True, 'published': publish})

View File

@@ -19,6 +19,10 @@
.block-item{background:var(--panel2);border:1px solid var(--border);padding:8px 10px;border-radius:10px;margin-bottom:8px;cursor:grab} .block-item{background:var(--panel2);border:1px solid var(--border);padding:8px 10px;border-radius:10px;margin-bottom:8px;cursor:grab}
.main{padding:18px} .main{padding:18px}
.topbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap} .topbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}
.save-status{font-size:12px;color:var(--muted);min-width:150px;text-align:right}
.save-status.ok{color:#34d399}
.save-status.error{color:#f87171}
.save-status.busy{color:#fbbf24}
.btn{background:var(--accent);color:#09121a;border:0;padding:8px 12px;border-radius:999px;font-weight:700;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease} .btn{background:var(--accent);color:#09121a;border:0;padding:8px 12px;border-radius:999px;font-weight:700;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease}
.btn:hover{transform:translateY(-1px);box-shadow:0 10px 22px rgba(15,23,42,.15)} .btn:hover{transform:translateY(-1px);box-shadow:0 10px 22px rgba(15,23,42,.15)}
.btn.secondary{background:transparent;color:var(--text);border:1px solid var(--border)} .btn.secondary{background:transparent;color:var(--text);border:1px solid var(--border)}
@@ -253,6 +257,7 @@
<button class="btn secondary" id="btnFreeDrag" style="display:none">Modo libre</button> <button class="btn secondary" id="btnFreeDrag" style="display:none">Modo libre</button>
<button class="btn secondary" id="btnAlign">Alinear</button> <button class="btn secondary" id="btnAlign">Alinear</button>
<button class="btn" id="btnSave">Publicar</button> <button class="btn" id="btnSave">Publicar</button>
<div class="save-status" id="saveStatus">Listo</div>
</div> </div>
</div> </div>
<div class="preview-shell"> <div class="preview-shell">
@@ -447,6 +452,7 @@ const state = {
let pendingMove = null; let pendingMove = null;
let pendingResize = null; let pendingResize = null;
let previewStateBefore = null; let previewStateBefore = null;
let isSaving = false;
function makeId(){ return "block_" + Date.now() + "_" + Math.floor(Math.random()*1000); } function makeId(){ return "block_" + Date.now() + "_" + Math.floor(Math.random()*1000); }
function getDefaultPos(){ function getDefaultPos(){
@@ -1657,19 +1663,64 @@ const state = {
}); });
renderPreview(); renderPreview();
} }
function setSaveStatus(msg, kind=""){
const status = document.getElementById("saveStatus");
if (!status) return;
status.textContent = msg;
status.className = `save-status${kind ? ` ${kind}` : ""}`;
}
function normalizeLoadedBlocks(blocks){
if (!Array.isArray(blocks)) return [];
return blocks
.filter((b)=>b && typeof b === "object")
.map((b)=>({
...b,
id: b.id || makeId(),
data: (b.data && typeof b.data === "object") ? b.data : {}
}));
}
async function saveContent(){ async function saveContent(){
const payload={ site_id: SITE_ID, content: { ...SERVER_CONTENT, settings: state.settings, blocks: state.blocks } }; if (isSaving) return;
try{ const res=await fetch("/api/customizer/save",{ method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(payload) }); const data=await res.json(); if (!data.success) throw new Error("save failed"); alert("Cambios guardados"); } isSaving = true;
catch(err){ console.error(err); alert("No se pudo guardar"); } const btn = document.getElementById("btnSave");
if (btn){
btn.disabled = true;
btn.textContent = "Publicando...";
}
setSaveStatus("Guardando cambios...", "busy");
const payload = {
site_id: SITE_ID,
publish: true,
content: { ...SERVER_CONTENT, settings: state.settings, blocks: state.blocks }
};
try{
const res = await fetch("/api/elementor/save",{
method:"POST",
headers:{ "Content-Type":"application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.error || "save failed");
setSaveStatus("Publicado", "ok");
} catch(err){
console.error(err);
setSaveStatus("Error al publicar", "error");
} finally {
isSaving = false;
if (btn){
btn.disabled = false;
btn.textContent = "Publicar";
}
}
} }
function init(){ function init(){
state.blocks = normalizeLoadedBlocks(state.blocks);
if (BUILDER_MODE === "ub24"){ if (BUILDER_MODE === "ub24"){
state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; }); state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; });
} }
if (BUILDER_MODE === "ub24"){ if (BUILDER_MODE === "ub24"){
state.settings.free_drag = false; state.settings.free_drag = false;
} }
state.blocks = [];
selectedBlockId = null; selectedBlockId = null;
wireSidebar(); wirePreviewDrop(); wireInlineEditing(); wireSettings(); wireFreeDragToggle(); wireJumpSelect(); wirePreviewSize(); wireThemeToggle(); wireSidebar(); wirePreviewDrop(); wireInlineEditing(); wireSettings(); wireFreeDragToggle(); wireJumpSelect(); wirePreviewSize(); wireThemeToggle();
const backBtn = document.getElementById("btnBack"); const backBtn = document.getElementById("btnBack");