Skip to content

Instantly share code, notes, and snippets.

@iDavidMorales
Created November 25, 2025 23:13
Show Gist options
  • Select an option

  • Save iDavidMorales/9994fb39bc8a342ebcf1ed24eef68ba8 to your computer and use it in GitHub Desktop.

Select an option

Save iDavidMorales/9994fb39bc8a342ebcf1ed24eef68ba8 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Cupones Digitales - Vista de marca</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Opcional: iconos -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
<style>
:root{
--brand:#00C2CB;
--brand-dark:#028499;
--accent:#FF4D5A;
--accent-soft:#FFE4E6;
--bg:#F3F4F6;
--card:#FFFFFF;
--border:#E5E7EB;
--text:#111827;
--muted:#6B7280;
--radius-lg:24px;
--radius-md:18px;
}
*{
box-sizing:border-box;
}
body{
margin:0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:var(--bg);
color:var(--text);
}
.page{
max-width:1120px;
margin:0 auto;
padding:16px 16px 32px;
}
/* NAVBAR */
.navbar{
display:flex;
align-items:center;
justify-content:space-between;
gap:12px;
padding:8px 12px;
margin-bottom:16px;
border-radius:999px;
background:#FFFFFF;
border:1px solid var(--border);
box-shadow:0 4px 18px rgba(15,23,42,0.06);
position:sticky;
top:8px;
z-index:10;
}
.nav-left{
display:flex;
align-items:center;
gap:10px;
min-width:0;
}
.nav-logo{
width:32px;
height:32px;
border-radius:999px;
background:radial-gradient(circle at 30% 30%, #FFFFFF 0, #FFFFFF 40%, var(--brand) 90%);
display:flex;
align-items:center;
justify-content:center;
color:#0f172a;
font-size:18px;
flex-shrink:0;
box-shadow:0 0 0 1px rgba(15,23,42,0.05);
}
.nav-title{
display:flex;
flex-direction:column;
min-width:0;
}
.nav-title strong{
font-size:14px;
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
}
.nav-title span{
font-size:12px;
color:var(--muted);
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
}
.nav-btn{
font-size:13px;
padding:6px 12px;
border-radius:999px;
border:1px solid var(--border);
background:#F9FAFB;
color:#374151;
display:inline-flex;
align-items:center;
gap:6px;
cursor:pointer;
white-space:nowrap;
}
.nav-btn i{
font-size:14px;
}
/* HERO */
.hero{
margin-top:12px;
margin-bottom:24px;
border-radius:var(--radius-lg);
background:
linear-gradient(120deg, #FDF2F8 0, #E0F2FE 45%, #EFF6FF 100%);
overflow:hidden;
padding:24px 28px;
display:flex;
gap:18px;
position:relative;
align-items:center;
}
.hero::after{
content:"";
position:absolute;
inset:0;
background:url("https://images.pexels.com/photos/302899/pexels-photo-302899.jpeg?auto=compress&cs=tinysrgb&w=1200") center/cover no-repeat;
opacity:0.12;
mix-blend-mode:multiply;
pointer-events:none;
}
.hero-content{
position:relative;
max-width:480px;
z-index:1;
}
.hero-kicker{
font-size:11px;
letter-spacing:0.16em;
text-transform:uppercase;
color:#B91C1C;
margin-bottom:4px;
font-weight:600;
}
.hero-title{
font-size:28px;
margin:0 0 4px;
}
.hero-subtitle{
font-size:14px;
color:#374151;
margin:0 0 16px;
}
.hero-actions{
display:flex;
flex-wrap:wrap;
gap:10px;
}
.btn-primary{
border:none;
border-radius:999px;
background:linear-gradient(135deg,var(--brand),var(--brand-dark));
color:#FFFFFF;
padding:8px 16px;
font-size:13px;
font-weight:600;
display:inline-flex;
gap:8px;
align-items:center;
cursor:pointer;
box-shadow:0 10px 25px rgba(8,47,73,0.25);
}
.btn-secondary{
border-radius:999px;
border:1px solid var(--border);
background:#FFFFFF;
color:#111827;
padding:8px 16px;
font-size:13px;
display:inline-flex;
gap:8px;
align-items:center;
cursor:pointer;
}
.hero-meta{
margin-top:10px;
font-size:11px;
color:#6B7280;
}
.hero-chip{
display:inline-flex;
align-items:center;
gap:4px;
padding:2px 8px;
border-radius:999px;
border:1px solid rgba(148,163,184,0.8);
background:rgba(255,255,255,0.8);
font-size:11px;
margin-right:4px;
}
.hero-chip i{
font-size:13px;
color:var(--accent);
}
/* SECTION TITLE */
.section-header{
display:flex;
justify-content:space-between;
align-items:flex-end;
gap:10px;
margin-bottom:12px;
}
.section-header h2{
margin:0;
font-size:18px;
}
.section-header p{
margin:4px 0 0;
font-size:13px;
color:var(--muted);
}
.section-meta{
font-size:12px;
color:var(--muted);
text-align:right;
}
/* GRID DE CUPONES */
.grid{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
gap:18px;
}
.coupon-card{
background:var(--card);
border-radius:var(--radius-md);
overflow:hidden;
border:1px solid var(--border);
box-shadow:0 6px 18px rgba(15,23,42,0.06);
display:flex;
flex-direction:column;
min-height:100%;
}
.coupon-media{
position:relative;
overflow:hidden;
}
.coupon-media::before{
content:"";
display:block;
padding-top:60%; /* 5:3 */
}
.coupon-media img{
position:absolute;
inset:0;
width:100%;
height:100%;
object-fit:cover;
transition:transform .35s ease-out;
}
.coupon-card:hover .coupon-media img{
transform:scale(1.04);
}
.coupon-badges{
position:absolute;
top:8px;
left:8px;
display:flex;
flex-wrap:wrap;
gap:4px;
}
.badge-pill{
font-size:11px;
padding:2px 7px;
border-radius:999px;
background:rgba(15,23,42,0.9);
color:#F9FAFB;
display:inline-flex;
align-items:center;
gap:4px;
}
.badge-pill.badge-main{
background:var(--accent);
color:#fff;
}
.badge-pill.badge-new{
background:#22C55E;
}
.badge-pill i{
font-size:11px;
}
.coupon-footer-logo{
position:absolute;
bottom:8px;
left:8px;
width:26px;
height:26px;
border-radius:999px;
overflow:hidden;
border:2px solid #fff;
background:#F9FAFB;
display:flex;
align-items:center;
justify-content:center;
font-size:13px;
color:#111827;
}
.coupon-body{
padding:12px 14px 12px;
display:flex;
flex-direction:column;
gap:6px;
flex:1;
}
.coupon-title{
font-size:14px;
font-weight:600;
margin:0;
}
.coupon-desc{
font-size:12px;
color:var(--muted);
margin:0;
max-height:3em;
overflow:hidden;
text-overflow:ellipsis;
}
.coupon-meta-line{
font-size:11px;
color:var(--muted);
margin-top:2px;
}
.coupon-meta-line span{
margin-right:8px;
}
.coupon-stats{
display:flex;
flex-wrap:wrap;
gap:6px;
margin-top:4px;
}
.chip-stat{
font-size:11px;
padding:2px 8px;
border-radius:999px;
background:#F3F4F6;
color:#4B5563;
display:inline-flex;
align-items:center;
gap:4px;
}
.chip-stat i{
font-size:11px;
color:#9CA3AF;
}
.chip-stat strong{
font-weight:600;
}
.coupon-actions{
display:flex;
justify-content:space-between;
align-items:center;
margin-top:8px;
}
.coupon-link{
font-size:12px;
padding:6px 10px;
border-radius:999px;
border:1px solid var(--border);
background:#FFFFFF;
color:#111827;
text-decoration:none;
display:inline-flex;
align-items:center;
gap:6px;
}
.coupon-link i{
font-size:13px;
}
.coupon-tag{
font-size:11px;
color:var(--muted);
}
/* ESTADOS */
.empty-state{
padding:32px 16px;
text-align:center;
font-size:14px;
color:var(--muted);
}
.error-box{
margin-top:12px;
padding:10px 12px;
border-radius:12px;
background:#FEE2E2;
color:#B91C1C;
font-size:13px;
border:1px solid #FCA5A5;
display:none;
}
@media (max-width:640px){
.navbar{
border-radius:16px;
}
.hero{
padding:18px 18px 20px;
}
.hero-title{
font-size:22px;
}
}
</style>
</head>
<body>
<div class="page">
<!-- NAVBAR -->
<nav class="navbar">
<div class="nav-left">
<div class="nav-logo">
<i class="bi bi-ticket-perforated"></i>
</div>
<div class="nav-title">
<strong>Cupones Digitales</strong>
<span id="nav-subtitle">Descubre ofertas locales personalizadas para tu comunidad.</span>
</div>
</div>
<button class="nav-btn" type="button">
<i class="bi bi-sliders"></i>
Configurar marca
</button>
</nav>
<!-- HERO -->
<section class="hero">
<div class="hero-content">
<div class="hero-kicker" id="hero-kicker">CAFÉ ARTESANAL</div>
<h1 class="hero-title">Cupones Digitales</h1>
<p class="hero-subtitle">
Descubre ofertas locales personalizadas para tu comunidad y comparte tus promociones en un solo enlace.
</p>
<div class="hero-actions">
<button type="button" class="btn-primary" onclick="scrollToPromos()">
<i class="bi bi-stars"></i>
Ver cupones destacados
</button>
<button type="button" class="btn-secondary">
<i class="bi bi-people"></i>
Personalizar experiencia
</button>
</div>
<div class="hero-meta" id="hero-meta">
<span class="hero-chip">
<i class="bi bi-activity"></i>
Esperando datos de la API...
</span>
</div>
</div>
</section>
<!-- HEADER LISTA -->
<section id="promos">
<div class="section-header">
<div>
<h2 id="section-title">Promociones de tu marca</h2>
<p>Comparte estas ofertas con tu comunidad y aumenta tus canjes digitales.</p>
</div>
<div class="section-meta" id="section-meta">
0 cupones · 0 vistas totales
</div>
</div>
<div id="error-box" class="error-box"></div>
<div id="coupon-grid" class="grid">
<div class="empty-state" id="empty-state">
Cargando cupones desde la API...
</div>
</div>
</section>
</div>
<script>
const qs = new URLSearchParams(window.location.search);
const apiPublic = qs.get('api_public') || '';
const brandName = qs.get('brand') || 'Café Artesanal';
const HERO_KICKER = document.getElementById('hero-kicker');
const HERO_META = document.getElementById('hero-meta');
const NAV_SUBTITLE = document.getElementById('nav-subtitle');
const SECTION_TITLE = document.getElementById('section-title');
const SECTION_META = document.getElementById('section-meta');
const GRID = document.getElementById('coupon-grid');
const EMPTY_STATE = document.getElementById('empty-state');
const ERROR_BOX = document.getElementById('error-box');
HERO_KICKER.textContent = brandName.toUpperCase();
SECTION_TITLE.textContent = `Promociones de ${brandName}`;
NAV_SUBTITLE.textContent = `Cupones digitales de ${brandName} para tu comunidad.`;
function scrollToPromos(){
document.getElementById('promos').scrollIntoView({behavior:'smooth', block:'start'});
}
function shortText(text, max){
if(!text) return '';
text = String(text);
if(text.length <= max) return text;
return text.slice(0,max-1) + '…';
}
function buildBadgeLabel(c){
// Heurística para texto principal del badge
const title = (c.titulo || c.text || '').toUpperCase();
if(title.includes('2X1')) return '2X1';
if(title.includes('%')) return title.match(/\d+%/g)?.[0] + ' OFF' || 'OFERTA';
if((c.condiciones || '').toLowerCase().includes('gratis')) return 'GRATIS';
if((c.tipo || '').toLowerCase() === 'promocion') return 'PROMO';
return 'OFERTA';
}
function isNewCoupon(c){
if(!c.fecha_create) return false;
const created = new Date(c.fecha_create.replace(' ','T'));
if(isNaN(created.getTime())) return false;
const now = new Date();
const diffDays = (now - created) / (1000*60*60*24);
return diffDays <= 14; // últimos 14 días
}
function renderCoupons(data){
const apiUsage = data.api_usage || {};
const stats = data.stats || {};
const cupones = Array.isArray(data.cupones) ? data.cupones : [];
// Meta hero
const totalViews = (stats.totals && stats.totals.vistosx) || 0;
const totalScan = (stats.totals && stats.totals.scan) || 0;
const records = stats.records || cupones.length;
HERO_META.innerHTML = `
<span class="hero-chip">
<i class="bi bi-activity"></i>
${records} cupones activos
</span>
<span class="hero-chip">
<i class="bi bi-eye"></i>
${totalViews.toLocaleString('es-MX')} vistas totales
</span>
<span class="hero-chip">
<i class="bi bi-upc-scan"></i>
${totalScan.toLocaleString('es-MX')} scans acumulados
</span>
`;
SECTION_META.textContent =
`${records} cupones · ${totalViews.toLocaleString('es-MX')} vistas totales`;
if(!cupones.length){
GRID.innerHTML = '';
const div = document.createElement('div');
div.className = 'empty-state';
div.textContent = 'No hay cupones vigentes para este token público.';
GRID.appendChild(div);
return;
}
GRID.innerHTML = '';
cupones.forEach(c => {
const card = document.createElement('article');
card.className = 'coupon-card';
const media = document.createElement('div');
media.className = 'coupon-media';
const img = document.createElement('img');
img.src = c.foto || c.foto_post || 'https://i.imgur.com/L3DeKNK.png';
img.alt = c.titulo || c.text || 'Cupón';
const badges = document.createElement('div');
badges.className = 'coupon-badges';
const mainBadge = document.createElement('span');
mainBadge.className = 'badge-pill badge-main';
mainBadge.innerHTML = `<i class="bi bi-ticket-perforated"></i>${buildBadgeLabel(c)}`;
badges.appendChild(mainBadge);
if(isNewCoupon(c)){
const newBadge = document.createElement('span');
newBadge.className = 'badge-pill badge-new';
newBadge.innerHTML = `<i class="bi bi-stars"></i>NUEVO`;
badges.appendChild(newBadge);
}
const footerLogo = document.createElement('div');
footerLogo.className = 'coupon-footer-logo';
footerLogo.innerHTML = `<i class="bi bi-shop"></i>`;
media.appendChild(img);
media.appendChild(badges);
media.appendChild(footerLogo);
const body = document.createElement('div');
body.className = 'coupon-body';
const title = document.createElement('h3');
title.className = 'coupon-title';
title.textContent = shortText(c.titulo || c.text || 'Cupón sin título', 70);
const desc = document.createElement('p');
desc.className = 'coupon-desc';
const baseDesc = (c.descripcion && c.descripcion !== '0') ? c.descripcion : (c.condiciones || '');
desc.textContent = shortText(baseDesc || 'Sin descripción definida.', 140);
const metaLine = document.createElement('div');
metaLine.className = 'coupon-meta-line';
metaLine.innerHTML = `
<span>Válido hasta <strong>${c.fecha_final || c.fecha || '—'}</strong></span>
<span>Tipo: ${(c.tipo || 'promo').toString()}</span>
`;
const statsRow = document.createElement('div');
statsRow.className = 'coupon-stats';
const vistasChip = document.createElement('div');
vistasChip.className = 'chip-stat';
vistasChip.innerHTML = `<i class="bi bi-eye"></i><strong>${(c.vistosx||0).toLocaleString('es-MX')}</strong> vistas`;
const scanChip = document.createElement('div');
scanChip.className = 'chip-stat';
scanChip.innerHTML = `<i class="bi bi-upc-scan"></i><strong>${(c.scan||0).toLocaleString('es-MX')}</strong> scans`;
const usadoChip = document.createElement('div');
usadoChip.className = 'chip-stat';
usadoChip.innerHTML = `<i class="bi bi-check2-circle"></i><strong>${(c.usado||0).toLocaleString('es-MX')}</strong> usos`;
statsRow.appendChild(vistasChip);
statsRow.appendChild(scanChip);
statsRow.appendChild(usadoChip);
const actions = document.createElement('div');
actions.className = 'coupon-actions';
const urlCupon = c.link || `https://routicket.com/cupon/?id_cupon=${c.id}`;
const linkBtn = document.createElement('a');
linkBtn.href = urlCupon;
linkBtn.target = '_blank';
linkBtn.className = 'coupon-link';
linkBtn.innerHTML = `Ver cupón <i class="bi bi-arrow-up-right"></i>`;
const tag = document.createElement('span');
tag.className = 'coupon-tag';
tag.textContent = `ID ${c.id} · empresa ${c.id_empresa || 0}`;
actions.appendChild(linkBtn);
actions.appendChild(tag);
body.appendChild(title);
body.appendChild(desc);
body.appendChild(metaLine);
body.appendChild(statsRow);
body.appendChild(actions);
card.appendChild(media);
card.appendChild(body);
GRID.appendChild(card);
});
// Info de errores limpia
ERROR_BOX.style.display = 'none';
}
async function init(){
if(!apiPublic){
EMPTY_STATE.textContent = 'Agrega ?api_public=TU_TOKEN a la URL para cargar los cupones.';
return;
}
const apiUrl = `https://routicket.com/cuponera/cancun/api_cupones.php?api_public=${encodeURIComponent(apiPublic)}`;
try{
const res = await fetch(apiUrl);
if(!res.ok){
throw new Error('Error HTTP ' + res.status);
}
const data = await res.json();
renderCoupons(data);
}catch(err){
console.error(err);
ERROR_BOX.style.display = 'block';
ERROR_BOX.textContent = 'No se pudieron cargar los cupones desde api_cupones.php: ' + err.message;
EMPTY_STATE.textContent = 'Error al cargar cupones.';
}
}
init();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment