feat(builder): educacion v2 + navegacion por anclas y regla token
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Versionado IA - UB24/Elementor
|
||||
# Versionado IA - UB24/Elementor
|
||||
|
||||
## Rama de trabajo
|
||||
- `ai/ub24-builder-v1`
|
||||
@@ -36,6 +36,16 @@
|
||||
- `git push origin ai/ub24-builder-v1`
|
||||
6. Registrar hash y objetivo en este archivo y en `codex/HISTORIAL_CAMBIOS.md`.
|
||||
|
||||
## Protocolo de inicio de sesion (obligatorio)
|
||||
1. Levantar entorno al iniciar cada sesion, sin esperar pedido del usuario.
|
||||
2. Verificar que servicios locales de trabajo esten activos (app/builder segun tarea).
|
||||
3. Si la tarea requiere validacion remota, conectarse a Raspberry y comprobar estado antes de editar.
|
||||
|
||||
## Regla operativa de contexto/token
|
||||
1. Trabajar por iteraciones cortas y versionar rapido para no perder avance cuando el presupuesto de tokens sea bajo.
|
||||
2. Priorizar cambios de alto impacto visual/funcional antes de detalles menores.
|
||||
3. Si el usuario avisa limite de tokens, cerrar cada bloque con commit y registro en este archivo.
|
||||
|
||||
## Registro de hashes
|
||||
### Baseline
|
||||
- Commit: `cb99f26`
|
||||
@@ -157,3 +167,201 @@ Notas:
|
||||
1. Fase 1 (UI Pro base): navbar premium, hero premium, sistema de espaciado/grid, pulido visual consistente.
|
||||
2. Fase 2 (estructura): separar renderers por bloque y reducir inline styles para automatizacion.
|
||||
3. Fase 3 (presets): presets por rubro + reglas responsive + variantes exportables.
|
||||
|
||||
## Verificacion tecnica (15 Febrero 2026)
|
||||
- Rama activa verificada: `ai/ub24-builder-v1`
|
||||
- Divergencia local/remoto: `0 0` (sin diferencias con `origin/ai/ub24-builder-v1`)
|
||||
- DB local: tabla `sites` operativa con registros de prueba (`id=1`, `id=2`)
|
||||
- Smoke test builder:
|
||||
- `GET /elementor/1` -> `200 OK`
|
||||
- `POST /api/elementor/save` con `site_id=1` -> `200 OK`, `{"success": true, "published": false}`
|
||||
|
||||
## Requisitos minimos para operar Elementor (base SaaS)
|
||||
1. Arrancar con `python -m demo.app` desde `c:\word`.
|
||||
2. Mantener URL canonica local en `http://127.0.0.1:5001` (no `localhost`).
|
||||
3. Confirmar que exista un `site_id` valido en DB (ejemplo activo: `1`).
|
||||
4. Mantener una sola ruta de guardado activa: `/api/elementor/save`.
|
||||
5. Validar guardado/publicacion con prueba rapida antes de cada lote.
|
||||
|
||||
## Regla memorizada de preview (15 Febrero 2026)
|
||||
- La preview del builder debe usar un solo flujo en `Bloques` con acomodo estable.
|
||||
- No mantener ni mezclar modos paralelos de preview.
|
||||
|
||||
## Ajuste operativo UX (15 Febrero 2026)
|
||||
- Se elimina `Completo` del builder.
|
||||
- Se elimina `Modo libre` como toggle separado.
|
||||
- El layout operativo queda unico en bloques:
|
||||
- cada bloque puede ir en `50%` (dos columnas) o `100%` (ancho completo),
|
||||
- controlado por `Ancho completo` en inspector o por drop lateral/centro.
|
||||
|
||||
## Regla de estabilidad (15 Febrero 2026)
|
||||
- Lo que ya funciona no se toca sin requerimiento explicito.
|
||||
- Los bloques del panel izquierdo no deben ocultarse al usarse.
|
||||
- Cada tipo de bloque debe poder agregarse cantidad ilimitada de veces.
|
||||
- Debe existir boton de `Vista previa` funcional en el builder.
|
||||
- El ancho completo/parcial se controla solo con `Ancho bloque (%)` del mismo bloque (sin checkbox separado).
|
||||
- La vista previa se abre en modo push/pop (`preview=1`) y muestra boton `Atras`.
|
||||
|
||||
## Estado real de cierre (15 Febrero 2026)
|
||||
- Tema bloqueante: drag & drop de bloques sigue inestable en el flujo actual.
|
||||
- Sintoma reportado por usuario: mover con mouse no queda consistente para alinear y ubicar bloques como espera.
|
||||
- Decision acordada: pausar cambios hoy para no seguir consumiendo tiempo/tokens.
|
||||
- Proximo reinicio: rehacer DnD con enfoque estable desde base limpia (una sola estrategia de movimiento), validar con pruebas manuales guiadas y recien despues tocar extras.
|
||||
- Regla reforzada: no tocar lo que ya funciona mientras se corrige solo el DnD.
|
||||
|
||||
## Regla operativa de sesion (15 Febrero 2026)
|
||||
- Al iniciar cada sesion de trabajo, levantar primero el entorno local del builder:
|
||||
- `python -m demo.app`
|
||||
- Verificar puerto antes de editar:
|
||||
- `http://127.0.0.1:5001`
|
||||
|
||||
## Acuerdo definitivo DnD (15 Febrero 2026)
|
||||
- Decision: implementar una solucion definitiva de drag & drop con una sola estrategia, sin mezclar motores.
|
||||
- Motor elegido: `SortableJS` para reordenamiento estable (desktop + touch).
|
||||
- Alcance de la correccion:
|
||||
- reemplazar reordenamiento actual por `SortableJS` en el canvas de bloques,
|
||||
- usar `handle` de arrastre para no interferir con edicion inline,
|
||||
- configurar fallback estable (`forceFallback` + umbrales de toque/movimiento),
|
||||
- eliminar handlers duplicados de drag que hoy se pisan entre si.
|
||||
- Regla de seguridad: no tocar guardado, preview ni bloques que ya funcionan.
|
||||
- Proxima sesion: implementar, hacer smoke test completo en `/elementor/1`, y recien despues validar cierre para lanzamiento SaaS.
|
||||
|
||||
## Cierre de sesion (16 Febrero 2026)
|
||||
- Rama de trabajo usada: `ai/ub24-builder-v1`
|
||||
- Entorno validado en sesion: `GET /elementor/1 -> 200 OK`
|
||||
- Commit de cierre funcional del dia:
|
||||
- Hash: `6f14308`
|
||||
- Mensaje: `fix(builder): unificar layout y estabilizar redimension en canvas`
|
||||
- Alcance: `elementor/templates/elementor_builder.html`
|
||||
|
||||
### Resultado operativo del dia
|
||||
- Se recupero el flujo de insercion de bloques desde panel izquierdo al canvas.
|
||||
- Se estabilizo el modelo de layout del canvas para evitar mezcla de estrategias.
|
||||
- Se dejo redimension por ancho de bloque en flujo normal y control desde inspector.
|
||||
- Se corrigio comportamiento por defecto para que bloques nuevos entren apilados (ancho completo) y no en paralelo automatico.
|
||||
- Se mantuvo `SortableJS` para reordenamiento de bloques en canvas.
|
||||
|
||||
### Decisiones memorizadas para siguiente sesion
|
||||
- Mantener una sola estrategia de layout y reordenamiento (sin volver a mezclar motores/flows).
|
||||
- No reabrir cambios globales cuando un fix local sea suficiente.
|
||||
- Si algo ya funciona, no rehacerlo completo: tocar solo el bloque minimo necesario.
|
||||
|
||||
### Pendientes acordados (proxima sesion)
|
||||
1. Footer obligatorio global en todas las paginas:
|
||||
- marca registrada del usuario + empresa desarrolladora.
|
||||
2. Watermark de autoria en codigo (convencion consistente del proyecto).
|
||||
3. Preview dual:
|
||||
- modo bloques (editor),
|
||||
- modo pagina completa real (resultado final).
|
||||
4. Mejorar landing base para nivel visual profesional.
|
||||
5. Redes sociales con posicion libre (ubicacion configurable).
|
||||
6. Menu superior: corregir modo acordeon (hoy no queda operativo como se espera).
|
||||
7. Mejorar interaccion touch/capacitiva del builder.
|
||||
|
||||
## Registro operativo (17 Febrero 2026)
|
||||
- TAG: `RUBROS_OFICIALES_BLOQUEADOS`
|
||||
- Regla fija: no usar rubros inventados; solo los oficiales definidos por usuario.
|
||||
- Rubros oficiales memorizados:
|
||||
- `restaurante`
|
||||
- `danza`
|
||||
- `cosmeticos`
|
||||
- `despachos`
|
||||
- `gimnasios`
|
||||
- `educacion`
|
||||
- `base_otro` (`Base (Otro)`)
|
||||
|
||||
- TAG: `PREVIEW_FINAL_SEPARADA`
|
||||
- Se agrega flujo dedicado de preview final real separado del hash del editor:
|
||||
- `GET /elementor/<site_id>/preview-final`
|
||||
- `GET /ub24/<site_id>/preview-final`
|
||||
- En preview final:
|
||||
- se fuerza `preview-mode` desde servidor (`preview_only=true`),
|
||||
- boton `Atras` vuelve al builder base.
|
||||
|
||||
- TAG: `UNIFICACION_TOPBAR_Y_DRAWER_FIX`
|
||||
- Ajustes aplicados:
|
||||
- se elimina duplicidad operativa de preview en topbar y se deja `Pagina real` como flujo principal,
|
||||
- selector de plantillas migra a `Plantillas por rubro` (catalogo oficial),
|
||||
- `Drawer Pro` se mueve a capa global para evitar render roto dentro del bloque/canvas,
|
||||
- etiqueta de pagina `Servicios` se neutraliza a `Seccion 2` para no mezclar paginas con rubros.
|
||||
|
||||
## Registro infraestructura Raspberry (17 Febrero 2026)
|
||||
- TAG: `KOMKIDA_WEB_ACCESS_RECOVERY`
|
||||
- Objetivo: recuperar `komkida.duckdns.org` como acceso web remoto a Raspberry con login (estilo Motion), sin romper:
|
||||
- `gk-saas.komkida.duckdns.org`
|
||||
- `git.gk-saas.komkida.duckdns.org`
|
||||
- `motion.komkida.duckdns.org`
|
||||
|
||||
### Diagnostico real
|
||||
1. El `502 Bad Gateway` en subdominios (`gk-saas`, `git`, `motion`) venia por `proxy_pass http://localhost:PUERTO`:
|
||||
- Nginx resolvia `localhost` a IPv6 (`[::1]`) en varios casos.
|
||||
- servicios escuchando en IPv4 (`127.0.0.1` / `0.0.0.0`) devolvian `connection refused`.
|
||||
2. `komkida.duckdns.org` quedo mezclado con proxy a metricas (`192.168.1.133:5000`) y luego con rutas no validas (`Not Found`).
|
||||
3. Hubo drift de configuracion:
|
||||
- archivo suelto viejo en `/etc/nginx/sites-enabled/komkida.duckdns.org` (no symlink) con reglas desactualizadas.
|
||||
4. El navegador intentaba `https://komkida.duckdns.org` y no habia listener en `443`:
|
||||
- error visible: `ERR_CONNECTION_REFUSED`.
|
||||
|
||||
### Cambios aplicados en Raspberry
|
||||
1. Normalizacion de upstreams Nginx a IPv4:
|
||||
- `proxy_pass http://127.0.0.1:<puerto>;` en vhosts activos.
|
||||
2. Limpieza de duplicados/conflictos:
|
||||
- quitar backups cargados en `sites-enabled`.
|
||||
- asegurar `sites-enabled/komkida.duckdns.org` como symlink al archivo de `sites-available`.
|
||||
3. Definicion final de `komkida.duckdns.org`:
|
||||
- auth_basic con `.htpasswd`.
|
||||
- `/` proxyeado a `https://127.0.0.1:4200` (ShellInABox).
|
||||
- `/api/` mantiene proxy a `127.0.0.1:9999` (no bloquea resto del sistema).
|
||||
4. Habilitacion HTTPS para `komkida`:
|
||||
- servidor Nginx en `443 ssl`.
|
||||
- certificado local en:
|
||||
- `/etc/nginx/ssl/komkida.crt`
|
||||
- `/etc/nginx/ssl/komkida.key`
|
||||
5. Verificacion final:
|
||||
- Nginx escuchando en `80` y `443`.
|
||||
- `http://komkida.duckdns.org` -> `401` (esperado: login).
|
||||
- `https://komkida.duckdns.org` -> `401` (esperado: login).
|
||||
- `gk-saas` y `git` siguen en `200`.
|
||||
|
||||
### Bots (estado para no cargar la Pi)
|
||||
- `clawdbot`: deshabilitado por OOM recurrente (`JavaScript heap out of memory`).
|
||||
- `moltobot`: deshabilitado temporalmente.
|
||||
- Decision: priorizar estabilidad de infraestructura y SaaS.
|
||||
|
||||
### Archivos clave tocados en la Pi
|
||||
- `/etc/nginx/sites-available/komkida.duckdns.org`
|
||||
- `/etc/nginx/sites-enabled/komkida.duckdns.org`
|
||||
- `/etc/nginx/ssl/komkida.crt`
|
||||
- `/etc/nginx/ssl/komkida.key`
|
||||
|
||||
### Checklist rapido de recuperacion futura
|
||||
1. `sudo nginx -t`
|
||||
2. `sudo systemctl restart nginx`
|
||||
3. `ss -tulpen | egrep ':80 |:443 '`
|
||||
4. `curl -I http://komkida.duckdns.org/` (debe pedir auth)
|
||||
5. `curl -k -I https://komkida.duckdns.org/` (debe pedir auth)
|
||||
6. `curl -I http://gk-saas.komkida.duckdns.org/` (debe responder `200`)
|
||||
|
||||
A ESTO SE TIENEN QUE PARECER:
|
||||
Correcto. Busqué referencias reales y modernas; vamos a dejar de “inventar” estilos internos.
|
||||
|
||||
Referencias que sí sirven para copiar estructura profesional:
|
||||
1. Webflow Education Templates (catálogo): https://webflow.com/templates/category/education-websites
|
||||
2. Hopedu (estructura académica limpia + CMS): https://webflow.com/templates/html/hopedu-school-website-template
|
||||
3. Eduvolv (hero + admissions + secciones institucionales): https://webflow.com/templates/html/eduvolv-website-template
|
||||
4. Thema (variantes de layout + menú fullscreen): https://webflow.com/made-in-webflow/website/thema-education-template-showcase
|
||||
5. Campus (layout simple usable): https://webflow.com/templates/html/campus-education-website-template
|
||||
6. ThemeForest Elementor kit (referencia de páginas mínimas): https://themeforest.net/item/educita-education-elementor-template-kit/37245135
|
||||
|
||||
Plan profesional (sin clones):
|
||||
1. Rehacer templates por rubro con 3 arquitecturas distintas, no solo cambiar color.
|
||||
2. Cada rubro tendrá:
|
||||
- header propio
|
||||
- hero propio
|
||||
- grid propio
|
||||
- CTA propio
|
||||
- footer propio
|
||||
3. El builder cargará esas arquitecturas base y luego se edita contenido.
|
||||
4. Eliminaré el estilo único actual que hace que todo se vea igual.
|
||||
|
||||
Si quieres, empiezo ya con `Educación` copiando patrón `Eduvolv/Hopedu` y luego replico la metodología al resto de rubros.
|
||||
|
||||
@@ -50,14 +50,15 @@
|
||||
.apple-dot{width:9px;height:9px;border-radius:50%}
|
||||
.red{background:#f87171}.yellow{background:#fbbf24}.green{background:#4ade80}
|
||||
.canvas{min-height:700px;padding:18px;background:#f6f7fb;transition:background .6s ease,color .4s ease; color:var(--site-text,#0b0c10)}
|
||||
.block{background:var(--site-card,#fff);border-radius:var(--radius-md);padding:var(--space-3);margin-bottom:var(--space-4);border:1px solid transparent;box-shadow:0 12px 30px rgba(15,23,42,.08);transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;animation:fadeUp .25s ease;color:var(--site-text,#0b0c10);position:relative;will-change:transform;cursor:grab}
|
||||
.canvas-inner{width:min(100%, var(--canvas-max, 1280px));margin:0 auto}
|
||||
.block{background:var(--site-block-bg,var(--site-card,#fff));border-radius:var(--site-block-radius,var(--radius-md));padding:var(--space-3);margin-bottom:var(--space-4);border:1px solid var(--site-block-border,transparent);box-shadow:var(--site-block-shadow,0 12px 30px rgba(15,23,42,.08));transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;animation:fadeUp .25s ease;color:var(--site-text,#0b0c10);position:relative;cursor:grab;touch-action:pan-y}
|
||||
.block.dragging,.block.resizing{transition:none;cursor:grabbing}
|
||||
body.dragging{user-select:none}
|
||||
.block:hover{transform:translateY(-2px);box-shadow:0 20px 46px rgba(15,23,42,.14)}
|
||||
.block.selected{border-color:#7aa7ff}
|
||||
.empty{padding:32px;border:1px dashed #cbd5e1;border-radius:12px;text-align:center;color:#64748b;background:#fff}
|
||||
.drop{height:8px;border-radius:6px;background:rgba(122,167,255,.4);margin:6px 0}
|
||||
.drag-handle{cursor:grab}
|
||||
.drag-handle{cursor:grab;touch-action:none}
|
||||
.block-drag-handle{
|
||||
position:absolute;left:8px;top:8px;z-index:3;
|
||||
width:24px;height:24px;border-radius:8px;border:1px solid #e5e7eb;
|
||||
@@ -134,31 +135,96 @@
|
||||
.contact-send{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:0;border-radius:10px;padding:10px 14px;background:var(--site-primary);color:#0b0f16;font-weight:700;cursor:pointer}
|
||||
.contact-send i{font-size:12px}
|
||||
@media (max-width:860px){.contact-pro{grid-template-columns:1fr}}
|
||||
.site-nav{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:10px 12px;border:1px solid #dde4ef;border-radius:var(--radius-md);background:rgba(255,255,255,.72);backdrop-filter:blur(8px)}
|
||||
.site-brand{display:flex;align-items:center;gap:10px;font-weight:800;letter-spacing:.2px;min-width:0}
|
||||
.site-nav{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:10px 12px;border:1px solid var(--site-nav-border,#dde4ef);border-radius:var(--site-nav-radius,var(--radius-md));background:var(--site-nav-bg,rgba(255,255,255,.72));backdrop-filter:blur(8px)}
|
||||
.site-brand{display:flex;align-items:center;gap:10px;font-weight:800;letter-spacing:.2px;min-width:120px;max-width:320px;flex:0 1 auto}
|
||||
.site-brand img{height:28px;width:auto;border-radius:8px;border:1px solid #dbe3ee}
|
||||
.site-brand-badge{height:28px;min-width:28px;padding:0 8px;border-radius:8px;background:var(--site-primary);display:inline-flex;align-items:center;justify-content:center;color:#0b0f16;font-weight:800;font-size:12px}
|
||||
.site-brand span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.site-nav-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:center}
|
||||
.site-nav-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;flex:1 1 auto}
|
||||
.site-nav-link{display:inline-flex;align-items:center;height:32px;padding:0 12px;border-radius:999px;border:1px solid transparent;text-decoration:none;font-size:13px;color:var(--site-text);transition:background .2s ease,border-color .2s ease,transform .2s ease}
|
||||
.site-nav-link:hover{background:#f1f5f9;border-color:#dbe3ee;transform:translateY(-1px)}
|
||||
.site-nav-cta{display:inline-flex;align-items:center;justify-content:center;padding:9px 14px;border-radius:999px;background:var(--site-primary);color:#0b0f16;text-decoration:none;font-weight:700;white-space:nowrap}
|
||||
.menu-empty{font-size:12px;color:var(--site-muted)}
|
||||
.menu-inline{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.menu-accordion{display:none;border:1px solid #e5e7eb;border-radius:var(--radius-md);padding:8px 10px;background:var(--site-card);width:100%}
|
||||
.menu-accordion summary{list-style:none;cursor:pointer;font-weight:600}
|
||||
.menu-drawer-toggle{display:none;align-items:center;justify-content:center;width:42px;height:42px;border-radius:10px;border:1px solid #2b2b2b;background:#202020;color:#f5f5f5;cursor:pointer}
|
||||
.menu-drawer-toggle:hover{border-color:#4b5563}
|
||||
.menu-drawer-overlay{display:none;position:fixed;inset:0;background:rgba(8,11,16,.5);z-index:1200}
|
||||
.menu-drawer{display:none;position:fixed;top:0;right:0;height:100vh;width:min(92vw,360px);background:#1f1f1f;color:#f5f5f5;border-left:1px solid #2d2d2d;z-index:1201;box-shadow:-14px 0 30px rgba(0,0,0,.25)}
|
||||
.menu-drawer.open,.menu-drawer-overlay.open{display:block}
|
||||
.menu-drawer-head{height:74px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #2d2d2d}
|
||||
.menu-drawer-brand{display:flex;align-items:center;gap:10px;font-weight:800;font-size:24px}
|
||||
.menu-drawer-close{width:42px;height:42px;border-radius:10px;border:2px solid #fbbf24;background:transparent;color:#f8fafc;font-size:22px;line-height:1;cursor:pointer}
|
||||
.menu-drawer-links{display:grid;gap:16px;padding:34px 28px}
|
||||
.menu-drawer-links .site-nav-link{height:auto;padding:0;border:0;border-radius:0;color:#f8fafc;font-size:clamp(22px,4vw,34px);line-height:1.15}
|
||||
.menu-drawer-links .site-nav-link:hover{background:transparent;border:0;transform:none;color:#cbd5e1}
|
||||
.menu-accordion{display:none;border:1px solid #e5e7eb;border-radius:var(--radius-md);padding:8px 10px;background:var(--site-card);width:auto;min-width:220px;max-width:420px;margin-left:auto;flex:0 1 auto}
|
||||
.menu-accordion summary{list-style:none;cursor:pointer;font-weight:600;touch-action:manipulation}
|
||||
.menu-accordion summary::-webkit-details-marker{display:none}
|
||||
.menu-links{display:flex;flex-direction:column;gap:8px;margin-top:8px}
|
||||
.menu-accordion summary::after{content:"v";float:right;color:var(--site-muted)}
|
||||
.edu-nav{display:grid;grid-template-columns:auto 1fr auto;gap:16px;align-items:center;padding:14px 16px;border:1px solid #d8e2f2;border-radius:16px;background:#ffffff}
|
||||
.edu-nav-links{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
|
||||
.edu-nav-link{padding:9px 12px;border-radius:10px;text-decoration:none;color:#274168;font-weight:600;font-size:13px}
|
||||
.edu-nav-link:hover{background:#eef4ff;color:#0a4dcf}
|
||||
.edu-nav-cta{padding:10px 14px;border-radius:10px;background:#0a4dcf;color:#fff;text-decoration:none;font-weight:700;font-size:13px}
|
||||
.edu-hero{display:grid;grid-template-columns:minmax(320px,1.15fr) minmax(280px,.85fr);gap:18px;align-items:stretch}
|
||||
.edu-hero-copy{padding:6px 4px}
|
||||
.edu-kicker{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;background:#e9f0ff;color:#0a4dcf;font-size:11px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;margin-bottom:10px}
|
||||
.edu-hero h2.editable{font-size:clamp(36px,4.8vw,62px);line-height:1.03;letter-spacing:-.5px;margin:0 0 10px}
|
||||
.edu-hero p.editable{font-size:20px;line-height:1.45;color:#3e5378;max-width:58ch}
|
||||
.edu-hero-panel{background:#ffffff;border:1px solid #d8e2f2;border-radius:16px;padding:16px;display:grid;gap:12px}
|
||||
.edu-stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||
.edu-stat{border:1px solid #e3ebf8;border-radius:12px;padding:12px;background:#f8fbff}
|
||||
.edu-stat strong{display:block;font-size:22px;color:#0a4dcf}
|
||||
.edu-stat span{font-size:12px;color:#4f6286}
|
||||
.edu-step-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}
|
||||
.edu-step{background:#fff;border:1px solid #d9e4f5;border-radius:14px;padding:14px}
|
||||
.edu-step-num{width:30px;height:30px;border-radius:999px;background:#0a4dcf;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;margin-bottom:8px}
|
||||
.edu-cards-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
|
||||
.edu-card{background:#fff;border:1px solid #d9e4f5;border-radius:14px;padding:16px}
|
||||
.edu-card .card-pro-title{font-size:22px;line-height:1.15;color:#0b1733}
|
||||
.edu-card .card-pro-desc{font-size:14px;line-height:1.6;color:#41557a}
|
||||
.education-site{padding:26px;background:linear-gradient(180deg,#f3f7ff 0%,#eef3fb 100%);border-radius:18px}
|
||||
.education-site .block{background:transparent;border:0;box-shadow:none;padding:0}
|
||||
.education-site .block[data-block-type="menu"]{margin-bottom:6px}
|
||||
.education-site .block[data-block-type="hero"]{background:linear-gradient(140deg,#ffffff 0%,#f6f9ff 100%);border:1px solid #d8e2f2;border-radius:18px;padding:22px}
|
||||
.education-site .block[data-block-type="cards"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
||||
.education-site .block[data-block-type="iconlist"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
||||
.education-site .block[data-block-type="features"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
||||
.education-site .block[data-block-type="gallery"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
||||
.education-site .block[data-block-type="review"]{background:linear-gradient(140deg,#0a4dcf 0%,#123c8a 100%);color:#fff;border-radius:18px;padding:20px}
|
||||
.education-site .block[data-block-type="review"] .editable{color:#fff}
|
||||
.education-site .block[data-block-type="review"] i{color:#ffd86b !important}
|
||||
.education-site .block[data-block-type="contact"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
||||
.education-site .block[data-block-type="map"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
||||
.education-site .block h3{font-size:32px;line-height:1.1;margin-bottom:14px !important;letter-spacing:-.3px}
|
||||
.education-site .edu-nav-link{border:1px solid transparent}
|
||||
.education-site .edu-nav-link:hover{border-color:#cddcf6;background:#f3f7ff}
|
||||
.education-site .edu-hero{position:relative}
|
||||
.education-site .edu-card:nth-child(1){border-top:4px solid #0a4dcf}
|
||||
.education-site .edu-card:nth-child(2){border-top:4px solid #1f6feb}
|
||||
.education-site .edu-card:nth-child(3){border-top:4px solid #58a6ff}
|
||||
.education-site .edu-step{position:relative;overflow:hidden}
|
||||
.education-site .edu-step:after{content:"";position:absolute;right:-26px;top:-26px;width:90px;height:90px;border-radius:999px;background:rgba(10,77,207,.07)}
|
||||
.edu-apply{border:1px solid #d8e2f2;background:#fff;border-radius:14px;padding:12px}
|
||||
.edu-apply h4{margin:0 0 8px;font-size:14px;color:#17356e}
|
||||
.edu-apply ul{margin:0;padding-left:18px;color:#38527f;font-size:13px;line-height:1.5}
|
||||
.edu-apply .deadline{display:inline-flex;margin-top:8px;padding:4px 8px;border-radius:999px;background:#e9f0ff;color:#0a4dcf;font-size:11px;font-weight:700}
|
||||
@media (max-width:980px){.edu-hero{grid-template-columns:1fr}}
|
||||
.site-global-footer{margin-top:26px;padding:16px 18px;border-radius:12px;border:1px solid #dbe3ee;background:rgba(255,255,255,.72);color:var(--site-muted);font-size:12px;line-height:1.4;text-align:center;width:100%;grid-column:1 / -1;justify-self:stretch}
|
||||
.float-whatsapp{position:fixed;right:22px;bottom:22px;width:52px;height:52px;border-radius:999px;background:#25d366;color:#fff;display:flex;align-items:center;justify-content:center;box-shadow:0 14px 30px rgba(0,0,0,.22);z-index:999;text-decoration:none}
|
||||
.float-whatsapp:hover{transform:translateY(-2px)}
|
||||
@media (max-width:980px){
|
||||
.menu-inline{display:none}
|
||||
.menu-accordion{display:block}
|
||||
.menu-drawer-toggle{display:inline-flex}
|
||||
.menu-accordion{display:block;width:100%;min-width:0;max-width:none;margin-left:0}
|
||||
}
|
||||
.preview-shell.size-phone .menu-inline,
|
||||
.preview-shell.size-tablet .menu-inline{display:none}
|
||||
.preview-shell.size-phone .menu-drawer-toggle,
|
||||
.preview-shell.size-tablet .menu-drawer-toggle{display:inline-flex}
|
||||
.preview-shell.size-phone .menu-accordion,
|
||||
.preview-shell.size-tablet .menu-accordion{display:block}
|
||||
.preview-shell.size-tablet .menu-accordion{display:block;width:100%;min-width:0;max-width:none;margin-left:0}
|
||||
.preview-shell.size-phone .hero-layout,
|
||||
.preview-shell.size-tablet .hero-layout{grid-template-columns:1fr}
|
||||
.preview-shell.size-phone .hero-media,
|
||||
@@ -236,7 +302,7 @@
|
||||
body.ub24 input, body.ub24 textarea, body.ub24 select{background:#f9fafb;color:#0b0f16;border-color:#e5e7eb}
|
||||
</style>
|
||||
</head>
|
||||
<body class="{{ 'ub24' if builder_mode == 'ub24' else '' }}">
|
||||
<body class="{{ 'ub24' if builder_mode == 'ub24' else '' }} {{ 'preview-mode' if preview_only else '' }}">
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<div class="brand"><span class="dot"></span>{{ 'Page Builder' if builder_mode == 'ub24' else 'GKACHELE Builder' }}</div>
|
||||
@@ -260,27 +326,31 @@
|
||||
</aside>
|
||||
<main class="main">
|
||||
<div class="topbar">
|
||||
<div style="font-weight:600">{{ 'Nueva pagina' if builder_mode == 'ub24' else 'Pagina Apple' }}</div>
|
||||
<div style="font-weight:600">{{ 'Nueva pagina' if builder_mode == 'ub24' else 'Page Builder' }}</div>
|
||||
<div style="color:var(--muted);font-size:12px">Arrastra bloques desde la izquierda.</div>
|
||||
<div class="top-meta" id="blockCount">Bloques: 0 (Ilimitado)</div>
|
||||
<div style="margin-left:auto;display:flex;gap:8px;align-items:center">
|
||||
{% if builder_mode == 'ub24' %}
|
||||
<select id="pageSelect" class="top-select">
|
||||
<option value="home">Inicio</option>
|
||||
<option value="servicios">Servicios</option>
|
||||
<option value="servicios">Seccion 2</option>
|
||||
<option value="galeria">Galeria</option>
|
||||
<option value="contacto">Contacto</option>
|
||||
</select>
|
||||
{% endif %}
|
||||
<select id="templateSelect" class="top-select">
|
||||
<option value="">Plantillas</option>
|
||||
<option value="servicios">Servicios</option>
|
||||
<option value="industrial">Industrial</option>
|
||||
<option value="">Plantillas por rubro</option>
|
||||
<option value="restaurante">Restaurante</option>
|
||||
<option value="streaming">Streaming</option>
|
||||
<option value="danza">Danza</option>
|
||||
<option value="cosmeticos">Cosméticos</option>
|
||||
<option value="despachos">Despachos</option>
|
||||
<option value="gimnasios">Gimnasios</option>
|
||||
<option value="educacion">Educación</option>
|
||||
<option value="base_otro">Base (Otro)</option>
|
||||
</select>
|
||||
<button class="btn secondary" id="btnBack">Atras</button>
|
||||
<button class="btn secondary" id="btnPreview">Vista previa</button>
|
||||
<button class="btn secondary" id="btnReset">Reset</button>
|
||||
<button class="btn secondary" id="btnTheme">Claro</button>
|
||||
<button class="btn secondary icon" id="btnSizePhone" title="Movil"><i class="fa-solid fa-mobile-screen-button"></i></button>
|
||||
<button class="btn secondary icon" id="btnSizeTablet" title="Tablet"><i class="fa-solid fa-tablet-screen-button"></i></button>
|
||||
@@ -386,6 +456,8 @@
|
||||
<div class="row"><label>Animaciones</label><input id="animToggle" type="checkbox"></div>
|
||||
<div class="row"><label>Alto minimo pagina (px)</label><input id="canvasMinHeightInput" type="number" min="700" max="5000" step="50"></div>
|
||||
<div class="row"><label>Espacio inferior (px)</label><input id="canvasBottomSpaceInput" type="number" min="0" max="1200" step="10"></div>
|
||||
<div class="row"><label>Marca registrada (footer)</label><input id="registeredBrandInput" type="text" placeholder="Tu marca registrada"></div>
|
||||
<div class="row"><label>Empresa desarrolladora (footer)</label><input id="developerBrandInput" type="text" placeholder="GKACHELE™"></div>
|
||||
<div class="row"><label>Creado por</label><input id="siteAuthorInput" type="text" placeholder="Tu nombre o marca"></div>
|
||||
</div>
|
||||
</details>
|
||||
@@ -398,6 +470,18 @@
|
||||
const SITE_SLUG = "{{ slug }}";
|
||||
const SERVER_CONTENT = {{ content|tojson }};
|
||||
const BUILDER_MODE = "{{ builder_mode or 'default' }}";
|
||||
const PREVIEW_ONLY = {{ 'true' if preview_only else 'false' }};
|
||||
const SERVER_RUBRO = "{{ rubro|default('restaurante') }}";
|
||||
const BUILDER_BASE_PATH = BUILDER_MODE === "ub24" ? `/ub24/${SITE_ID}` : `/elementor/${SITE_ID}`;
|
||||
const OFFICIAL_RUBROS = [
|
||||
{ value: "restaurante", label: "Restaurante" },
|
||||
{ value: "danza", label: "Danza" },
|
||||
{ value: "cosmeticos", label: "Cosméticos" },
|
||||
{ value: "despachos", label: "Despachos" },
|
||||
{ value: "gimnasios", label: "Gimnasios" },
|
||||
{ value: "educacion", label: "Educación" },
|
||||
{ value: "base_otro", label: "Base (Otro)" }
|
||||
];
|
||||
const defaultSettings = {
|
||||
site_name: "{{ slug }}",
|
||||
primary_color: "#59d9c8",
|
||||
@@ -418,30 +502,33 @@
|
||||
bg_video_url: "",
|
||||
canvas_min_height: 1200,
|
||||
canvas_bottom_space: 180,
|
||||
site_author: ""
|
||||
site_author: "",
|
||||
registered_brand: "",
|
||||
developer_brand: "GKACHELE™",
|
||||
business_rubro: "restaurante"
|
||||
};
|
||||
const templates = {
|
||||
servicios: {
|
||||
base_otro: {
|
||||
settings: { primary_color: '#1d4ed8', bg_color: '#f6f7fb', text_color: '#0b0c10', muted_color: '#6b7280', font_body: 'Manrope', font_heading: 'Manrope', bg_gradient: true, bg_color2: '#e9eef5' },
|
||||
blocks: [
|
||||
{ id: makeId(), type: 'menu', data: defaultData('menu') },
|
||||
{ id: makeId(), type: 'hero', data: { title: 'Servicios profesionales para tu negocio', subtitle: 'Crecemos contigo con soluciones claras y resultados medibles.', button_text: 'Cotizar ahora', button_url: '#contacto', image_url: '' } },
|
||||
{ id: makeId(), type: 'hero', data: { title: 'Solucion profesional para tu negocio', subtitle: 'Crecemos contigo con ejecucion clara y resultados medibles.', button_text: 'Cotizar ahora', button_url: '#contacto', image_url: '' } },
|
||||
{ id: makeId(), type: 'features', data: { title: 'Beneficios', items: ['Rapido','Profesional','Confiable'] } },
|
||||
{ id: makeId(), type: 'cards', data: { title: 'Servicios', items: ['Consultoria|Estrategia y ejecucion','Marketing|Crecimiento real','Soporte|Atencion prioritaria'] } },
|
||||
{ id: makeId(), type: 'cards', data: { title: 'Propuesta', items: ['Consultoria|Estrategia y ejecucion','Marketing|Crecimiento real','Soporte|Atencion prioritaria'] } },
|
||||
{ id: makeId(), type: 'gallery', data: { title: 'Proyectos', images: ['','',''], captions: ['','',''], fit: 'cover' } },
|
||||
{ id: makeId(), type: 'contact', data: { title: 'Hablemos', email: '', phone: '', address: '' } },
|
||||
{ id: makeId(), type: 'social', data: defaultData('social') }
|
||||
]
|
||||
},
|
||||
industrial: {
|
||||
gimnasios: {
|
||||
settings: { primary_color: '#f97316', bg_color: '#f8fafc', text_color: '#0b0c10', muted_color: '#6b7280', font_body: 'IBM Plex Sans', font_heading: 'Space Grotesk', bg_gradient: false },
|
||||
blocks: [
|
||||
{ id: makeId(), type: 'menu', data: defaultData('menu') },
|
||||
{ id: makeId(), type: 'hero', data: { title: 'Soluciones industriales confiables', subtitle: 'Calidad, seguridad y cumplimiento en cada proyecto.', button_text: 'Solicitar presupuesto', button_url: '#contacto', image_url: '' } },
|
||||
{ id: makeId(), type: 'iconlist', data: { title: 'Diferenciales', items: ['Certificados|Normas y seguridad','Experiencia|Equipos expertos','Entrega|Plazos claros'] } },
|
||||
{ id: makeId(), type: 'features', data: { title: 'Servicios', items: ['Mantenimiento','Montajes','Ingenieria'] } },
|
||||
{ id: makeId(), type: 'gallery', data: { title: 'Proyectos', images: ['','',''], captions: ['','',''], fit: 'cover' } },
|
||||
{ id: makeId(), type: 'contact', data: { title: 'Contacto', email: '', phone: '', address: '' } }
|
||||
{ id: makeId(), type: 'hero', data: { title: 'Entrena mejor con acompañamiento profesional', subtitle: 'Planes personalizados, equipos modernos y seguimiento real.', button_text: 'Prueba gratis', button_url: '#contacto', image_url: '' } },
|
||||
{ id: makeId(), type: 'iconlist', data: { title: 'Diferenciales', items: ['Entrenadores|Acompañamiento experto','Programas|Objetivos medibles','Comunidad|Motivación constante'] } },
|
||||
{ id: makeId(), type: 'features', data: { title: 'Áreas', items: ['Fuerza','Cardio','Funcional'] } },
|
||||
{ id: makeId(), type: 'gallery', data: { title: 'Instalaciones', images: ['','',''], captions: ['','',''], fit: 'cover' } },
|
||||
{ id: makeId(), type: 'contact', data: { title: 'Inscripción', email: '', phone: '', address: '' } }
|
||||
]
|
||||
},
|
||||
restaurante: {
|
||||
@@ -456,7 +543,7 @@
|
||||
{ id: makeId(), type: 'map', data: { title: 'Ubicacion', address: '' } }
|
||||
]
|
||||
},
|
||||
streaming: {
|
||||
cosmeticos: {
|
||||
settings: { primary_color: '#4f8cff', bg_color: '#0b0f16', text_color: '#e7ebf0', muted_color: '#98a3b6', font_body: 'DM Sans', font_heading: 'Space Grotesk', bg_gradient: true, bg_color2: '#111827' },
|
||||
blocks: [
|
||||
{ id: makeId(), type: 'menu', data: defaultData('menu') },
|
||||
@@ -466,6 +553,44 @@
|
||||
{ id: makeId(), type: 'button', data: { text: 'Crear mi cuenta', url: '#contacto', style: 'primary', size: 'lg' } },
|
||||
{ id: makeId(), type: 'contact', data: { title: 'Contacto', email: '', phone: '', address: '' } }
|
||||
]
|
||||
},
|
||||
danza: {
|
||||
settings: { primary_color: '#7c3aed', bg_color: '#f8f5ff', text_color: '#160f29', muted_color: '#6d5c8f', font_body: 'Outfit', font_heading: 'Playfair Display', bg_gradient: true, bg_color2: '#efe7ff' },
|
||||
blocks: [
|
||||
{ id: makeId(), type: 'menu', data: defaultData('menu') },
|
||||
{ id: makeId(), type: 'hero', data: { title: 'Escuela de danza con enfoque escénico', subtitle: 'Formación técnica, expresión artística y montaje profesional.', button_text: 'Agendar clase', button_url: '#contacto', image_url: '' } },
|
||||
{ id: makeId(), type: 'video', data: { url: '', description: 'Video de presentacion' } },
|
||||
{ id: makeId(), type: 'features', data: { title: 'Programas', items: ['Iniciación','Intermedio','Avanzado'] } },
|
||||
{ id: makeId(), type: 'calendar', data: { title: 'Horarios', note: 'Consulta disponibilidad semanal.', embed_url: '' } },
|
||||
{ id: makeId(), type: 'contact', data: { title: 'Inscripciones', email: '', phone: '', address: '' } }
|
||||
]
|
||||
},
|
||||
despachos: {
|
||||
settings: { primary_color: '#0f766e', bg_color: '#f7faf9', text_color: '#0b0c10', muted_color: '#54656a', font_body: 'IBM Plex Sans', font_heading: 'Merriweather', bg_gradient: false },
|
||||
blocks: [
|
||||
{ id: makeId(), type: 'menu', data: defaultData('menu') },
|
||||
{ id: makeId(), type: 'hero', data: { title: 'Asesoría estratégica para decisiones seguras', subtitle: 'Despacho especializado en gestión legal y administrativa.', button_text: 'Solicitar consulta', button_url: '#contacto', image_url: '' } },
|
||||
{ id: makeId(), type: 'iconlist', data: { title: 'Áreas', items: ['Corporativo|Acompañamiento empresarial','Tributario|Planificación y cumplimiento','Laboral|Prevención y defensa'] } },
|
||||
{ id: makeId(), type: 'review', data: { title: 'Casos', name: 'Cliente empresarial', text: 'Resolución eficiente y comunicación clara en cada etapa.', rating: 5, style: 'quote' } },
|
||||
{ id: makeId(), type: 'contact', data: { title: 'Contacto', email: '', phone: '', address: '' } }
|
||||
]
|
||||
},
|
||||
educacion: {
|
||||
settings: { primary_color: '#0a4dcf', bg_color: '#f4f7ff', text_color: '#0b1733', muted_color: '#4f6286', font_body: 'IBM Plex Sans', font_heading: 'Space Grotesk', bg_gradient: true, bg_color2: '#dbe8ff' },
|
||||
blocks: [
|
||||
{ id: makeId(), type: 'menu', data: { title: 'Academia', items: ['Inicio','Programas','Admisiones','Campus','Contacto'], menu_mode: 'both', menu_mobile_style: 'accordion', width: 100 } },
|
||||
{ id: makeId(), type: 'hero', data: { title: 'Formacion academica con enfoque profesional', subtitle: 'Desarrolla competencias reales con docentes expertos, laboratorios modernos y acompanamiento continuo.', button_text: 'Iniciar admision', button_url: '#contacto', image_url: '' } },
|
||||
{ id: makeId(), type: 'cards', data: { title: 'Programas destacados', items: ['Pregrado|Carreras con plan curricular actualizado y enfoque en empleabilidad.','Diplomados|Especializacion intensiva para perfiles tecnicos y profesionales.','Educacion continua|Trayectos cortos para actualizar habilidades de alto impacto.'] } },
|
||||
{ id: makeId(), type: 'iconlist', data: { title: 'Ruta de admision', items: ['Postula online|Completa tu solicitud y adjunta documentos en minutos.','Entrevista academica|Recibe orientacion personalizada segun tu perfil.','Matricula guiada|Formaliza tu ingreso con soporte del equipo academico.'] } },
|
||||
{ id: makeId(), type: 'features', data: { title: 'Por que elegirnos', items: ['Docentes activos en industria','Modelo hibrido flexible','Convenios empresariales','Tutoria academica permanente'] } },
|
||||
{ id: makeId(), type: 'gallery', data: { title: 'Vida universitaria', images: ['','',''], captions: ['Laboratorios y practica aplicada','Clases colaborativas','Eventos academicos y comunidad'], fit: 'cover' } },
|
||||
{ id: makeId(), type: 'calendar', data: { title: 'Fechas clave', note: 'Convocatoria abierta. Inicio de clases y cronograma de admision disponible para edicion.', embed_url: '' } },
|
||||
{ id: makeId(), type: 'review', data: { title: 'Historias de estudiantes', name: 'Estudiante de Ingenieria', text: 'La metodologia es practica y exigente; pude aplicar lo aprendido desde el primer ciclo.', rating: 5, style: 'quote' } },
|
||||
{ id: makeId(), type: 'button', data: { text: 'Descargar brochure academico', url: '#contacto', style: 'primary', size: 'lg' } },
|
||||
{ id: makeId(), type: 'contact', data: { title: 'Oficina de admisiones', email: 'admisiones@tuinstitucion.edu', phone: '+54 9 11 0000 0000', address: 'Campus Central - Av. Principal 123' } },
|
||||
{ id: makeId(), type: 'map', data: { title: 'Visita el campus', address: 'Campus Central - Av. Principal 123' } },
|
||||
{ id: makeId(), type: 'social', data: { instagram: '', facebook: '', whatsapp: '', tiktok: '', youtube: '', icon_size: 18, icon_color: '#0b1733', show_text: true, icon_style: 'pill', width: 100 } }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -503,7 +628,7 @@ const state = {
|
||||
function makeId(){ return "block_" + Date.now() + "_" + Math.floor(Math.random()*1000); }
|
||||
function snapBlockWidth(type, raw){
|
||||
if (type === "menu") return 100;
|
||||
return Math.max(30, Math.min(100, Number(raw || 50)));
|
||||
return Math.max(30, Math.min(100, Number(raw || 100)));
|
||||
}
|
||||
function getDefaultPos(){
|
||||
const base = 20;
|
||||
@@ -513,13 +638,13 @@ const state = {
|
||||
}
|
||||
function defaultData(type){
|
||||
switch(type){
|
||||
case "menu": return { title:"Menu", items:[], menu_mode:"both", width:100 };
|
||||
case "menu": return { title:"Menu", items:["Inicio","Productos","Blog","La Empresa","Contacto"], menu_mode:"both", menu_mobile_style:"accordion", width:100 };
|
||||
case "hero": return { title:"Tu propuesta de valor", subtitle:"Explica en una frase por que elegirte.", button_text:"Contactar", button_url:"#contacto", image_url:"", width:100 };
|
||||
case "text": return { text:"Describe tu negocio aqui.", width:100 };
|
||||
case "image": return { url:"", alt:"", caption:"", fit:"cover", overlay_text:"", width:100 };
|
||||
case "features": return { title:"Beneficios", items:["Rapido","Profesional","Confiable"], width:100 };
|
||||
case "gallery": return { title:"Proyectos", images:["","",""], captions:["","",""], fit:"cover", width:100 };
|
||||
case "cards": return { title:"Servicios", items:["Titulo 1|Texto breve","Titulo 2|Texto breve","Titulo 3|Texto breve"], width:100 };
|
||||
case "cards": return { title:"Propuesta", items:["Titulo 1|Texto breve","Titulo 2|Texto breve","Titulo 3|Texto breve"], width:100 };
|
||||
case "iconlist": return { title:"Diferenciales", items:["Rapido|Ahorra tiempo","Seguro|Datos protegidos","Soporte|Respuesta rapida"], width:100 };
|
||||
case "contact": return { title:"Hablemos", email:"", phone:"", address:"", width:100 };
|
||||
case "map": return { title:"Ubicacion", address:"", width:100 };
|
||||
@@ -531,6 +656,42 @@ const state = {
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
function normalizeMenuMode(value){
|
||||
const raw = String(value || "both").trim().toLowerCase();
|
||||
if (raw === "acordeon" || raw === "accordion") return "accordion";
|
||||
if (raw === "horizontal" || raw === "inline") return "inline";
|
||||
if (raw === "ambos" || raw === "both") return "both";
|
||||
return "both";
|
||||
}
|
||||
function normalizeKey(value){
|
||||
return String(value || "")
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
function findFirstBlockIdByTypes(types){
|
||||
const set = new Set(types || []);
|
||||
const found = state.blocks.find((b)=>b && b.type !== "menu" && set.has(b.type));
|
||||
return found ? found.id : "";
|
||||
}
|
||||
function resolveEducationMenuTarget(label){
|
||||
const key = normalizeKey(label);
|
||||
if (key.includes("inicio")) return findFirstBlockIdByTypes(["hero","cards","features"]);
|
||||
if (key.includes("program")) return findFirstBlockIdByTypes(["cards","features"]);
|
||||
if (key.includes("admi")) return findFirstBlockIdByTypes(["iconlist","calendar","review"]);
|
||||
if (key.includes("campus") || key.includes("galer")) return findFirstBlockIdByTypes(["gallery","map"]);
|
||||
if (key.includes("contact")) return findFirstBlockIdByTypes(["contact","map"]);
|
||||
return findFirstBlockIdByTypes(["hero","cards","features","iconlist","gallery","contact","map"]);
|
||||
}
|
||||
function isEducationRubro(){
|
||||
return normalizeRubro(state?.settings?.business_rubro || SERVER_RUBRO || "") === "educacion";
|
||||
}
|
||||
function normalizeRubro(value){
|
||||
const raw = String(value || "").trim().toLowerCase();
|
||||
const allowed = new Set(OFFICIAL_RUBROS.map((r)=>r.value));
|
||||
return allowed.has(raw) ? raw : "restaurante";
|
||||
}
|
||||
function repairMojibake(text){
|
||||
const raw = String(text || "");
|
||||
if (!/[ÃÂ]/.test(raw)) return raw;
|
||||
@@ -725,28 +886,106 @@ const state = {
|
||||
|
||||
function renderBlockHtml(block){
|
||||
if (block.type==="menu"){
|
||||
const items = state.blocks
|
||||
.filter(b=>b.type!=="menu")
|
||||
.map((b, i)=>{
|
||||
const title = (b.data && (b.data.title || b.data.text || b.type)) || b.type;
|
||||
return { id: b.id, label: String(title).slice(0, 24), index: i+1 };
|
||||
});
|
||||
const links = items.map(it=>`<a class="site-nav-link" href="#${it.id}">${escapeHtml(it.label)}</a>`).join("");
|
||||
const mobileLinks = items.map(it=>`<a class="site-nav-link" href="#${it.id}">${escapeHtml(it.label)}</a>`).join("");
|
||||
const logo = state.settings.logo_url ? `<img src="${escapeHtml(state.settings.logo_url)}" alt="Logo" />` : "";
|
||||
const mode = (block.data?.menu_mode || "both").toLowerCase();
|
||||
if (isEducationRubro()){
|
||||
const items = Array.isArray(block.data?.items) && block.data.items.length ? block.data.items : ["Inicio","Programas","Admisiones","Campus","Contacto"];
|
||||
const links = items.map((it, idx)=>{
|
||||
const target = resolveEducationMenuTarget(it);
|
||||
const href = target ? `#${target}` : "#";
|
||||
return `<a class="edu-nav-link editable" data-field="items.${idx}" data-placeholder="Item menu" contenteditable="true" href="${href}">${escapeHtml(it)}</a>`;
|
||||
}).join("");
|
||||
const contactTarget = resolveEducationMenuTarget("contacto");
|
||||
const contactHref = contactTarget ? `#${contactTarget}` : "#";
|
||||
const brand = state.settings.site_name || "Academia";
|
||||
return `<div class="edu-nav">
|
||||
<div class="site-brand">${state.settings.logo_url ? `<img src="${escapeHtml(state.settings.logo_url)}" alt="Logo" />` : `<span class="site-brand-badge">${escapeHtml(String(brand).slice(0,1).toUpperCase())}</span>`}<span>${escapeHtml(brand)}</span></div>
|
||||
<div class="edu-nav-links">${links}</div>
|
||||
<a href="${contactHref}" class="edu-nav-cta">Postular ahora</a>
|
||||
</div>`;
|
||||
}
|
||||
const manualItems = Array.isArray(block.data?.items) ? block.data.items.map((x)=>String(x || "").trim()).filter(Boolean) : [];
|
||||
const items = manualItems.length
|
||||
? (manualItems.length >= 3
|
||||
? manualItems.map((label, i)=>({ id: `menu_item_${i+1}`, label: String(label).slice(0, 24), index: i+1 }))
|
||||
: [])
|
||||
: state.blocks
|
||||
.filter(b=>b.type!=="menu")
|
||||
.map((b, i)=>{
|
||||
const title = (b.data && (b.data.title || b.data.text || b.type)) || b.type;
|
||||
return { id: b.id, label: String(title).slice(0, 24), index: i+1 };
|
||||
});
|
||||
const safeItems = items.length ? items : [
|
||||
{ id: "menu_item_1", label: "Inicio", index: 1 },
|
||||
{ id: "menu_item_2", label: "Productos", index: 2 },
|
||||
{ id: "menu_item_3", label: "Contacto", index: 3 }
|
||||
];
|
||||
const links = safeItems.map(it=>`<a class="site-nav-link" href="#${it.id}">${escapeHtml(it.label)}</a>`).join("");
|
||||
const mobileLinks = safeItems.map(it=>`<a class="site-nav-link" href="#${it.id}" data-drawer-link="1">${escapeHtml(it.label)}</a>`).join("");
|
||||
const siteName = String(state.settings.site_name || "GKACHELE");
|
||||
const initial = escapeHtml(siteName.slice(0, 1).toUpperCase());
|
||||
const logo = state.settings.logo_url
|
||||
? `<img src="${escapeHtml(state.settings.logo_url)}" alt="Logo" />`
|
||||
: `<span class="site-brand-badge">${initial}</span>`;
|
||||
const mode = normalizeMenuMode(block.data?.menu_mode || "both");
|
||||
const mobileStyle = (block.data?.menu_mobile_style || "accordion").toLowerCase() === "drawer" ? "drawer" : "accordion";
|
||||
const showInline = mode === "both" || mode === "inline";
|
||||
const showAccordion = mode === "both" || mode === "accordion";
|
||||
const showDrawer = showAccordion && mobileStyle === "drawer";
|
||||
const menuDrawerId = `drawer_${block.id}`;
|
||||
const menuOverlayId = `drawer_overlay_${block.id}`;
|
||||
return `<div class="site-nav">
|
||||
<div class="site-brand">${logo}<span>${escapeHtml(state.settings.site_name||"GKACHELE")}</span></div>
|
||||
<div class="site-brand">${logo}<span>${escapeHtml(siteName)}</span></div>
|
||||
<div class="menu-inline site-nav-links" style="${showInline ? "" : "display:none"}">${links}</div>
|
||||
<details class="menu-accordion" style="${showAccordion ? "" : "display:none"}">
|
||||
<button class="menu-drawer-toggle" type="button" data-drawer-toggle="${menuDrawerId}" style="${showDrawer ? "" : "display:none"}"><i class="fa-solid fa-bars"></i></button>
|
||||
<details class="menu-accordion" style="${showAccordion && !showDrawer ? "" : "display:none"}">
|
||||
<summary>Menu</summary>
|
||||
<div class="menu-links">${mobileLinks}</div>
|
||||
</details>
|
||||
<div class="menu-drawer-overlay menu-drawer-global" id="${menuOverlayId}" data-drawer-overlay="${menuDrawerId}"></div>
|
||||
<aside class="menu-drawer menu-drawer-global" id="${menuDrawerId}">
|
||||
<div class="menu-drawer-head">
|
||||
<div class="menu-drawer-brand">${logo}<span>${escapeHtml(siteName)}</span></div>
|
||||
<button class="menu-drawer-close" type="button" data-drawer-close="${menuDrawerId}"><i class="fa-solid fa-xmark"></i></button>
|
||||
</div>
|
||||
<nav class="menu-drawer-links">${mobileLinks}</nav>
|
||||
</aside>
|
||||
</div>`;
|
||||
}
|
||||
if (block.type==="hero"){
|
||||
if (isEducationRubro()){
|
||||
const admissionTarget = resolveEducationMenuTarget("admisiones");
|
||||
const programsTarget = resolveEducationMenuTarget("programas");
|
||||
const admissionHref = admissionTarget ? `#${admissionTarget}` : "#";
|
||||
const programsHref = programsTarget ? `#${programsTarget}` : "#";
|
||||
return `<div class="edu-hero">
|
||||
<div class="edu-hero-copy">
|
||||
<div class="edu-kicker">Admisiones 2026 abiertas</div>
|
||||
${editable("h2","title",block.data.title,"Titulo",false,"")}
|
||||
${editable("p","subtitle",block.data.subtitle,"Subtitulo",true,"")}
|
||||
<div class="hero-actions">
|
||||
<a href="${admissionHref}" class="editable hero-cta" data-field="button_text" data-placeholder="Boton" contenteditable="true">${escapeHtml(block.data.button_text || "Iniciar admision")}</a>
|
||||
<a href="${programsHref}" class="hero-cta-secondary">Ver programas</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edu-hero-panel">
|
||||
<div class="edu-stat-grid">
|
||||
<div class="edu-stat"><strong>+4,200</strong><span>Estudiantes activos</span></div>
|
||||
<div class="edu-stat"><strong>92%</strong><span>Insercion laboral</span></div>
|
||||
<div class="edu-stat"><strong>+80</strong><span>Docentes especialistas</span></div>
|
||||
<div class="edu-stat"><strong>35</strong><span>Convenios empresariales</span></div>
|
||||
</div>
|
||||
<div class="edu-apply">
|
||||
<h4>Proceso de postulacion</h4>
|
||||
<ul>
|
||||
<li>Registro en linea y carga de documentos.</li>
|
||||
<li>Entrevista de orientacion academica.</li>
|
||||
<li>Evaluacion y confirmacion de vacante.</li>
|
||||
</ul>
|
||||
<span class="deadline">Cierre de convocatoria: 30 marzo</span>
|
||||
</div>
|
||||
<div class="hero-media" style="min-height:180px">${block.data.image_url ? `<img src="${escapeHtml(block.data.image_url)}" alt="Campus">` : `<div class="hero-media-empty">Agrega imagen institucional para completar la portada.</div>`}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
const image = block.data.image_url ? `<img src="${escapeHtml(block.data.image_url)}" alt="Hero image">` : `<div class="hero-media-empty">Agrega imagen en "Imagen URL" para completar el hero.</div>`;
|
||||
return `<div class="hero-pro hero-layout">
|
||||
<div class="hero-copy">
|
||||
@@ -775,6 +1014,9 @@ const state = {
|
||||
}
|
||||
if (block.type==="features"){
|
||||
const items = Array.isArray(block.data.items)?block.data.items:[];
|
||||
if (isEducationRubro()){
|
||||
return `${editable("h3","title",block.data.title,"Titulo",false,"margin:0 0 12px;font-size:28px")}<div class="edu-step-grid">${items.map((i,idx)=>`<div class="edu-step"><span class="edu-step-num">${idx+1}</span><div class="editable" data-field="items.${idx}" data-placeholder="Item" contenteditable="true" style="font-weight:600;color:#0b1733;line-height:1.45">${escapeHtml(i)}</div></div>`).join("")}</div>`;
|
||||
}
|
||||
return `${editable("h3","title",block.data.title,"Titulo",false,"margin:0 0 10px")}<div class="feature-grid">${items.map((i,idx)=>`<div class="editable feature-pill" data-field="items.${idx}" data-placeholder="Item" contenteditable="true">${escapeHtml(i)}</div>`).join("")}</div>`;
|
||||
}
|
||||
if (block.type==="gallery"){
|
||||
@@ -786,10 +1028,16 @@ const state = {
|
||||
}
|
||||
if (block.type==="cards"){
|
||||
const items = Array.isArray(block.data.items)?block.data.items:[];
|
||||
if (isEducationRubro()){
|
||||
return `${editable("h3","title",block.data.title,"Titulo",false,"font-size:30px;margin:0 0 14px")}<div class="edu-cards-grid">${items.map((raw,idx)=>{const parts=String(raw).split("|");const t=parts[0]||"";const d=parts[1]||"";return `<div class="edu-card"><div class="editable card-pro-title" data-field="items.${idx}.title" data-placeholder="Titulo" contenteditable="true">${escapeHtml(t)}</div><div class="editable card-pro-desc" data-field="items.${idx}.desc" data-placeholder="Descripcion" contenteditable="true">${escapeHtml(d)}</div><a href="#contacto" style="display:inline-flex;margin-top:10px;color:#0a4dcf;font-weight:700;text-decoration:none">Ver plan de estudios</a></div>`;}).join("")}</div>`;
|
||||
}
|
||||
return `${editable("h3","title",block.data.title,"Titulo",false,"")}<div class="cards-grid">${items.map((raw,idx)=>{const parts=String(raw).split("|");const t=parts[0]||"";const d=parts[1]||"";return `<div class="card-pro"><div class="editable card-pro-title" data-field="items.${idx}.title" data-placeholder="Titulo" contenteditable="true">${escapeHtml(t)}</div><div class="editable card-pro-desc" data-field="items.${idx}.desc" data-placeholder="Descripcion" contenteditable="true">${escapeHtml(d)}</div></div>`;}).join("")}</div>`;
|
||||
}
|
||||
if (block.type==="iconlist"){
|
||||
const items = Array.isArray(block.data.items)?block.data.items:[];
|
||||
if (isEducationRubro()){
|
||||
return `${editable("h3","title",block.data.title,"Titulo",false,"margin:0 0 12px;font-size:28px")}<div class="edu-step-grid">${items.map((raw,idx)=>{const parts=String(raw).split("|");const t=parts[0]||"";const d=parts[1]||"";return `<div class="edu-step"><span class="edu-step-num">${idx+1}</span><div class="editable" data-field="items.${idx}.title" data-placeholder="Titulo" contenteditable="true" style="font-weight:700;color:#0b1733">${escapeHtml(t)}</div><div class="editable" data-field="items.${idx}.desc" data-placeholder="Descripcion" contenteditable="true" style="color:#4f6286;margin-top:6px;line-height:1.45">${escapeHtml(d)}</div></div>`;}).join("")}</div>`;
|
||||
}
|
||||
return `${editable("h3","title",block.data.title,"Titulo",false,"margin:0 0 10px")}<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">${items.map((raw,idx)=>{const parts=String(raw).split("|");const t=parts[0]||"";const d=parts[1]||"";const letter=(t||"I").trim().slice(0,1).toUpperCase();return `<div style="display:flex;gap:10px;align-items:flex-start;background:var(--site-card);padding:12px;border-radius:12px"><div style="width:34px;height:34px;border-radius:10px;background:rgba(89,217,200,.2);display:flex;align-items:center;justify-content:center;font-weight:700;color:var(--site-primary)">${escapeHtml(letter)}</div><div><div class="editable" data-field="items.${idx}.title" data-placeholder="Titulo" contenteditable="true" style="font-weight:600">${escapeHtml(t)}</div><div class="editable" data-field="items.${idx}.desc" data-placeholder="Descripcion" contenteditable="true" style="color:var(--site-muted);font-size:13px">${escapeHtml(d)}</div></div></div>`;}).join("")}</div>`;
|
||||
}
|
||||
if (block.type==="contact"){
|
||||
@@ -945,12 +1193,15 @@ const state = {
|
||||
|
||||
function renderPreview(){
|
||||
const canvas = document.getElementById("previewCanvas");
|
||||
document.body.style.overflow = "";
|
||||
document.querySelectorAll(".menu-drawer-global").forEach((n)=>n.remove());
|
||||
if (canvasSortable){
|
||||
canvasSortable.destroy();
|
||||
canvasSortable = null;
|
||||
}
|
||||
canvas.innerHTML = "";
|
||||
canvas.classList.toggle("free-drag", !!state.settings.free_drag);
|
||||
canvas.classList.toggle("education-site", isEducationRubro());
|
||||
applySiteTheme();
|
||||
if (state.settings.bg_anim_url){
|
||||
canvas.style.background = `url('${state.settings.bg_anim_url}') center/cover no-repeat`;
|
||||
@@ -1009,6 +1260,7 @@ const state = {
|
||||
el.className = "block";
|
||||
if (!state.settings.free_drag){ el.removeAttribute("draggable"); }
|
||||
el.dataset.blockId = block.id;
|
||||
el.dataset.blockType = block.type;
|
||||
el.id = block.id;
|
||||
if (state.settings.free_drag){
|
||||
const pos = block.pos || getDefaultPos();
|
||||
@@ -1040,6 +1292,7 @@ const state = {
|
||||
}
|
||||
}
|
||||
el.innerHTML = renderBlockHtml(block);
|
||||
if (block.type === "menu"){ wireMenuAccordionInteractions(el); }
|
||||
if (block.type === "image"){ bindInlineImageDrop(el, block); }
|
||||
if (block.type === "gallery"){ bindGalleryDrops(el, block); }
|
||||
if (block.type === "video"){ bindInlineVideoDrop(el, block); }
|
||||
@@ -1167,21 +1420,17 @@ const state = {
|
||||
});
|
||||
canvas.style.minHeight = maxBottom + "px";
|
||||
} else {
|
||||
const minH = Math.max(700, Number(state.settings.canvas_min_height || 1200));
|
||||
const minH = Math.max(900, Number(state.settings.canvas_min_height || 1200));
|
||||
canvas.style.minHeight = `${minH}px`;
|
||||
}
|
||||
if (state.settings.site_author){
|
||||
const credit = document.createElement("div");
|
||||
credit.style.marginTop = "20px";
|
||||
credit.style.padding = "12px 14px";
|
||||
credit.style.borderRadius = "10px";
|
||||
credit.style.background = "rgba(15,23,42,.06)";
|
||||
credit.style.color = "var(--site-muted)";
|
||||
credit.style.fontSize = "12px";
|
||||
credit.style.textAlign = "center";
|
||||
credit.textContent = `Creado por ${state.settings.site_author}`;
|
||||
inner.appendChild(credit);
|
||||
}
|
||||
const footer = document.createElement("footer");
|
||||
footer.className = "site-global-footer";
|
||||
const year = new Date().getFullYear();
|
||||
const registered = String(state.settings.registered_brand || state.settings.site_name || "Tu marca").trim();
|
||||
const devBrand = String(state.settings.developer_brand || "GKACHELE™").trim();
|
||||
const author = String(state.settings.site_author || "").trim();
|
||||
footer.innerHTML = `© ${year} ${escapeHtml(registered)}. Todos los derechos reservados.<br>Desarrollado por ${escapeHtml(devBrand)}${author ? ` · Creado por ${escapeHtml(author)}` : ""}`;
|
||||
inner.appendChild(footer);
|
||||
const scrollBtn = document.createElement("button");
|
||||
scrollBtn.className = "scroll-btn";
|
||||
scrollBtn.innerHTML = '<i class="fa-solid fa-arrow-up"></i>';
|
||||
@@ -1382,8 +1631,11 @@ const state = {
|
||||
const input=(label,id,val)=>`<div class="row"><label>${label}</label><input id="${id}" type="text" value="${escapeHtml(val||"")}"></div>`;
|
||||
if (block.type==="menu"){
|
||||
html+=input("Titulo","menuTitle",data.title);
|
||||
const mm = escapeHtml(data.menu_mode || "both");
|
||||
const mm = escapeHtml(normalizeMenuMode(data.menu_mode || "both"));
|
||||
html+=`<div class="row"><label>Modo menu</label><select id="menuMode"><option value="both" ${mm==="both"?"selected":""}>Ambos</option><option value="inline" ${mm==="inline"?"selected":""}>Horizontal</option><option value="accordion" ${mm==="accordion"?"selected":""}>Acordeon</option></select></div>`;
|
||||
const mms = escapeHtml((data.menu_mobile_style || "accordion").toLowerCase() === "drawer" ? "drawer" : "accordion");
|
||||
html+=`<div class="row"><label>Movil</label><select id="menuMobileStyle"><option value="accordion" ${mms==="accordion"?"selected":""}>Acordeon</option><option value="drawer" ${mms==="drawer"?"selected":""}>Drawer Pro</option></select></div>`;
|
||||
html+=`<div class="row"><label>Items menu (una linea)</label><textarea id="menuItems">${escapeHtml((data.items||[]).join("\n"))}</textarea></div>`;
|
||||
} else if (block.type==="hero"){
|
||||
html+=input("Titulo","heroTitle",data.title);
|
||||
html+=input("Subtitulo","heroSubtitle",data.subtitle);
|
||||
@@ -1402,7 +1654,7 @@ const state = {
|
||||
html+=`<div class="row"><label>Imagen (arrastrar)</label><div class="dropzone" id="imageDrop">${data.url ? "Imagen cargada" : "Suelta imagen o click"}</div><input id="imageFile" type="file" accept="image/*" hidden></div>`;
|
||||
} else if (block.type==="features"){
|
||||
html+=input("Titulo","featuresTitle",data.title);
|
||||
html+=`<div class="row"><label>Items (una linea)</label><textarea id="featuresItems">${escapeHtml((data.items||[]).join("\\n"))}</textarea></div>`;
|
||||
html+=`<div class="row"><label>Items (una linea)</label><textarea id="featuresItems">${escapeHtml((data.items||[]).join("\n"))}</textarea></div>`;
|
||||
} else if (block.type==="gallery"){
|
||||
html+=input("Titulo","galleryTitle",data.title);
|
||||
const fitVal = escapeHtml(data.fit || "cover");
|
||||
@@ -1412,14 +1664,14 @@ const state = {
|
||||
const has = (data.images||[])[i];
|
||||
html+=`<div class="row"><div class="dropzone" id="galleryDrop${i}">${has ? "Imagen cargada" : "Suelta imagen o click"}</div><input id="galleryFile${i}" type="file" accept="image/*" hidden></div>`;
|
||||
});
|
||||
html+=`<div class="row"><label>Imagenes (una linea)</label><textarea id="galleryImages">${escapeHtml((data.images||[]).join("\\n"))}</textarea></div>`;
|
||||
html+=`<div class="row"><label>Descripciones (una linea)</label><textarea id="galleryCaptions">${escapeHtml((data.captions||[]).join("\\n"))}</textarea></div>`;
|
||||
html+=`<div class="row"><label>Imagenes (una linea)</label><textarea id="galleryImages">${escapeHtml((data.images||[]).join("\n"))}</textarea></div>`;
|
||||
html+=`<div class="row"><label>Descripciones (una linea)</label><textarea id="galleryCaptions">${escapeHtml((data.captions||[]).join("\n"))}</textarea></div>`;
|
||||
} else if (block.type==="cards"){
|
||||
html+=input("Titulo","cardsTitle",data.title);
|
||||
html+=`<div class="row"><label>Tarjetas (Titulo|Texto por linea)</label><textarea id="cardsItems">${escapeHtml((data.items||[]).join("\\n"))}</textarea></div>`;
|
||||
html+=`<div class="row"><label>Tarjetas (Titulo|Texto por linea)</label><textarea id="cardsItems">${escapeHtml((data.items||[]).join("\n"))}</textarea></div>`;
|
||||
} else if (block.type==="iconlist"){
|
||||
html+=input("Titulo","iconTitle",data.title);
|
||||
html+=`<div class="row"><label>Items (Titulo|Texto por linea)</label><textarea id="iconItems">${escapeHtml((data.items||[]).join("\\n"))}</textarea></div>`;
|
||||
html+=`<div class="row"><label>Items (Titulo|Texto por linea)</label><textarea id="iconItems">${escapeHtml((data.items||[]).join("\n"))}</textarea></div>`;
|
||||
} else if (block.type==="contact"){
|
||||
html+=input("Titulo","contactTitle",data.title);
|
||||
html+=input("Email","contactEmail",data.email);
|
||||
@@ -1507,7 +1759,11 @@ const state = {
|
||||
if (block.type==="menu"){
|
||||
block.data.title=document.getElementById("menuTitle").value;
|
||||
const mm = document.getElementById("menuMode");
|
||||
if (mm){ block.data.menu_mode = mm.value || "both"; }
|
||||
if (mm){ block.data.menu_mode = normalizeMenuMode(mm.value || "both"); }
|
||||
const mms = document.getElementById("menuMobileStyle");
|
||||
if (mms){ block.data.menu_mobile_style = (mms.value || "accordion") === "drawer" ? "drawer" : "accordion"; }
|
||||
const mi = document.getElementById("menuItems");
|
||||
if (mi){ block.data.items = mi.value.split("\n").map((x)=>x.trim()).filter(Boolean); }
|
||||
}
|
||||
else if (block.type==="hero"){
|
||||
block.data.title=document.getElementById("heroTitle").value;
|
||||
@@ -1526,17 +1782,17 @@ const state = {
|
||||
const fit = document.getElementById("imageFit");
|
||||
if (fit){ block.data.fit = fit.value || "cover"; }
|
||||
}
|
||||
else if (block.type==="features"){ block.data.title=document.getElementById("featuresTitle").value; block.data.items=document.getElementById("featuresItems").value.split("\\n").filter(Boolean); }
|
||||
else if (block.type==="features"){ block.data.title=document.getElementById("featuresTitle").value; block.data.items=document.getElementById("featuresItems").value.split("\n").filter(Boolean); }
|
||||
else if (block.type==="gallery"){
|
||||
block.data.title=document.getElementById("galleryTitle").value;
|
||||
block.data.images=document.getElementById("galleryImages").value.split("\\n");
|
||||
block.data.images=document.getElementById("galleryImages").value.split("\n");
|
||||
const caps = document.getElementById("galleryCaptions");
|
||||
if (caps){ block.data.captions = caps.value.split("\\n"); }
|
||||
if (caps){ block.data.captions = caps.value.split("\n"); }
|
||||
const fit = document.getElementById("galleryFit");
|
||||
if (fit){ block.data.fit = fit.value || "cover"; }
|
||||
}
|
||||
else if (block.type==="cards"){ block.data.title=document.getElementById("cardsTitle").value; block.data.items=document.getElementById("cardsItems").value.split("\\n").filter(Boolean); }
|
||||
else if (block.type==="iconlist"){ block.data.title=document.getElementById("iconTitle").value; block.data.items=document.getElementById("iconItems").value.split("\\n").filter(Boolean); }
|
||||
else if (block.type==="cards"){ block.data.title=document.getElementById("cardsTitle").value; block.data.items=document.getElementById("cardsItems").value.split("\n").filter(Boolean); }
|
||||
else if (block.type==="iconlist"){ block.data.title=document.getElementById("iconTitle").value; block.data.items=document.getElementById("iconItems").value.split("\n").filter(Boolean); }
|
||||
else if (block.type==="contact"){ block.data.title=document.getElementById("contactTitle").value; block.data.email=document.getElementById("contactEmail").value; block.data.phone=document.getElementById("contactPhone").value; block.data.address=document.getElementById("contactAddress").value; }
|
||||
else if (block.type==="button"){
|
||||
block.data.text=document.getElementById("buttonText").value;
|
||||
@@ -1630,18 +1886,80 @@ const state = {
|
||||
});
|
||||
}
|
||||
function wireSortableDnD(){ return; }
|
||||
function wireMenuAccordionInteractions(blockEl){
|
||||
if (!blockEl) return;
|
||||
blockEl.querySelectorAll(".menu-accordion, .menu-accordion summary, .menu-accordion a").forEach((node)=>{
|
||||
node.addEventListener("pointerdown",(e)=>{ e.stopPropagation(); });
|
||||
node.addEventListener("click",(e)=>{ e.stopPropagation(); });
|
||||
});
|
||||
blockEl.querySelectorAll(".menu-drawer-global").forEach((node)=>{
|
||||
if (node.parentElement !== document.body){
|
||||
document.body.appendChild(node);
|
||||
}
|
||||
});
|
||||
const closeDrawer = (drawerId)=>{
|
||||
if (!drawerId) return;
|
||||
const panel = document.getElementById(drawerId);
|
||||
const overlay = document.querySelector(`[data-drawer-overlay="${drawerId}"]`);
|
||||
if (panel) panel.classList.remove("open");
|
||||
if (overlay) overlay.classList.remove("open");
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
const openDrawer = (drawerId)=>{
|
||||
if (!drawerId) return;
|
||||
const shell = document.querySelector(".preview-shell");
|
||||
const forceMobile = shell && (shell.classList.contains("size-phone") || shell.classList.contains("size-tablet"));
|
||||
if (!forceMobile && window.innerWidth > 980) return;
|
||||
const panel = document.getElementById(drawerId);
|
||||
const overlay = document.querySelector(`[data-drawer-overlay="${drawerId}"]`);
|
||||
if (panel) panel.classList.add("open");
|
||||
if (overlay) overlay.classList.add("open");
|
||||
document.body.style.overflow = "hidden";
|
||||
};
|
||||
blockEl.querySelectorAll("[data-drawer-toggle]").forEach((btn)=>{
|
||||
btn.addEventListener("click",(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openDrawer(btn.getAttribute("data-drawer-toggle"));
|
||||
});
|
||||
});
|
||||
blockEl.querySelectorAll("[data-drawer-close]").forEach((btn)=>{
|
||||
btn.addEventListener("click",(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeDrawer(btn.getAttribute("data-drawer-close"));
|
||||
});
|
||||
});
|
||||
blockEl.querySelectorAll("[data-drawer-overlay]").forEach((ov)=>{
|
||||
ov.addEventListener("click",(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeDrawer(ov.getAttribute("data-drawer-overlay"));
|
||||
});
|
||||
});
|
||||
blockEl.querySelectorAll("[data-drawer-link]").forEach((a)=>{
|
||||
a.addEventListener("click",()=>{
|
||||
const parent = a.closest(".menu-drawer");
|
||||
if (parent && parent.id) closeDrawer(parent.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
function initCanvasSortable(container){
|
||||
if (!hasSortable() || !container) return;
|
||||
canvasSortable = Sortable.create(container, {
|
||||
group: { name: "builderBlocks", pull: false, put: false },
|
||||
draggable: ".block",
|
||||
handle: ".drag-handle",
|
||||
filter: "input,textarea,select,a,summary,details,.editable,.menu-links,.menu-accordion",
|
||||
preventOnFilter: false,
|
||||
animation: 150,
|
||||
forceFallback: true,
|
||||
fallbackOnBody: true,
|
||||
fallbackTolerance: 8,
|
||||
delayOnTouchOnly: true,
|
||||
delay: 120,
|
||||
touchStartThreshold: 6,
|
||||
bubbleScroll: true,
|
||||
onEnd: (evt)=>{
|
||||
if (!evt || evt.from !== evt.to) return;
|
||||
const orderedIds = [...container.querySelectorAll(".block")].map((n)=>n.dataset.blockId).filter(Boolean);
|
||||
@@ -1930,6 +2248,8 @@ const state = {
|
||||
const animToggle=document.getElementById("animToggle");
|
||||
const canvasMinHeight=document.getElementById("canvasMinHeightInput");
|
||||
const canvasBottomSpace=document.getElementById("canvasBottomSpaceInput");
|
||||
const registeredBrand=document.getElementById("registeredBrandInput");
|
||||
const developerBrand=document.getElementById("developerBrandInput");
|
||||
const siteAuthor=document.getElementById("siteAuthorInput");
|
||||
siteName.value=s.site_name||""; primary.value=s.primary_color||"#59d9c8";
|
||||
bgColor.value=s.bg_color||"#f6f7fb"; bgColor2.value=s.bg_color2||"#e9eef5"; bgGradient.checked=!!s.bg_gradient;
|
||||
@@ -1943,6 +2263,8 @@ const state = {
|
||||
animToggle.checked = s.animations !== false;
|
||||
canvasMinHeight.value = Math.max(700, Number(s.canvas_min_height || 1200));
|
||||
canvasBottomSpace.value = Math.max(0, Number(s.canvas_bottom_space || 180));
|
||||
registeredBrand.value = s.registered_brand || s.site_name || "";
|
||||
developerBrand.value = s.developer_brand || "GKACHELE™";
|
||||
siteAuthor.value = s.site_author || "";
|
||||
siteName.addEventListener("input",()=>{ s.site_name=siteName.value; renderPreview(); });
|
||||
primary.addEventListener("input",()=>{ s.primary_color=primary.value; renderPreview(); });
|
||||
@@ -1959,6 +2281,8 @@ const state = {
|
||||
animToggle.addEventListener("change",()=>{ s.animations=animToggle.checked; renderPreview(); });
|
||||
canvasMinHeight.addEventListener("input",()=>{ s.canvas_min_height = Math.max(700, Number(canvasMinHeight.value || 1200)); renderPreview(); });
|
||||
canvasBottomSpace.addEventListener("input",()=>{ s.canvas_bottom_space = Math.max(0, Number(canvasBottomSpace.value || 0)); renderPreview(); });
|
||||
registeredBrand.addEventListener("input",()=>{ s.registered_brand = registeredBrand.value; renderPreview(); });
|
||||
developerBrand.addEventListener("input",()=>{ s.developer_brand = developerBrand.value; renderPreview(); });
|
||||
siteAuthor.addEventListener("input",()=>{ s.site_author = siteAuthor.value; renderPreview(); });
|
||||
|
||||
bindDrop(logoDrop, logoFile, (data)=>{ s.logo_url = data; }, "Logo cargado");
|
||||
@@ -1988,20 +2312,24 @@ const state = {
|
||||
setShellClass("size-desktop");
|
||||
}
|
||||
function wirePreviewToggle(){
|
||||
const btn = document.getElementById("btnPreview");
|
||||
if (!btn) return;
|
||||
btn.addEventListener("click", ()=>{
|
||||
const isPreview = document.body.classList.contains("preview-mode");
|
||||
if (isPreview){
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
const previewUrl = new URL(window.location.href);
|
||||
previewUrl.hash = "preview";
|
||||
window.history.pushState({ preview: true }, "", previewUrl.toString());
|
||||
document.body.classList.add("preview-mode");
|
||||
ensurePreviewBackButton();
|
||||
});
|
||||
const btnPreview = document.getElementById("btnPreview");
|
||||
if (btnPreview){
|
||||
btnPreview.addEventListener("click", async ()=>{
|
||||
btnPreview.disabled = true;
|
||||
const old = btnPreview.textContent;
|
||||
btnPreview.textContent = "Abriendo...";
|
||||
try{
|
||||
await saveDraftSilently();
|
||||
const url = new URL(`${BUILDER_BASE_PATH}/preview-final`, window.location.origin);
|
||||
window.open(url.toString(), "_blank", "noopener,noreferrer");
|
||||
} catch(_e){
|
||||
window.alert("No se pudo abrir la vista previa. Reintenta.");
|
||||
} finally {
|
||||
btnPreview.disabled = false;
|
||||
btnPreview.textContent = old;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function ensurePreviewBackButton(){
|
||||
let back = document.getElementById("previewBackBtn");
|
||||
@@ -2011,6 +2339,10 @@ const state = {
|
||||
back.className = "preview-back";
|
||||
back.textContent = "Atras";
|
||||
back.addEventListener("click", ()=>{
|
||||
if (PREVIEW_ONLY){
|
||||
window.location.href = BUILDER_BASE_PATH;
|
||||
return;
|
||||
}
|
||||
if (window.history.length > 1){
|
||||
window.history.back();
|
||||
return;
|
||||
@@ -2026,7 +2358,7 @@ const state = {
|
||||
}
|
||||
function initStandalonePreviewMode(){
|
||||
const apply = ()=>{
|
||||
const isPreview = window.location.hash === "#preview";
|
||||
const isPreview = PREVIEW_ONLY;
|
||||
document.body.classList.toggle("preview-mode", isPreview);
|
||||
if (isPreview){
|
||||
ensurePreviewBackButton();
|
||||
@@ -2036,10 +2368,6 @@ const state = {
|
||||
}
|
||||
};
|
||||
apply();
|
||||
if (!window.__previewPopBound){
|
||||
window.addEventListener("popstate", apply);
|
||||
window.__previewPopBound = true;
|
||||
}
|
||||
}
|
||||
function wireThemeToggle(){
|
||||
const btn = document.getElementById("btnTheme");
|
||||
@@ -2085,12 +2413,36 @@ const state = {
|
||||
status.textContent = msg;
|
||||
status.className = `save-status${kind ? ` ${kind}` : ""}`;
|
||||
}
|
||||
async function saveDraftSilently(){
|
||||
const payload = {
|
||||
site_id: SITE_ID,
|
||||
publish: false,
|
||||
content: { ...SERVER_CONTENT, settings: state.settings, blocks: state.blocks }
|
||||
};
|
||||
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 draft failed");
|
||||
return true;
|
||||
}
|
||||
function normalizeLoadedBlocks(blocks){
|
||||
if (!Array.isArray(blocks)) return [];
|
||||
return blocks
|
||||
.filter((b)=>b && typeof b === "object")
|
||||
.map((b)=>{
|
||||
const data = (b.data && typeof b.data === "object") ? { ...b.data } : {};
|
||||
if (b.type === "menu"){
|
||||
data.menu_mode = normalizeMenuMode(data.menu_mode || "both");
|
||||
data.menu_mobile_style = (data.menu_mobile_style || "accordion") === "drawer" ? "drawer" : "accordion";
|
||||
if (!Array.isArray(data.items) || data.items.length < 3){
|
||||
data.items = ["Inicio","Productos","Blog","La Empresa","Contacto"];
|
||||
} else {
|
||||
data.items = data.items.map((x)=>String(x || "").trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
const fallback = 100;
|
||||
data.width = snapBlockWidth(b.type, (typeof data.width === "number" ? data.width : fallback));
|
||||
delete data.full_width;
|
||||
@@ -2136,9 +2488,27 @@ const state = {
|
||||
}
|
||||
}
|
||||
}
|
||||
async function resetBlocks(){
|
||||
if (isSaving) return;
|
||||
const ok = window.confirm("Esto borrara todos los bloques actuales. Deseas continuar?");
|
||||
if (!ok) return;
|
||||
state.blocks = [];
|
||||
selectedBlockId = null;
|
||||
renderInspector();
|
||||
renderPreview();
|
||||
setSaveStatus("Reseteando...", "busy");
|
||||
try{
|
||||
await saveDraftSilently();
|
||||
setSaveStatus("Reset aplicado", "ok");
|
||||
} catch(_e){
|
||||
setSaveStatus("Error al resetear", "error");
|
||||
}
|
||||
}
|
||||
function init(){
|
||||
// By default we keep section flow layout for stable full-page composition.
|
||||
state.settings.free_drag = false;
|
||||
const initialRubro = normalizeRubro(state.settings.business_rubro || SERVER_RUBRO || "restaurante");
|
||||
state.settings.business_rubro = initialRubro;
|
||||
state.blocks = normalizeLoadedBlocks(state.blocks);
|
||||
if (BUILDER_MODE === "ub24"){
|
||||
state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; });
|
||||
@@ -2166,18 +2536,26 @@ const state = {
|
||||
if (!key || !templates[key]) return;
|
||||
const t = templates[key];
|
||||
state.settings = { ...state.settings, ...t.settings };
|
||||
state.settings.business_rubro = normalizeRubro(key);
|
||||
state.blocks = t.blocks.map(b=>({ ...b, id: makeId(), page: (BUILDER_MODE==="ub24" ? "home" : b.page) }));
|
||||
selectedBlockId = null;
|
||||
renderInspector(); renderPreview();
|
||||
wireSettings();
|
||||
};
|
||||
if (templateSelect){
|
||||
templateSelect.value = state.settings.business_rubro || "";
|
||||
templateSelect.addEventListener("change", ()=>{
|
||||
applyTemplate(templateSelect.value);
|
||||
});
|
||||
}
|
||||
if (!state.blocks.length && templates[initialRubro]){
|
||||
applyTemplate(initialRubro);
|
||||
if (templateSelect){ templateSelect.value = initialRubro; }
|
||||
}
|
||||
document.getElementById("previewCanvas").addEventListener("click",()=>{ selectedBlockId=null; renderInspector(); renderPreview(); });
|
||||
document.getElementById("btnSave").addEventListener("click",saveContent);
|
||||
const resetBtn = document.getElementById("btnReset");
|
||||
if (resetBtn){ resetBtn.addEventListener("click", resetBlocks); }
|
||||
renderInspector(); renderPreview();
|
||||
}
|
||||
init();
|
||||
|
||||
Reference in New Issue
Block a user