Ir al contenido
URL:
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&amp;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;

}


Calificación
0 0

No hay comentarios por ahora.

para ser el primero en comentar.