Created
August 22, 2025 03:51
-
-
Save iDavidMorales/b3c6452e884839b2cae207aa6f4edc26 to your computer and use it in GitHub Desktop.
menu digital restaurante api
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>La Regia Grill - Menú Digital</title> | |
| <!-- Incluye Tailwind CSS para los estilos --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Configura la fuente Inter para una mejor legibilidad --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| /* Estilos personalizados para la barra de desplazamiento */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #111827; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background-color: #f97316; | |
| border-radius: 4px; | |
| } | |
| .pulse-once { | |
| animation: pulse 0.5s cubic-bezier(0.4, 0, 0.6, 1) 1; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { | |
| transform: scale(1); | |
| } | |
| 50% { | |
| transform: scale(1.1); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100 min-h-screen"> | |
| <!-- Encabezado de la página --> | |
| <header class="py-6 px-4 sm:px-8 text-center"> | |
| <h1 class="text-4xl sm:text-5xl font-extrabold text-orange-500 tracking-wider"> | |
| La regia Grill | |
| </h1> | |
| <p class="mt-2 text-xl sm:text-2xl text-orange-200"> | |
| ¡Descubre nuestras deliciosas opciones! | |
| </p> | |
| </header> | |
| <!-- Barra de búsqueda con ícono --> | |
| <div class="px-4 sm:px-8 max-w-6xl mx-auto mb-6"> | |
| <div class="relative"> | |
| <input type="text" id="search-input" placeholder="Buscar productos..." class="w-full pl-12 pr-4 py-3 bg-gray-800 text-gray-200 placeholder-gray-400 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500 transition-colors"> | |
| <!-- Ícono de búsqueda (SVG) --> | |
| <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | |
| <svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> | |
| </svg> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Contenedor principal del menú con pre-loader --> | |
| <main id="menu-container" class="p-4 max-w-6xl mx-auto grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <!-- Pre-loader que se muestra mientras se carga el menú --> | |
| <div id="loading-spinner" class="col-span-1 sm:col-span-2 lg:col-span-3 text-center p-8"> | |
| <div class="flex flex-col items-center justify-center"> | |
| <svg class="animate-spin -ml-1 mr-3 h-10 w-10 text-orange-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | |
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| <p class="mt-4 text-gray-400 text-lg">Cargando menú...</p> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Botón flotante para el carrito de compras con contador de productos --> | |
| <button id="open-cart-btn" class="fixed bottom-6 right-6 p-4 bg-orange-500 hover:bg-orange-600 text-white rounded-full shadow-lg transition-transform transform hover:scale-110 focus:outline-none focus:ring-4 focus:ring-orange-500 focus:ring-opacity-50 z-50"> | |
| <!-- Ícono de carrito (SVG) --> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" viewBox="0 0 24 24" fill="currentColor"> | |
| <path d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zm10 0c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2zm-9.35-1.55l.85-1.13.11-.15c.16-.23.4-.37.66-.37h10.74c.48 0 .85.38.93.85L21.49 14.8c.18.9-.45 1.7-1.35 1.7H8.57c-.52 0-.96-.32-1.14-.8L4.2 4.41C4.05 3.79 3.44 3.4 2.81 3.4H1c-.55 0-1 .45-1 1s.45 1 1 1h1.56L5.27 15.65c.18.52.62.85 1.14.85h11.23c.52 0 .96-.32 1.14-.8L18.89 10H8.91c-.55 0-1-.45-1-1s.45-1 1-1h10.9c.55 0 1 .45 1 1v1.16l-3.35 5.25c-.18.28-.48.45-.82.45H7.65c-.34 0-.65-.17-.82-.45L5.7 12.16zM7.5 14h10.36L16 9H9l-1.5 5z"/> | |
| </svg> | |
| <span id="cart-counter" class="absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-red-500 text-white text-sm font-bold rounded-full h-6 w-6 flex items-center justify-center">0</span> | |
| </button> | |
| <!-- Modal del carrito de compras --> | |
| <div id="cart-modal" class="fixed inset-0 bg-black bg-opacity-70 hidden items-center justify-center p-4 z-[100]"> | |
| <div class="bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-xl relative transition-all duration-300 transform scale-95 opacity-0"> | |
| <!-- Botón para cerrar la modal --> | |
| <button id="close-cart-btn" class="absolute top-3 right-3 text-gray-400 hover:text-gray-100 text-3xl transition-colors"> | |
| × | |
| </button> | |
| <h2 class="text-3xl font-bold mb-4 text-orange-400">Tu Pedido</h2> | |
| <div id="cart-items" class="max-h-[50vh] overflow-y-auto mb-4 border-b border-gray-700 pb-4 space-y-3"> | |
| <!-- Los elementos del carrito se renderizarán aquí --> | |
| <p class="text-center text-gray-400 text-base">El carrito está vacío.</p> | |
| </div> | |
| <div class="flex justify-between items-center text-2xl font-semibold mb-6"> | |
| <span>Total:</span> | |
| <span id="cart-total" class="text-orange-400">$0.00</span> | |
| </div> | |
| <!-- Botones de acción --> | |
| <div class="flex flex-col space-y-4"> | |
| <button id="send-whatsapp-btn" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-xl transition-colors shadow-lg text-xl"> | |
| Enviar pedido por WhatsApp | |
| </button> | |
| <button id="send-email-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-xl transition-colors shadow-lg text-xl"> | |
| Enviar pedido por Email | |
| </button> | |
| <button id="pay-local-btn" class="w-full bg-orange-600 hover:bg-orange-700 text-white font-bold py-3 px-6 rounded-xl transition-colors shadow-lg text-xl"> | |
| Pagar o comprar en el local | |
| </button> | |
| <button id="clear-cart-btn" class="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-xl transition-colors shadow-lg text-xl"> | |
| Vaciar carrito | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Notificación (Toast) de producto agregado --> | |
| <div id="toast-notification" class="fixed top-6 left-1/2 transform -translate-x-1/2 bg-gray-800 text-green-400 px-6 py-3 rounded-xl shadow-lg transition-all duration-300 ease-out opacity-0 translate-y-4 z-[200]"> | |
| <span id="toast-message" class="text-lg"></span> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Variable global para los datos del menú | |
| let menuData = []; | |
| // Variables de estado | |
| let cart = []; | |
| let total = 0; | |
| let totalItems = 0; | |
| // Elementos del DOM | |
| const menuContainer = document.getElementById('menu-container'); | |
| const loadingSpinner = document.getElementById('loading-spinner'); | |
| const searchInput = document.getElementById('search-input'); | |
| const cartModal = document.getElementById('cart-modal'); | |
| const openCartBtn = document.getElementById('open-cart-btn'); | |
| const closeCartBtn = document.getElementById('close-cart-btn'); | |
| const cartItemsContainer = document.getElementById('cart-items'); | |
| const cartTotalEl = document.getElementById('cart-total'); | |
| const sendWhatsappBtn = document.getElementById('send-whatsapp-btn'); | |
| const sendEmailBtn = document.getElementById('send-email-btn'); | |
| const payLocalBtn = document.getElementById('pay-local-btn'); | |
| const clearCartBtn = document.getElementById('clear-cart-btn'); | |
| const cartCounterEl = document.getElementById('cart-counter'); | |
| const toastNotificationEl = document.getElementById('toast-notification'); | |
| const toastMessageEl = document.getElementById('toast-message'); | |
| // Función para renderizar los elementos del menú | |
| function renderMenu(productsToRender) { | |
| // Limpiar el contenedor | |
| menuContainer.innerHTML = ''; | |
| const products = productsToRender || menuData; | |
| if (products.length === 0) { | |
| menuContainer.innerHTML = ` | |
| <div class="col-span-1 sm:col-span-2 lg:col-span-3 text-center p-8"> | |
| <p class="text-gray-400 text-lg">No se encontraron productos para mostrar.</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| products.forEach(item => { | |
| // Buscar la cantidad actual en el carrito o empezar en 0 | |
| const cartItem = cart.find(i => i.id === item.id); | |
| const currentQuantity = cartItem ? cartItem.quantity : 0; | |
| const card = document.createElement('div'); | |
| card.className = 'bg-gray-800 rounded-2xl shadow-xl overflow-hidden transform hover:scale-105 transition-transform duration-300 ease-in-out cursor-pointer'; | |
| card.innerHTML = ` | |
| <img src="${item.image}" alt="${item.name}" class="w-full h-48 object-cover" onerror="this.src='${item.fallbackImage}'"> | |
| <div class="p-4"> | |
| <h3 class="text-2xl font-bold text-orange-400 mb-1">${item.name}</h3> | |
| <p class="text-gray-300 text-base mb-3">${item.description}</p> | |
| <div class="flex items-center justify-between"> | |
| <span class="text-xl font-bold text-white">$${item.price.toFixed(2)}</span> | |
| <div class="flex items-center space-x-1" data-id="${item.id}"> | |
| <button class="quantity-btn-minus bg-gray-700 hover:bg-gray-600 text-white text-2xl font-bold w-10 h-10 rounded-full shadow-md transition-colors transform active:scale-95"> | |
| - | |
| </button> | |
| <span class="quantity-display text-xl font-bold w-10 text-center text-white">${currentQuantity}</span> | |
| <button class="quantity-btn-plus bg-orange-500 hover:bg-orange-600 text-white text-2xl font-bold w-10 h-10 rounded-full shadow-md transition-colors transform active:scale-95"> | |
| + | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| menuContainer.appendChild(card); | |
| }); | |
| } | |
| // Función para obtener los datos del menú desde el servidor | |
| async function fetchMenuData() { | |
| try { | |
| const response = await fetch('productos.php'); | |
| if (!response.ok) { | |
| throw new Error('Error al cargar los productos'); | |
| } | |
| menuData = await response.json(); | |
| renderMenu(menuData); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| menuContainer.innerHTML = ` | |
| <div class="col-span-1 sm:col-span-2 lg:col-span-3 text-center p-8"> | |
| <p class="text-red-400 text-lg">Ocurrió un error al cargar el menú. Por favor, intenta de nuevo más tarde.</p> | |
| </div> | |
| `; | |
| } | |
| } | |
| // Función para actualizar el carrito y refrescar la UI | |
| function updateCart(itemId, quantityChange) { | |
| const existingItemIndex = cart.findIndex(item => item.id === itemId); | |
| const menuItem = menuData.find(item => item.id === itemId); | |
| if (!menuItem) return; | |
| if (existingItemIndex > -1) { | |
| cart[existingItemIndex].quantity += quantityChange; | |
| } else if (quantityChange > 0) { | |
| cart.push({ ...menuItem, quantity: quantityChange }); | |
| } | |
| // Filtrar elementos con cantidad 0 o menos | |
| cart = cart.filter(item => item.quantity > 0); | |
| // Recalcular el total y el número de artículos | |
| total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0); | |
| totalItems = cart.reduce((sum, item) => sum + item.quantity, 0); | |
| renderCart(); | |
| updateCartCounter(); | |
| // Actualizar la cantidad en la tarjeta del menú | |
| const card = menuContainer.querySelector(`[data-id="${itemId}"]`); | |
| if (card) { | |
| const quantityDisplay = card.querySelector('.quantity-display'); | |
| const cartItem = cart.find(i => i.id === itemId); | |
| quantityDisplay.textContent = cartItem ? cartItem.quantity : 0; | |
| } | |
| } | |
| // Función para actualizar el contador del carrito | |
| function updateCartCounter() { | |
| cartCounterEl.textContent = totalItems; | |
| if (totalItems > 0) { | |
| cartCounterEl.classList.remove('hidden'); | |
| cartCounterEl.classList.add('flex'); | |
| openCartBtn.classList.add('pulse-once'); | |
| } else { | |
| cartCounterEl.classList.add('hidden'); | |
| cartCounterEl.classList.remove('flex'); | |
| } | |
| } | |
| // Función para renderizar el carrito | |
| function renderCart() { | |
| cartItemsContainer.innerHTML = ''; | |
| if (cart.length === 0) { | |
| cartItemsContainer.innerHTML = '<p class="text-center text-gray-400 py-4 text-base">El carrito está vacío.</p>'; | |
| } else { | |
| cart.forEach(item => { | |
| const cartItemEl = document.createElement('div'); | |
| cartItemEl.className = 'flex items-center justify-between py-3 border-b border-gray-700'; | |
| cartItemEl.innerHTML = ` | |
| <div class="flex-1"> | |
| <span class="text-lg font-semibold text-gray-200">${item.name}</span> | |
| <span class="block text-gray-400 text-sm">Cantidad: ${item.quantity}</span> | |
| </div> | |
| <span class="text-lg font-bold text-orange-400">$${(item.price * item.quantity).toFixed(2)}</span> | |
| `; | |
| cartItemsContainer.appendChild(cartItemEl); | |
| }); | |
| } | |
| cartTotalEl.textContent = `$${total.toFixed(2)}`; | |
| } | |
| // Función para mostrar un mensaje en una notificación (toast) | |
| function showToast(message) { | |
| toastMessageEl.textContent = message; | |
| toastNotificationEl.classList.remove('opacity-0', 'translate-y-4'); | |
| toastNotificationEl.classList.add('opacity-100', 'translate-y-0'); | |
| setTimeout(() => { | |
| toastNotificationEl.classList.remove('opacity-100', 'translate-y-0'); | |
| toastNotificationEl.classList.add('opacity-0', 'translate-y-4'); | |
| }, 3000); // Ocultar después de 3 segundos | |
| } | |
| // Función para construir el mensaje del pedido | |
| function buildOrderMessage() { | |
| if (cart.length === 0) { | |
| return null; | |
| } | |
| let message = "¡Hola! Me gustaría hacer el siguiente pedido:\n\n"; | |
| cart.forEach(item => { | |
| message += `- ${item.name} (${item.quantity} unidad${item.quantity > 1 ? 'es' : ''}) - $${(item.price * item.quantity).toFixed(2)}\n`; | |
| }); | |
| message += `\nTotal: $${total.toFixed(2)}\n\n`; | |
| return message; | |
| } | |
| // Función para enviar el pedido a WhatsApp | |
| function sendWhatsApp() { | |
| const message = buildOrderMessage(); | |
| if (!message) { | |
| showToast("El carrito está vacío. Agrega productos antes de enviar el pedido."); | |
| return; | |
| } | |
| const encodedMessage = encodeURIComponent(message); | |
| const phoneNumber = "529842665680"; // Número sin el "+" | |
| const whatsappUrl = `https://wa.me/${phoneNumber}?text=${encodedMessage}`; | |
| window.open(whatsappUrl, '_blank'); | |
| } | |
| // Función para enviar el pedido por correo electrónico | |
| function sendEmail() { | |
| const message = buildOrderMessage(); | |
| if (!message) { | |
| showToast("El carrito está vacío. Agrega productos antes de enviar el pedido."); | |
| return; | |
| } | |
| const subject = "Nuevo Pedido de Menú Digital"; | |
| const body = message + "Espero tu confirmación. ¡Gracias!"; | |
| const emailAddress = "[email protected]"; | |
| const mailtoUrl = `mailto:${emailAddress}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; | |
| window.open(mailtoUrl, '_blank'); | |
| } | |
| // Función para manejar el pedido en el local | |
| function handlePayLocal() { | |
| if (cart.length === 0) { | |
| showToast("El carrito está vacío. Agrega productos antes de finalizar el pedido."); | |
| return; | |
| } | |
| showToast("¡Pedido listo! Puedes pasar a pagar y recogerlo en el local."); | |
| } | |
| // Event Listeners | |
| menuContainer.addEventListener('click', (e) => { | |
| const plusBtn = e.target.closest('.quantity-btn-plus'); | |
| const minusBtn = e.target.closest('.quantity-btn-minus'); | |
| if (plusBtn) { | |
| const itemId = parseInt(plusBtn.closest('[data-id]').dataset.id); | |
| updateCart(itemId, 1); | |
| const menuItem = menuData.find(item => item.id === itemId); | |
| if (menuItem) { | |
| showToast(`${menuItem.name} agregado al carrito.`); | |
| } | |
| } else if (minusBtn) { | |
| const itemId = parseInt(minusBtn.closest('[data-id]').dataset.id); | |
| // Solo si la cantidad es mayor a 0, restar | |
| const cartItem = cart.find(i => i.id === itemId); | |
| if (cartItem && cartItem.quantity > 0) { | |
| updateCart(itemId, -1); | |
| const menuItem = menuData.find(item => item.id === itemId); | |
| if (menuItem) { | |
| showToast(`${menuItem.name} eliminado del carrito.`); | |
| } | |
| } else { | |
| showToast("No hay elementos para eliminar."); | |
| } | |
| } | |
| }); | |
| // Listener para el buscador | |
| searchInput.addEventListener('input', (e) => { | |
| const searchTerm = e.target.value.toLowerCase(); | |
| const filteredProducts = menuData.filter(item => { | |
| return item.name.toLowerCase().includes(searchTerm) || | |
| item.description.toLowerCase().includes(searchTerm); | |
| }); | |
| renderMenu(filteredProducts); | |
| }); | |
| openCartBtn.addEventListener('click', () => { | |
| cartModal.classList.remove('hidden'); | |
| cartModal.classList.add('flex'); | |
| setTimeout(() => { | |
| const modalContent = cartModal.querySelector('div'); | |
| modalContent.classList.remove('scale-95', 'opacity-0'); | |
| modalContent.classList.add('scale-100', 'opacity-100'); | |
| }, 10); | |
| renderCart(); | |
| }); | |
| closeCartBtn.addEventListener('click', () => { | |
| const modalContent = cartModal.querySelector('div'); | |
| modalContent.classList.remove('scale-100', 'opacity-100'); | |
| modalContent.classList.add('scale-95', 'opacity-0'); | |
| setTimeout(() => { | |
| cartModal.classList.remove('flex'); | |
| cartModal.classList.add('hidden'); | |
| }, 300); | |
| }); | |
| clearCartBtn.addEventListener('click', () => { | |
| cart = []; | |
| total = 0; | |
| totalItems = 0; | |
| renderCart(); | |
| renderMenu(); // Refrescar el menú para resetear los contadores | |
| updateCartCounter(); | |
| showToast("El carrito ha sido vaciado."); | |
| }); | |
| sendWhatsappBtn.addEventListener('click', sendWhatsApp); | |
| sendEmailBtn.addEventListener('click', sendEmail); | |
| payLocalBtn.addEventListener('click', handlePayLocal); | |
| // Cierra la modal si se hace clic fuera del contenido | |
| cartModal.addEventListener('click', (e) => { | |
| if (e.target === cartModal) { | |
| const modalContent = cartModal.querySelector('div'); | |
| modalContent.classList.remove('scale-100', 'opacity-100'); | |
| modalContent.classList.add('scale-95', 'opacity-0'); | |
| setTimeout(() => { | |
| cartModal.classList.remove('flex'); | |
| cartModal.classList.add('hidden'); | |
| }, 300); | |
| } | |
| }); | |
| // Inicializa la página | |
| fetchMenuData(); // Llama a la nueva función para cargar los datos | |
| updateCartCounter(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment