Files
whatsapp-api-mockup/js/store.js
T
2026-09-27 19:17:33 -06:00

545 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* WAM · store.js — dueño D (núcleo).
Estado único, persistencia en localStorage, normalización, generación de IDs
y demo por defecto "Café Aurora" (los 13 tipos, respuestas encadenadas).
No toca el DOM al cargar: solo define WAM.*. */
window.WAM = window.WAM || {};
(function () {
"use strict";
WAM.CLAVE = "wam-mockup-v1";
WAM.estado = null;
var TIPOS = [
"text", "image", "video", "audio", "document", "sticker", "location",
"contact", "template", "buttons", "list", "cta_url", "carousel"
];
var HEADER_TIPOS = ["texto", "imagen", "video", "documento"];
/* ————————————————— utilidades ————————————————— */
function dos(n) { return (n < 10 ? "0" : "") + n; }
WAM.horaAhora = function () {
var d = new Date();
return dos(d.getHours()) + ":" + dos(d.getMinutes());
};
function cad(v, porDefecto) {
if (v === undefined || v === null) return porDefecto || "";
return String(v);
}
function escaparXml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
if (c === "&") return "&amp;";
if (c === "<") return "&lt;";
if (c === ">") return "&gt;";
if (c === '"') return "&quot;";
return "&apos;";
});
}
function aDataUri(svg) {
return "data:image/svg+xml," + encodeURIComponent(svg);
}
/* Placeholder genérico (taza) para src/avatar/headerMedia vacíos. */
WAM.svgPlaceholder = function (texto) {
var t = escaparXml(texto || "");
var svg =
'<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320">' +
'<rect width="480" height="320" fill="#efe6dc"/>' +
'<path d="M204 96 l8 -16 m16 16 l8 -16 m16 16 l8 -16" fill="none" stroke="#c8a27a" stroke-width="5" stroke-linecap="round"/>' +
'<rect x="184" y="118" width="70" height="52" rx="10" fill="none" stroke="#a9825f" stroke-width="7"/>' +
'<path d="M254 130 h13 a13 13 0 0 1 0 26 h-13" fill="none" stroke="#a9825f" stroke-width="7"/>' +
(t ? '<text x="240" y="242" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="24" fill="#7f5539">' + t + '</text>' : '') +
'</svg>';
return aDataUri(svg);
};
/* Imagen SVG en línea y pequeña para las medias de la demo (sin red). */
function svgDemo(texto, emoji, fondo, acento) {
var svg =
'<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320">' +
'<rect width="480" height="320" fill="' + fondo + '"/>' +
'<circle cx="240" cy="132" r="76" fill="' + acento + '"/>' +
'<text x="240" y="156" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="68">' + escaparXml(emoji || "☕") + '</text>' +
'<text x="240" y="272" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="25" fill="#5b4232">' + escaparXml(texto || "") + '</text>' +
'</svg>';
return aDataUri(svg);
}
function svgAvatar() {
return aDataUri(
'<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">' +
'<circle cx="48" cy="48" r="48" fill="#6f4e37"/>' +
'<text x="48" y="63" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="42">☕</text>' +
'</svg>'
);
}
function svgSticker() {
return aDataUri(
'<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320" viewBox="0 0 320 320">' +
'<circle cx="160" cy="160" r="150" fill="#fff8ef" stroke="#e3cfae" stroke-width="10"/>' +
'<text x="160" y="198" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="130">☕</text>' +
'</svg>'
);
}
/* ————————————————— IDs secuenciales ————————————————— */
/* Recorre el estado (mensajes, respuestas anidadas, botones, filas, cards)
y junta todos los id existentes para no colisionar nunca. */
function recolectarIds(o, conjunto) {
if (!o || typeof o !== "object") return;
if (Array.isArray(o)) {
for (var i = 0; i < o.length; i++) recolectarIds(o[i], conjunto);
return;
}
if (typeof o.id === "string" && o.id) conjunto.add(o.id);
var claves = ["mensajes", "respuestas", "botones", "secciones", "filas", "cards"];
for (var c = 0; c < claves.length; c++) {
var v = o[claves[c]];
if (v && typeof v === "object") recolectarIds(v, conjunto);
}
}
function crearAlocador(usados) {
var conjunto = usados || new Set();
return function tomar() {
var n = 1;
while (conjunto.has("m" + n)) n++;
var id = "m" + n;
conjunto.add(id);
return id;
};
}
WAM.generarId = function () {
var conjunto = new Set();
recolectarIds(WAM.estado, conjunto);
return crearAlocador(conjunto)();
};
/* ————————————————— mensajes ————————————————— */
var CAMPOS = {
text: function () { return { texto: "" }; },
image: function () { return { src: "", caption: "" }; },
video: function () { return { src: "", caption: "" }; },
audio: function () { return { src: "", voz: true, duracion: "0:00" }; },
document: function () { return { src: "", caption: "", filename: "documento.pdf" }; },
sticker: function () { return { src: "" }; },
location: function () { return { latitud: "", longitud: "", nombre: "", direccion: "" }; },
contact: function () { return { nombre: "", telefono: "", email: "", empresa: "" }; },
template: function () { return { headerTipo: "", headerTexto: "", headerMedia: "", body: "", footer: "", botones: [] }; },
buttons: function () { return { headerTipo: "", headerTexto: "", headerMedia: "", body: "", footer: "", botones: [] }; },
list: function () { return { header: "", body: "", footer: "", boton: "", secciones: [] }; },
cta_url: function () { return { headerMedia: "", body: "", footer: "", textoBoton: "", url: "" }; },
carousel: function () { return { body: "", cards: [] }; }
};
WAM.nuevoMensaje = function (tipo) {
var valido = Object.prototype.hasOwnProperty.call(CAMPOS, tipo);
var fabrica = valido ? CAMPOS[tipo] : CAMPOS.text;
var msg = {
id: WAM.generarId(),
dir: "in",
tipo: valido ? tipo : "text",
hora: WAM.horaAhora(),
estado: "sent"
};
var campos = fabrica();
for (var k in campos) {
if (Object.prototype.hasOwnProperty.call(campos, k)) msg[k] = campos[k];
}
return msg;
};
/* ————————————————— normalización ————————————————— */
/* Rellena los campos que faltan por tipo y devuelve un estado válido.
Usado por cargar() y por editor.importarJSON (import / upgrade). */
WAM.normalizar = function (o) {
var src = (o && typeof o === "object" && !Array.isArray(o)) ? o : {};
var conjunto = new Set();
recolectarIds(src, conjunto);
recolectarIds(WAM.estado, conjunto); /* evita colisiones con lo vivo */
var tomar = crearAlocador(conjunto);
function normalizarMensaje(msj, forzarEntrada) {
var m = (msj && typeof msj === "object") ? msj : {};
var tipo = TIPOS.indexOf(m.tipo) !== -1 ? m.tipo : "text";
var msg = {
id: (typeof m.id === "string" && m.id) ? m.id : tomar(),
dir: (!forzarEntrada && m.dir === "out") ? "out" : "in",
tipo: tipo,
hora: (typeof m.hora === "string" && m.hora) ? m.hora : WAM.horaAhora(),
estado: (m.estado === "delivered" || m.estado === "read") ? m.estado : "sent"
};
function respuestas(valor) {
var lista = Array.isArray(valor) ? valor : [];
return lista.map(function (r) { return normalizarMensaje(r, true); });
}
function botones(valor) {
var lista = Array.isArray(valor) ? valor : [];
return lista.map(function (b) {
var boton = (b && typeof b === "object") ? b : {};
var out = { texto: cad(boton.texto), respuestas: respuestas(boton.respuestas) };
/* Botón URL de plantilla (research-api §7.2): se conserva si viene */
if (typeof boton.url === "string") out.url = boton.url;
return out;
});
}
switch (tipo) {
case "text":
msg.texto = cad(m.texto);
break;
case "image":
case "video":
msg.src = cad(m.src);
msg.caption = cad(m.caption);
break;
case "audio":
msg.src = cad(m.src);
msg.voz = m.voz === true;
msg.duracion = cad(m.duracion, "0:00");
break;
case "document":
msg.src = cad(m.src);
msg.caption = cad(m.caption);
msg.filename = cad(m.filename, "documento.pdf");
break;
case "sticker":
msg.src = cad(m.src);
break;
case "location":
msg.latitud = cad(m.latitud);
msg.longitud = cad(m.longitud);
msg.nombre = cad(m.nombre);
msg.direccion = cad(m.direccion);
break;
case "contact":
msg.nombre = cad(m.nombre);
msg.telefono = cad(m.telefono);
msg.email = cad(m.email);
msg.empresa = cad(m.empresa);
break;
case "template":
msg.headerTipo = HEADER_TIPOS.indexOf(m.headerTipo) !== -1 ? m.headerTipo : "";
msg.headerTexto = cad(m.headerTexto);
msg.headerMedia = cad(m.headerMedia);
msg.body = cad(m.body);
msg.footer = cad(m.footer);
msg.botones = botones(m.botones);
break;
case "buttons":
msg.headerTipo = HEADER_TIPOS.indexOf(m.headerTipo) !== -1 ? m.headerTipo : "";
msg.headerTexto = cad(m.headerTexto);
msg.headerMedia = cad(m.headerMedia);
msg.body = cad(m.body);
msg.footer = cad(m.footer);
msg.botones = botones(m.botones);
break;
case "list":
msg.header = cad(m.header);
msg.body = cad(m.body);
msg.footer = cad(m.footer);
msg.boton = cad(m.boton);
msg.secciones = (Array.isArray(m.secciones) ? m.secciones : []).map(function (s) {
var seccion = (s && typeof s === "object") ? s : {};
return {
titulo: cad(seccion.titulo),
filas: (Array.isArray(seccion.filas) ? seccion.filas : []).map(function (f) {
var fila = (f && typeof f === "object") ? f : {};
return {
titulo: cad(fila.titulo),
descripcion: cad(fila.descripcion),
respuestas: respuestas(fila.respuestas)
};
})
};
});
break;
case "cta_url":
msg.headerMedia = cad(m.headerMedia);
msg.body = cad(m.body);
msg.footer = cad(m.footer);
msg.textoBoton = cad(m.textoBoton);
msg.url = cad(m.url);
break;
case "carousel":
msg.body = cad(m.body);
msg.cards = (Array.isArray(m.cards) ? m.cards : []).map(function (c) {
var card = (c && typeof c === "object") ? c : {};
return {
media: cad(card.media),
texto: cad(card.texto),
textoBoton: cad(card.textoBoton),
url: cad(card.url),
respuestas: respuestas(card.respuestas)
};
});
break;
}
return msg;
}
var cuentaSrc = (src.cuenta && typeof src.cuenta === "object") ? src.cuenta : {};
return {
version: 1,
tema: src.tema === "oscuro" ? "oscuro" : "claro",
cuenta: {
nombre: cad(cuentaSrc.nombre, "Café Aurora"),
avatar: cad(cuentaSrc.avatar),
subtitulo: cad(cuentaSrc.subtitulo, "en línea"),
verificado: cuentaSrc.verificado === true
},
mensajes: (Array.isArray(src.mensajes) ? src.mensajes : []).map(function (x) {
return normalizarMensaje(x, false);
})
};
};
/* ————————————————— persistencia ————————————————— */
WAM.cargar = function () {
var bruto = null;
try { bruto = window.localStorage.getItem(WAM.CLAVE); } catch (e) { bruto = null; }
var dato = null;
if (bruto) {
try { dato = JSON.parse(bruto); } catch (e) { dato = null; }
}
if (!dato) dato = WAM.datosDemo();
WAM.estado = WAM.normalizar(dato);
return WAM.estado;
};
WAM.guardar = function () {
try {
window.localStorage.setItem(WAM.CLAVE, JSON.stringify(WAM.estado));
} catch (e) { /* sin localStorage disponible (cuota/file://): se ignora */ }
};
WAM.resetear = function () {
try { window.localStorage.removeItem(WAM.CLAVE); } catch (e) { /* idem */ }
WAM.estado = WAM.normalizar(WAM.datosDemo());
};
/* ————————————————— demo por defecto: "Café Aurora" ————————————————— */
function men(dir, tipo, hora, campos) {
var msg = { id: "", dir: dir, tipo: tipo, hora: hora, estado: "sent" };
var k;
for (k in campos) {
if (Object.prototype.hasOwnProperty.call(campos, k)) msg[k] = campos[k];
}
return msg;
}
WAM.datosDemo = function () {
return {
version: 1,
tema: "claro",
cuenta: {
nombre: "Café Aurora",
avatar: svgAvatar(),
subtitulo: "en línea",
verificado: true
},
mensajes: [
/* 1 · text in (saludo) */
men("in", "text", "10:24", {
texto: "¡Hola! 👋 Bienvenido a Café Aurora. Soy Luna, tu barista de guardia. ¿Te cuento las novedades?"
}),
/* 2 · text out (cliente) */
men("out", "text", "10:24", {
estado: "read",
texto: "¡Hola, Luna! Sí, cuéntame ☕"
}),
/* 3 · image */
men("in", "image", "10:25", {
src: svgDemo("Nuevo horario", "⏰", "#f3e7d8", "#c8a27a"),
caption: "Nuevo horario desde el 1 de octubre ☕"
}),
/* 4 · video (sin src → placeholder SVG) */
men("in", "video", "10:25", {
src: "",
caption: "Así tostamos nuestros granos cada mañana"
}),
/* 5 · location */
men("in", "location", "10:26", {
latitud: "40.4168",
longitud: "-3.7038",
nombre: "Café Aurora",
direccion: "Calle Mayor 12, 28013 Madrid"
}),
/* 6 · contact */
men("in", "contact", "10:26", {
nombre: "Luna Mendoza",
telefono: "+34 611 222 333",
email: "[email protected]",
empresa: "Café Aurora"
}),
/* 7 · document */
men("in", "document", "10:27", {
src: "",
caption: "Nuestra carta con granos y precios",
filename: "carta-cafe-aurora.pdf"
}),
/* 8 · audio (nota de voz) */
men("in", "audio", "10:27", {
src: "",
voz: true,
duracion: "0:12"
}),
/* 9 · sticker */
men("in", "sticker", "10:28", {
src: svgSticker()
}),
/* 10 · template (header imagen + confirmación de reserva) */
men("in", "template", "10:29", {
headerTipo: "imagen",
headerTexto: "",
headerMedia: svgDemo("Tu mesa te espera", "☕", "#e9d8c3", "#a9825f"),
body: "Hola 👋 Tu mesa para 2 personas está reservada este viernes a las 19:30. ¿Confirmas la reserva?",
footer: "Café Aurora · Reservas",
botones: [
{
texto: "Confirmar",
respuestas: [
men("in", "text", "10:29", {
texto: "¡Genial! Reserva confirmada para el viernes a las 19:30 ✨ Te esperamos."
})
]
},
{
texto: "Cambiar fecha",
respuestas: [
men("in", "text", "10:29", {
texto: "Sin problema. ¿Para qué día y hora te la cambio?"
})
]
}
]
}),
/* 11 · buttons (quick replies, máx 3 · títulos ≤ 20) */
men("in", "buttons", "10:30", {
body: "¿Qué te muestro ahora?",
footer: "Elige una opción",
botones: [
{
texto: "Horarios",
respuestas: [
men("in", "text", "10:30", {
texto: "Abrimos de lunes a viernes de 7:30 a 20:00 y sábados de 9:00 a 14:00."
})
]
},
{
texto: "Promociones",
respuestas: [
men("in", "text", "10:30", { texto: "🔥 Martes de cold brew: 2×1 todo el día." }),
men("in", "text", "10:30", { texto: "🥐 Viernes: croissant gratis con tu café." })
]
},
{
texto: "Hablar con el equipo",
respuestas: [
men("in", "text", "10:30", {
texto: "Te paso con Marta del equipo humano, un momento…"
})
]
}
]
}),
/* 12 · list (filas ≤ 10 · títulos fila ≤ 24 · descripción ≤ 72) */
men("in", "list", "10:31", {
header: "Menú de la casa",
body: "¿Qué te apetece hoy?",
footer: "Catálogo 2026",
boton: "Ver opciones",
secciones: [
{
titulo: "Bebidas",
filas: [
{
titulo: "Espresso double",
descripcion: "Intenso, 100 % arábica",
respuestas: [men("in", "text", "10:31", { texto: "Un espresso double rumbo a tu mesa ☕" })]
},
{
titulo: "Latte de avena",
descripcion: "Suave, con leche de avena",
respuestas: [men("in", "text", "10:31", { texto: "¡Buen gusto! Latte de avena anotado 🌾" })]
},
{
titulo: "Cold brew 12 h",
descripcion: "Macerado en frío durante 12 horas",
respuestas: [men("in", "text", "10:31", { texto: "Cold brew bien frío, ¡perfecto para hoy! 🧊" })]
}
]
},
{
titulo: "Postres",
filas: [
{
titulo: "Tarta de queso",
descripcion: "La de siempre, casera",
respuestas: [men("in", "text", "10:31", { texto: "Una porción de tarta de queso, ¡gran elección! 🍰" })]
},
{
titulo: "Croissant almendra",
descripcion: "Horneado cada mañana",
respuestas: [men("in", "text", "10:31", { texto: "Croissant de almendra recién hecho 🥐" })]
}
]
}
]
}),
/* 13 · cta_url (botón con URL → <a target="_blank">, sin flujo) */
men("in", "cta_url", "10:32", {
headerMedia: svgDemo("Tienda online", "🛍️", "#efe0cd", "#b08968"),
body: "Ya puedes pedir nuestros granos tostados a domicilio.",
footer: "Envío en 24-48 h",
textoBoton: "Abrir tienda",
url: "https://cafeaurora.example/tienda"
}),
/* 14 · carousel (3 cards con respuestas encadenadas) */
men("in", "carousel", "10:33", {
body: "Los favoritos de la casa ☕",
cards: [
{
media: svgDemo("Espresso Aurora", "☕", "#f0e3d0", "#a9825f"),
texto: "Espresso Aurora · tueste medio con notas de cacao. Bolsa 250 g.",
textoBoton: "¡Lo quiero!",
url: "",
respuestas: [men("in", "text", "10:33", { texto: "¡Excelente! El Espresso Aurora sale hoy mismo hacia ti 📦" })]
},
{
media: svgDemo("Cold brew botella", "🧋", "#e4ecf0", "#7f9cab"),
texto: "Cold brew en botella de 1 L, listo para servir muy frío.",
textoBoton: "Pedir",
url: "",
respuestas: [men("in", "text", "10:33", { texto: "Cold brew de 1 L añadido a tu pedido 🧊" })]
},
{
media: svgDemo("Tarta de queso", "🍰", "#f5ebe2", "#c9a227"),
texto: "Tarta de queso casera, entera o por porciones.",
textoBoton: "Probar",
url: "",
respuestas: [men("in", "text", "10:33", { texto: "La tarta de queso sale del horno a las 12:00 🍰" })]
}
]
})
]
};
};
})();