Plantilla Tailwindshop
http://localhost/turnero/template/screen.php?tpl=tailwindshop&queue_id=1&title=Carnicer%C3%ADa
Archivo PHP de la plantilla tailwindshop
/templates/layout.php
<?php
// /turnero/template/tailwindshop/layout.php
declare(strict_types=1);
/** @var array $TEMPLATE_VARS */
extract($TEMPLATE_VARS, EXTR_SKIP);
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Sistema de Turnos - <?= htmlspecialchars($title) ?></title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet"/>
<!-- Carga de CSS del template (style.css) -->
<?php if ($HAS_TPL_STYLE): ?>
<link rel="stylesheet" href="<?= htmlspecialchars($TPL_STYLE) ?>">
<?php endif; ?>
<script>
// Configuración de Tailwind CSS
tailwind.config = {
theme: {
extend: {
colors: {
'brand-red': 'var(--brand-red)',
'brand-yellow': 'var(--brand-yellow)',
'neutral-bg': 'var(--neutral-bg)',
'text-dark': 'var(--text-dark)',
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
},
},
};
</script>
</head>
<body class="bg-neutral-bg font-sans text-text-dark flex flex-col min-h-screen">
<<header class="absolute top-0 left-0 w-full flex justify-center items-center px-6 py-3 bg-white shadow-md z-10">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-brand-red text-5xl">local_dining</span>
<h1 class="text-4xl font-bold text-text-dark"><?= htmlspecialchars($title) ?></h1>
</div>
</header>
<main class="flex-grow flex items-stretch relative">
<div class="absolute inset-0 z-0">
<div class="w-full h-full" id="ads-container">
<!-- Contenido estático de fondo que se mostrará mientras carga la API -->
<div class="relative w-full h-full">
<img alt="Carne fresca" class="absolute inset-0 w-full h-full object-cover" src="https://images.unsplash.com/photo-1551028150-64b9f398f67b?q=80&w=2070&auto=format&fit=crop">
<div class="absolute inset-0 bg-black bg-opacity-40 flex flex-col justify-center items-center p-8">
<h2 class="text-5xl md:text-7xl font-black text-white text-shadow-custom text-center mb-4">¡Las mejores carnes!</h2>
<p class="text-3xl md:text-4xl text-brand-yellow font-bold text-center">Calidad garantizada</p>
</div>
</div>
</div>
</div>
<div class="absolute bottom-4 left-4 p-4 md:p-6 bg-white rounded-2xl shadow-xl z-10 w-auto">
<div class="mb-4">
<p class="text-lg text-gray-500">Atendiendo</p>
<div class="text-7xl font-black text-brand-red text-shadow-custom" id="current-turn">--</div>
</div>
<div class="grid grid-cols-3 gap-2" id="next-turns-container">
<!-- Placeholders para los próximos turnos -->
<div class="bg-neutral-bg p-3 rounded-md flex items-center justify-center"><span class="text-4xl font-bold text-text-dark">---</span></div>
<div class="bg-neutral-bg p-3 rounded-md flex items-center justify-center"><span class="text-4xl font-bold text-text-dark">---</span></div>
<div class="bg-neutral-bg p-3 rounded-md flex items-center justify-center"><span class="text-4xl font-bold text-text-dark">---</span></div>
<div class="bg-neutral-bg p-3 rounded-md flex items-center justify-center"><span class="text-4xl font-bold text-text-dark">---</span></div>
<div class="bg-gray-200 p-3 rounded-md opacity-60 flex items-center justify-center"><span class="text-4xl font-bold text-gray-400">---</span></div>
<div class="bg-gray-200 p-3 rounded-md opacity-60 flex items-center justify-center"><span class="text-4xl font-bold text-gray-400">---</span></div>
</div>
</div>
</main>
<footer class="absolute bottom-0 w-full bg-white py-3 px-4 shadow-t-md z-10">
<div class="carousel flex items-center h-16" id="marquee-footer">
<div class="carousel-item flex items-center justify-center text-center px-4">
<p class="text-lg font-bold text-text-dark" id="marquee-text"><?= htmlspecialchars($marquee) ?></p>
</div>
</div>
</footer>
<audio id="chime" src="<?=$PUBLIC?>/chime.mp3" preload="auto"></audio>
<script>
const TPL_CONFIG = {
queueId: <?= json_encode($queueId) ?>,
API_BASE: <?= json_encode($API) ?>,
nextMax: <?= json_encode($nextMax) ?>,
initialMarquee: <?= json_encode($marquee) ?>,
};
let lastCurrentNumber = null;
const pad = (n, p) => String(n).padStart(p, '0');
const label = (pre, n, p) => `${pre}${pad(n, p)}`;
// --- LÓGICA DE TURNOS ---
function processApiData(j) {
const PAD = j.pad || 3;
const pre = j.prefix || 'C';
const MAX_ITEMS = 7;
const hist = j.last_calls || j.recent || j.served || [];
if (Array.isArray(hist) && hist.length > 0) {
return hist.slice(0, MAX_ITEMS).map(h => ({
ticket: label(pre, Number(h.number ?? h.current ?? h), PAD)
}));
}
if (j.current === null || j.current === undefined) return [];
const base = [{ ticket: label(pre, j.current, PAD) }];
const prev = (j.prev || j.previous || []).map(n => ({ ticket: label(pre, n, PAD) }));
const nx = (j.next || []).map(n => ({ ticket: label(pre, n, PAD) }));
return base.concat(prev).concat(nx).slice(0, MAX_ITEMS);
}
function updateDisplay(processedRows) {
if (!processedRows || processedRows.length === 0) return;
const currentTurnEl = document.getElementById('current-turn');
const nextTurnsContainer = document.getElementById('next-turns-container');
currentTurnEl.textContent = processedRows[0].ticket;
const nextTurns = processedRows.slice(1);
nextTurnsContainer.innerHTML = '';
const totalSlots = 6;
for (let i = 0; i < totalSlots; i++) {
const turn = nextTurns[i];
const turnText = turn ? turn.ticket : '---';
const isPlaceholder = !turn;
const div = document.createElement('div');
div.className = `p-3 rounded-md flex items-center justify-center ${isPlaceholder || i >= 4 ? 'bg-gray-200 opacity-60' : 'bg-neutral-bg'}`;
div.innerHTML = `<span class="text-4xl font-bold ${isPlaceholder || i >= 4 ? 'text-gray-400' : 'text-text-dark'}">${turnText}</span>`;
nextTurnsContainer.appendChild(div);
}
}
async function tick() {
try {
const r = await fetch(`${TPL_CONFIG.API_BASE}/queues_state.php?queue_id=${TPL_CONFIG.queueId}&next_limit=${TPL_CONFIG.nextMax}&_=${Date.now()}`);
if (!r.ok) return;
const j = await r.json();
if (!j || !j.ok) return;
const processedRows = processApiData(j);
updateDisplay(processedRows);
if (lastCurrentNumber !== null && j.current !== lastCurrentNumber) {
const chime = document.getElementById('chime');
if (chime) {
chime.currentTime = 0;
chime.play().catch(e => console.warn("No se pudo reproducir el sonido.", e));
}
}
lastCurrentNumber = j.current;
} catch (e) {
console.error("Error en la actualización de turnos:", e);
}
}
// --- LÓGICA PARA PUBLICIDAD ---
async function fetchAds() {
try {
const r = await fetch(`${TPL_CONFIG.API_BASE}/ads.php?_=${Date.now()}`);
if (!r.ok) return [];
const data = await r.json();
return Array.isArray(data) ? data : (data.items || data.ads || []);
} catch (e) {
console.error("Error al cargar la publicidad:", e);
return [];
}
}
function updateAdsDisplay(ads) {
const adsContainer = document.getElementById('ads-container');
if (!adsContainer || !ads || ads.length === 0) return;
adsContainer.innerHTML = '';
const carousel = document.createElement('div');
carousel.className = 'carousel w-full h-full flex';
ads.forEach(ad => {
let backgroundElement = '';
const isVideo = ad.url.endsWith('.mp4') || ad.type === 'video';
if (isVideo) {
backgroundElement = `<video class="absolute inset-0 w-full h-full object-cover" autoplay muted loop playsinline><source src="${ad.url}" type="video/mp4"></video>`;
} else {
backgroundElement = `<img alt="${ad.title || 'Publicidad'}" class="absolute inset-0 w-full h-full object-cover" src="${ad.url}">`;
}
const carouselItem = document.createElement('div');
carouselItem.className = 'carousel-item relative w-full h-full';
carouselItem.innerHTML = `${backgroundElement}<div class="absolute inset-0 bg-black bg-opacity-40 flex flex-col justify-center items-center p-8"><h2 class="text-5xl md:text-7xl font-black text-white text-shadow-custom text-center mb-4">${ad.title || ''}</h2><p class="text-3xl md:text-4xl text-brand-yellow font-bold text-center">${ad.subtitle || ''}</p></div>`;
carousel.appendChild(carouselItem);
});
adsContainer.appendChild(carousel);
if (ads.length > 1) {
let currentAdIndex = 0;
setInterval(() => {
currentAdIndex = (currentAdIndex + 1) % ads.length;
carousel.scrollTo({ left: currentAdIndex * carousel.offsetWidth, behavior: 'smooth' });
}, 10000);
}
}
// --- LÓGICA PARA MARQUESINA (NUEVA) ---
async function fetchMarquee() {
try {
const r = await fetch(`${TPL_CONFIG.API_BASE}/marquee.php?_=${Date.now()}`);
if (!r.ok) return TPL_CONFIG.initialMarquee;
const data = await r.json();
// Tu API devuelve {marquee: "..."} o {text: "..."}
return data.marquee || data.text || TPL_CONFIG.initialMarquee;
} catch (e) {
console.error("Error al cargar la marquesina:", e);
return TPL_CONFIG.initialMarquee;
}
}
function updateMarqueeDisplay(text) {
const marqueeTextEl = document.getElementById('marquee-text');
if (marqueeTextEl) {
marqueeTextEl.textContent = text;
}
}
// --- INICIALIZACIÓN ---
document.addEventListener('DOMContentLoaded', () => {
// Iniciar actualización de turnos
tick();
setInterval(tick, 2000);
// Iniciar actualización de publicidad
async function initAds() {
const adsData = await fetchAds();
updateAdsDisplay(adsData);
}
initAds();
setInterval(initAds, 30000);
// Iniciar actualización de marquesina
async function initMarquee() {
const marqueeData = await fetchMarquee();
updateMarqueeDisplay(marqueeData);
}
initMarquee();
setInterval(initMarquee, 30000); // Refrescar texto de marquesina cada 30s
});
</script>
</body>
</html>
Archivo de Estilos
template/tailwindshop/style.css
/* /turnero/template/tailwindshop/style.css */
/* Definiciones de variables de marca para usar en Tailwind CSS */
/* Estas variables también se configuran en tailwind.config.js en layout.php,
pero definirlas aquí permite usarlas directamente en CSS si fuera necesario. */
:root {
--brand-red: #D52B1E;
--brand-yellow: #FFC72C;
--neutral-bg: #F8F8F8;
--text-dark: #333333;
}
/* Estilos personalizados para el carrusel */
.carousel {
scroll-snap-type: x mandatory;
overflow-x: auto;
-webkit-overflow-scrolling: touch; /* Para un scroll más suave en iOS */
scrollbar-width: none; /* Ocultar la barra de desplazamiento en Firefox */
}
/* Ocultar la barra de desplazamiento en navegadores WebKit (Chrome, Safari) */
.carousel::-webkit-scrollbar {
display: none;
}
.carousel-item {
scroll-snap-align: start; /* Ajustar al inicio de cada ítem al desplazar */
flex-shrink: 0; /* Evitar que los ítems se encojan */
width: 100%; /* Cada ítem ocupa el 100% del ancho del carrusel */
}
/* Sombra de texto personalizada */
.text-shadow-custom {
text-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
}
/* Animación de fade in/out (para ofertas, etc.) */
.animate-fade-in-out {
animation: fadeInOut 10s infinite;
}
@keyframes fadeInOut {
0%, 100% { opacity: 0; }
10%, 90% { opacity: 1; }
}
/* Ajustes responsivos básicos si es necesario y no cubiertos por Tailwind */
/* Ejemplo: para pantallas muy pequeñas, ajustar el tamaño de fuente si el clamp no es suficiente */
@media (max-width: 640px) {
.text-5xl { font-size: 3rem; /* Equivalente a text-4xl o menos si se desea */ }
.md\:text-7xl { font-size: 4rem; /* Ajuste para móviles */ }
}
/* Reset básico (considera si es necesario si ya se usa un base.css o reset de Tailwind) */
body {
margin: 0;
}

No hay comentarios por ahora.