Instantly share code, notes, and snippets.
Created
November 3, 2025 19:16
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save iDavidMorales/cedab11c0826eb85630f00346a4b00dd to your computer and use it in GitHub Desktop.
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>Dana Tours Riviera Maya</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"> | |
| <style> | |
| /* --------- Paginación / Modales / Botón flotante --------- */ | |
| .pagination-container{display:flex;justify-content:center;align-items:center;gap:.5rem;margin-top:1.25rem} | |
| .pagination-button{padding:.5rem 1rem;border-radius:9999px;border:1px solid #d1d5db;color:#4b5563;font-weight:500;transition:all .2s;cursor:pointer} | |
| .pagination-button:hover:not(.active){background-color:#f3f4f6} | |
| .pagination-button.active{background-color:#1f2937;color:#fff;border-color:#1f2937} | |
| .modal{display:none;position:fixed;z-index:50;inset:0;width:100%;height:100%;overflow:auto;background:rgba(0,0,0,.6)} | |
| .modal-content{background:#fff;margin:10% auto;padding:20px;border:1px solid #888;width:90%;max-width:600px;border-radius:12px;box-shadow:0 5px 15px rgba(0,0,0,.3);animation:fadeIn .3s} | |
| @keyframes fadeIn{from{opacity:0;transform:scale(.98)}to{opacity:1;transform:scale(1)}} | |
| .close-button{color:#aaa;float:right;font-size:28px;font-weight:700;cursor:pointer} | |
| .close-button:hover,.close-button:focus{color:#000} | |
| .floating-button{position:fixed;bottom:20px;right:20px;z-index:45} | |
| /* --------- Clamp por líneas + desvanecido --------- */ | |
| .clamp{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden} | |
| .clamp-1{-webkit-line-clamp:1} | |
| .clamp-2{-webkit-line-clamp:2} | |
| .clamp-3{-webkit-line-clamp:3} | |
| .clamp-4{-webkit-line-clamp:4} | |
| .clamp-fade{position:relative} | |
| .clamp-fade::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2.25rem;background:linear-gradient(to bottom, rgba(255,255,255,0), #fff);pointer-events:none} | |
| .no-fade::after{display:none} | |
| .read-toggle{display:none} | |
| /* --------- Slider: layout, dots y tarjeta --------- */ | |
| .slider-wrap{position:relative;overflow:hidden;border-radius:1rem} | |
| .slider-track{display:flex;transition:transform .6s ease;will-change:transform} | |
| .slider-slide{min-width:100%;height:52vh} | |
| @media (min-width:768px){.slider-slide{height:70vh}} | |
| .slider-dot{width:.6rem;height:.6rem;border-radius:9999px;background:#9ca3af;opacity:.7;border:1px solid rgba(255,255,255,.6)} | |
| .slider-dot.active{background:#111827;opacity:1;box-shadow:0 0 0 3px rgba(255,255,255,.5)} | |
| .slider-btn{position:absolute;top:50%;transform:translateY(-50%);background:rgba(17,24,39,.7);color:#fff;border:1px solid rgba(255,255,255,.2);width:42px;height:42px;border-radius:9999px;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)} | |
| .slider-btn:hover{background:rgba(17,24,39,.9)} | |
| .slider-btn.prev{left:.75rem} | |
| .slider-btn.next{right:.75rem} | |
| .slider-card{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.35);backdrop-filter:blur(8px);border-radius:1rem;padding:1rem 1rem 1.25rem;} | |
| @media (min-width:768px){.slider-card{padding:1.25rem 1.5rem 1.5rem}} | |
| .btn{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:9999px;font-weight:600;transition:all .15s;border:1px solid transparent} | |
| .btn-primary{background:#10b981;color:#fff;border-color:#0ea371} | |
| .btn-primary:hover{background:#0ea371} | |
| .btn-ghost{background:#fff;color:#111827} | |
| .btn-ghost:hover{background:#f3f4f6} | |
| .btn-outline{background:rgba(255,255,255,.08);color:#fff;border-color:rgba(255,255,255,.6)} | |
| .btn-outline:hover{background:rgba(255,255,255,.18)} | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <!-- Navegación --> | |
| <nav class="sticky top-0 bg-white p-4 shadow-md z-40"> | |
| <div class="container mx-auto flex justify-between items-center"> | |
| <div class="flex items-center space-x-2"> | |
| <img src="https://i.imgur.com/KVwHRYl.png" alt="Dana Tours Logo" class="w-[60px]"> | |
| </div> | |
| <div class="md:hidden"> | |
| <button id="menu-button" class="text-gray-800 focus:outline-none"> | |
| <i class="fas fa-bars text-xl"></i> | |
| </button> | |
| </div> | |
| <div id="desktop-menu" class="hidden md:flex space-x-6"> | |
| <a href="/" class="text-gray-600 hover:text-gray-800 transition-colors flex items-center space-x-1"><i class="fas fa-home"></i><span>Inicio</span></a> | |
| <a href="#tours" class="text-gray-600 hover:text-gray-800 transition-colors flex items-center space-x-1"><i class="fas fa-map-marked-alt"></i><span>Tours</span></a> | |
| <a href="#promociones" class="text-gray-600 hover:text-gray-800 transition-colors flex items-center space-x-1"><i class="fas fa-tags"></i><span>Promociones</span></a> | |
| <a href="#contacto" class="text-gray-600 hover:text-gray-800 transition-colors flex items-center space-x-1"><i class="fas fa-envelope"></i><span>Contacto</span></a> | |
| <button id="currency-toggle-btn" class="text-gray-600 hover:text-gray-800 transition-colors flex items-center space-x-1"> | |
| <i class="fas fa-money-bill-wave"></i><span id="currency-label">MXN</span> | |
| </button> | |
| <button id="open-language-modal-btn" class="text-gray-600 hover:text-gray-800 transition-colors flex items-center space-x-1"> | |
| <i class="fas fa-globe"></i><span>Idioma</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Menú móvil --> | |
| <div id="mobile-menu" class="md:hidden hidden mt-4"> | |
| <a href="/" class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100">Inicio</a> | |
| <a href="#tours" class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100">Tours</a> | |
| <a href="#promociones" class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100">Promociones</a> | |
| <a href="#contacto" class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100">Contacto</a> | |
| <button id="currency-toggle-btn-mobile" class="w-full text-left py-2 px-4 text-sm text-gray-700 hover:bg-gray-100"> | |
| Cambiar a <span id="currency-label-mobile">USD</span> | |
| </button> | |
| <button id="open-language-modal-btn-mobile" class="w-full text-left py-2 px-4 text-sm text-gray-700 hover:bg-gray-100">Idioma</button> | |
| </div> | |
| </nav> | |
| <main class="container mx-auto mt-8 p-4"> | |
| <!-- Búsqueda --> | |
| <div class="mb-8 max-w-lg mx-auto"> | |
| <div class="relative"> | |
| <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> | |
| <i class="fas fa-search text-gray-400"></i> | |
| </div> | |
| <input id="tour-search-input" type="text" placeholder="Busca tu tour por nombre o descripción..." class="w-full p-3 pl-12 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-800 transition"/> | |
| </div> | |
| </div> | |
| <!-- Slider (desde API) --> | |
| <section class="mb-12"> | |
| <div class="slider-wrap ring-1 ring-gray-200"> | |
| <div id="sliderTrack" class="slider-track"> | |
| <!-- Slides generados desde la API --> | |
| </div> | |
| <button class="slider-btn prev" id="prevBtn" aria-label="Anterior"><i class="fas fa-chevron-left"></i></button> | |
| <button class="slider-btn next" id="nextBtn" aria-label="Siguiente"><i class="fas fa-chevron-right"></i></button> | |
| </div> | |
| <div id="sliderDots" class="flex justify-center gap-2 mt-3"></div> | |
| </section> | |
| <!-- Tours --> | |
| <section id="tours" class="mb-12"> | |
| <h2 class="text-3xl font-bold text-center mb-4">Nuestros Tours Destacados</h2> | |
| <div id="tours-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <p class="text-center text-gray-500 col-span-full" id="tours-loading-message">Cargando tours...</p> | |
| </div> | |
| <div id="tours-pagination" class="pagination-container"></div> | |
| </section> | |
| <!-- Promociones --> | |
| <section id="promociones" class="mb-12 p-4 md:p-8 bg-white rounded-xl shadow-sm"> | |
| <h2 class="text-3xl font-bold text-center mb-6">Promociones y Cupones</h2> | |
| <div id="promotions-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <div id="promotions-loading-spinner" class="col-span-full flex flex-col items-center justify-center p-12 text-gray-500"> | |
| <i class="fas fa-spinner fa-spin text-4xl mb-4"></i> | |
| <p>Cargando promociones...</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Contacto --> | |
| <section id="contacto" class="my-12 p-8 bg-white rounded-lg shadow-md"> | |
| <h2 class="text-3xl font-bold text-center mb-6 text-gray-800">Contáctanos para una Cotización</h2> | |
| <form id="contact-form" class="max-w-xl mx-auto space-y-4"> | |
| <div id="quote-details-container" class="border p-4 rounded-lg bg-gray-50 mb-4"> | |
| <h4 class="font-bold text-lg mb-2">Resumen de tu Cotización</h4> | |
| <div id="quote-summary-list" class="space-y-2"></div> | |
| <div id="quote-summary-prices" class="mt-4 pt-2 border-t text-sm space-y-1"></div> | |
| <button type="button" id="edit-quote-button" class="mt-4 w-full text-center text-sm bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-2 rounded-lg transition-colors"> | |
| <i class="fas fa-edit mr-2"></i> Editar Cotización | |
| </button> | |
| </div> | |
| <div> | |
| <label for="name" class="block text-gray-600 font-semibold mb-2">Nombre Completo</label> | |
| <input id="name" name="name" type="text" required class="w-full p-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-800"/> | |
| </div> | |
| <div> | |
| <label for="email" class="block text-gray-600 font-semibold mb-2">Correo Electrónico</label> | |
| <input id="email" name="email" type="email" required class="w-full p-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-800"/> | |
| </div> | |
| <div> | |
| <label for="phone" class="block text-gray-600 font-semibold mb-2">Teléfono</label> | |
| <input id="phone" name="phone" type="tel" class="w-full p-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-800"/> | |
| </div> | |
| <input type="hidden" id="final-quote-message" name="message"/> | |
| <div class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4"> | |
| <button type="button" id="whatsapp-button" class="w-full bg-gray-800 hover:bg-gray-700 text-white p-3 rounded-lg font-semibold transition-colors flex items-center justify-center"> | |
| <i class="fab fa-whatsapp mr-2"></i> Enviar a WhatsApp | |
| </button> | |
| <button type="button" id="email-button" class="w-full bg-gray-600 hover:bg-gray-800 text-white p-3 rounded-lg font-semibold transition-colors flex items-center justify-center"> | |
| <i class="fas fa-envelope mr-2"></i> Enviar por Correo | |
| </button> | |
| <button type="button" id="paypal-button" class="w-full bg-yellow-500 hover:bg-yellow-600 text-white p-3 rounded-lg font-semibold transition-colors flex items-center justify-center"> | |
| <i class="fab fa-paypal mr-2"></i> Pagar con PayPal | |
| </button> | |
| </div> | |
| </form> | |
| </section> | |
| </main> | |
| <!-- Botón flotante --> | |
| <div class="floating-button"> | |
| <button id="summary-button" class="bg-gray-800 hover:bg-gray-700 text-white px-6 py-4 rounded-full shadow-lg transition-transform transform hover:scale-110 relative"> | |
| <i class="fas fa-shopping-bag text-2xl"></i> | |
| <span id="selection-count" class="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold w-6 h-6 flex items-center justify-center rounded-full border-2 border-white">0</span> | |
| </button> | |
| </div> | |
| <!-- Modales --> | |
| <div id="item-details-modal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-button" onclick="closeModal('item-details-modal')">×</span> | |
| <div id="modal-body-content"></div> | |
| <div class="modal-footer mt-4 text-right"> | |
| <button class="bg-gray-800 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition-colors" onclick="closeModal('item-details-modal')">Cerrar</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="language-modal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-button" onclick="closeModal('language-modal')">×</span> | |
| <h3 class="text-xl font-semibold mb-4 text-center">Selecciona tu Idioma</h3> | |
| <div id="google_translate_element_modal" class="flex justify-center"></div> | |
| </div> | |
| </div> | |
| <div id="quote-builder-modal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-button" onclick="closeModal('quote-builder-modal')">×</span> | |
| <h3 class="text-2xl font-semibold mb-4">Tu Cotización</h3> | |
| <div id="quote-actions" class="mb-4 text-right"> | |
| <button id="clear-all-button" class="text-sm text-red-500 hover:underline"><i class="fas fa-trash-alt mr-1"></i>Vaciar Carrito</button> | |
| </div> | |
| <form id="quote-form" class="space-y-4"> | |
| <div id="quote-items-container" class="space-y-6"></div> | |
| <div id="quote-summary" class="mt-8 border-t-2 border-gray-800 pt-4 space-y-2 font-bold text-lg"> | |
| <p>Subtotal: <span id="subtotal-price" class="float-right text-gray-800">$0.00 MXN</span></p> | |
| <p>Descuento: <span id="discount-price" class="float-right text-red-500">$0.00 MXN</span></p> | |
| <p class="text-2xl">Total: <span id="total-price" class="float-right text-gray-800">$0.00 MXN</span></p> | |
| </div> | |
| <button type="submit" class="w-full bg-gray-800 hover:bg-gray-700 text-white p-3 rounded-lg font-semibold transition-colors"> | |
| Continuar al Contacto | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Google Translate --> | |
| <script> | |
| function googleTranslateElementInit() { | |
| new google.translate.TranslateElement({ | |
| pageLanguage: 'es', | |
| layout: google.translate.TranslateElement.InlineLayout.DROPDOWN | |
| }, 'google_translate_element_modal'); | |
| } | |
| </script> | |
| <script src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script> | |
| <style> | |
| /* Widget translate */ | |
| .goog-te-gadget-simple, .goog-te-combo{border:1px solid #ccc;border-radius:5px;background:#fff;color:#333;padding:5px} | |
| .goog-te-combo{width:auto!important;appearance:none;background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-down-fill" viewBox="0 0 16 16"><path d="M7.247 11.14l-2.433-2.433a.5.5 0 0 1 .708-.708L8 10.292l2.488-2.488a.5.5 0 0 1 .708.708L8.707 11.14a.5.5 0 0 1-.707 0z"/></svg>');background-repeat:no-repeat;background-position:right 8px center;padding-right:25px} | |
| .goog-logo-link{display:none!important} | |
| .goog-te-gadget-simple{font-size:0} | |
| </style> | |
| <script> | |
| /* ===================== HELPERS DE TEXTO & SEGURIDAD ===================== */ | |
| const decodeEntities = (s) => { const txt = document.createElement('textarea'); txt.innerHTML = s; return txt.value; }; | |
| const escapeHTML = (s) => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); | |
| const normalizeText = (s) => { | |
| if (s == null) return ''; | |
| let t = String(s).trim(); | |
| if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) t = t.slice(1,-1); | |
| t = t.replace(/\\"/g,'"').replace(/\\'/g,"'"); | |
| t = t.replace(/\\r\\n|\\n|\\r/g,'\n'); | |
| t = decodeEntities(t); | |
| t = t.replace(/\n{3,}/g,'\n\n'); | |
| return t.trim(); | |
| }; | |
| const toPreview = (t) => normalizeText(t).replace(/\s*\n+\s*/g,' • '); | |
| // Pasa líneas con -, •, – a <ul> y párrafos; añade respiración entre bloques | |
| const renderRichTextInto = (container, raw) => { | |
| const t = normalizeText(raw); | |
| const lines = t.split('\n').map(l => l.trim()); | |
| let html = '', inList = false; | |
| const openList = () => { if(!inList){ html+='<ul class="list-disc pl-5 mb-2 space-y-1">'; inList=true; } }; | |
| const closeList = () => { if(inList){ html+='</ul>'; inList=false; } }; | |
| for (const line of lines) { | |
| if (!line) { closeList(); html += '<div class="h-3"></div>'; continue; } | |
| if (/^[-–•]\s+/.test(line)) { openList(); html += `<li>${escapeHTML(line.replace(/^[-–•]\s+/,''))}</li>`; } | |
| else if (/^[A-Za-zÁÉÍÓÚÜÑ0-9].*:$/.test(line)) { closeList(); html += `<p class="font-semibold mt-2">${escapeHTML(line)}</p>`; } | |
| else { closeList(); html += `<p class="mb-2 leading-relaxed">${escapeHTML(line)}</p>`; } | |
| } | |
| closeList(); | |
| container.innerHTML += html; | |
| }; | |
| /* ===================== STATE ===================== */ | |
| let toursData = []; | |
| let promotionsData = { coupons: [], flashSales: [] }; | |
| let selection = []; | |
| let currentCurrency = 'MXN'; // <--- por defecto MXN | |
| const exchangeRate = 17.0; // 1 USD = 17 MXN | |
| const toursPerPage = 6; | |
| let currentPage = 1; | |
| /* =============== SLIDER STATE/UTILS (desde API) =============== */ | |
| let sliderIndex = 0, sliderTimer = null; | |
| const SLIDER_MAX = 6; | |
| // Subtítulo compacto para slider (sin viñetas ni saltos) | |
| const toSliderSubtitleRaw = (raw) => { | |
| const t = normalizeText(raw) | |
| .replace(/^\s*[-–•]\s*/gm, '') | |
| .replace(/\s*\n+\s*/g, ' ') | |
| .replace(/\s{2,}/g, ' ') | |
| .trim(); | |
| return t; | |
| }; | |
| // Truncado por palabras con elipsis | |
| const truncateByChars = (s, max) => { | |
| if (!s) return ''; | |
| if (s.length <= max) return s; | |
| const cut = s.slice(0, max); | |
| const idx = cut.lastIndexOf(' '); | |
| return (idx > 30 ? cut.slice(0, idx) : cut) + '…'; | |
| }; | |
| const setResponsiveTruncation = (selector, maxMobile, maxDesktop) => { | |
| const isMobile = window.matchMedia('(max-width: 768px)').matches; | |
| document.querySelectorAll(selector).forEach(el => { | |
| const full = el.getAttribute('data-fulltext') || el.textContent; | |
| el.setAttribute('data-fulltext', full); | |
| el.textContent = truncateByChars(full, isMobile ? maxMobile : maxDesktop); | |
| }); | |
| }; | |
| const applySliderTruncation = () => { | |
| setResponsiveTruncation('.slider-title', 28, 42); | |
| setResponsiveTruncation('.slider-subtitle', 80, 120); | |
| }; | |
| const renderSlider = (slides) => { | |
| const track = document.getElementById('sliderTrack'); | |
| const dots = document.getElementById('sliderDots'); | |
| const wrap = document.querySelector('.slider-wrap'); | |
| if (!track || !dots || !wrap) return; | |
| if (!slides.length) { wrap.style.display='none'; return; } | |
| track.innerHTML=''; dots.innerHTML=''; | |
| slides.forEach((s,i)=>{ | |
| const slide = document.createElement('div'); | |
| slide.className = 'slider-slide relative'; | |
| slide.innerHTML = ` | |
| <img src="${s.img}" alt="${escapeHTML(s.title)}" class="w-full h-full object-cover" onerror="this.src='https://placehold.co/1400x700/e2e8f0/64748b?text=Imagen';"> | |
| <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/35 to-transparent"></div> | |
| <div class="absolute inset-0 flex items-end md:items-center"> | |
| <div class="px-5 md:px-10 pb-8 md:pb-0 max-w-[48ch] text-white"> | |
| <div class="slider-card"> | |
| <h2 class="slider-title text-2xl md:text-4xl font-bold clamp clamp-2" data-fulltext="${escapeHTML(s.title)}">${escapeHTML(s.title)}</h2> | |
| <p class="slider-subtitle mt-2 md:mt-3 text-sm md:text-lg text-white/90 clamp clamp-3 leading-relaxed" data-fulltext="${escapeHTML(s.subtitle)}">${escapeHTML(s.subtitle)}</p> | |
| <div class="mt-4 flex flex-wrap gap-2"> | |
| <button class="btn btn-primary slider-reserve" data-id="${s.id}" aria-label="Reservar"><i class="fas fa-cart-plus"></i> Reservar</button> | |
| <button class="btn btn-outline slider-details" data-id="${s.id}" aria-label="Ver detalles"><i class="fas fa-info-circle"></i> Detalles</button> | |
| <button class="btn btn-ghost slider-contact" aria-label="Contactar"><i class="fas fa-paper-plane"></i> Contactar</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div>`; | |
| track.appendChild(slide); | |
| const dot = document.createElement('button'); | |
| dot.className='slider-dot'; dot.setAttribute('aria-label',`Ir al slide ${i+1}`); | |
| dot.addEventListener('click', ()=>goToSlide(i)); | |
| dots.appendChild(dot); | |
| }); | |
| goToSlide(0, false); | |
| applySliderTruncation(); | |
| startAutoplay(); | |
| }; | |
| const goToSlide = (i, animate=true) => { | |
| const slidesCount = document.querySelectorAll('#sliderTrack .slider-slide').length || 1; | |
| sliderIndex = (i + slidesCount) % slidesCount; | |
| const track = document.getElementById('sliderTrack'); | |
| track.style.transition = animate ? 'transform .6s ease' : 'none'; | |
| track.style.transform = `translateX(-${sliderIndex*100}%)`; | |
| document.querySelectorAll('.slider-dot').forEach((d,idx)=>d.classList.toggle('active', idx===sliderIndex)); | |
| }; | |
| const nextSlide = () => goToSlide(sliderIndex+1); | |
| const prevSlide = () => goToSlide(sliderIndex-1); | |
| const startAutoplay = () => { stopAutoplay(); sliderTimer = setInterval(nextSlide, 5000); }; | |
| const stopAutoplay = () => { if (sliderTimer) clearInterval(sliderTimer); sliderTimer=null; }; | |
| const buildSliderFromTours = (tours) => { | |
| const chosen = tours.filter(t => t.image && String(t.image).trim() !== '').slice(0, SLIDER_MAX); | |
| const slides = chosen.map(t => ({ | |
| id: t.id, | |
| title: t.name, | |
| subtitle: toSliderSubtitleRaw(t.description || ''), | |
| img: t.image | |
| })); | |
| renderSlider(slides); | |
| }; | |
| /* ===================== LOCAL STORAGE ===================== */ | |
| const saveSelectionToLocalStorage = () => localStorage.setItem('routicketSelection', JSON.stringify(selection)); | |
| const loadSelectionFromLocalStorage = () => { const s = localStorage.getItem('routicketSelection'); if (s) selection = JSON.parse(s); }; | |
| /* ===================== FORMATO MONEDA ===================== */ | |
| const formatPrice = (price, currency = currentCurrency) => { | |
| let p = parseFloat(price); | |
| if (currency === 'USD') p /= exchangeRate; // origen MXN | |
| const num = p.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); | |
| return `$${num} ${currency}`; | |
| }; | |
| /* ===================== UI UPDATES ===================== */ | |
| const updateSelectionCount = () => { document.getElementById('selection-count').textContent = selection.length; }; | |
| const updateButtonStates = () => { | |
| document.querySelectorAll('.tour-select-button').forEach(btn => { | |
| const id = parseInt(btn.dataset.id); | |
| const isSelected = selection.some(it => it.id === id && it.type === 'tour'); | |
| btn.innerHTML = isSelected ? '<i class="fas fa-check-circle mr-2"></i>Añadido' : '<i class="fas fa-plus-circle mr-2"></i>Añadir'; | |
| btn.classList.toggle('bg-red-500', isSelected); | |
| btn.classList.toggle('text-white', isSelected); | |
| btn.classList.toggle('bg-gray-200', !isSelected); | |
| btn.classList.toggle('text-gray-600', !isSelected); | |
| }); | |
| document.querySelectorAll('.coupon-select-button').forEach(btn => { | |
| const id = parseInt(btn.dataset.id); | |
| const isSelected = selection.some(it => it.id === id && it.type === 'coupon'); | |
| btn.innerHTML = isSelected ? '<i class="fas fa-check-circle mr-2"></i>Añadido' : '<i class="fas fa-plus-circle mr-2"></i>Añadir'; | |
| btn.classList.toggle('bg-red-500', isSelected); | |
| btn.classList.toggle('text-white', isSelected); | |
| btn.classList.toggle('bg-gray-200', !isSelected); | |
| btn.classList.toggle('text-gray-600', !isSelected); | |
| }); | |
| }; | |
| const updateTourPricesOnScreen = () => { | |
| document.querySelectorAll('#tours-grid .tour-card').forEach(card => { | |
| const tourId = parseInt(card.dataset.id); | |
| const t = toursData.find(x => x.id === tourId); | |
| if (!t) return; | |
| const priceEl = card.querySelector('.tour-price'); | |
| const flashSale = promotionsData.flashSales.find(fs => fs.tourId === tourId); | |
| let displayPrice = parseFloat(t.precio); | |
| if (flashSale) displayPrice *= (1 - flashSale.discount); | |
| if (priceEl) priceEl.textContent = `${formatPrice(displayPrice)} por persona`; | |
| }); | |
| }; | |
| const calculatePrices = () => { | |
| let subtotal = 0; | |
| selection.filter(it => it.type === 'tour').forEach(it => { | |
| const t = toursData.find(x => x.id === it.id); | |
| if (!t) return; | |
| let itemPrice = parseFloat(t.precio); | |
| const flashSale = promotionsData.flashSales.find(fs => fs.tourId === it.id); | |
| if (flashSale) itemPrice *= (1 - flashSale.discount); | |
| subtotal += (it.quantity || 1) * itemPrice; | |
| }); | |
| const appliedCoupon = selection.find(it => it.type === 'coupon'); | |
| let discountAmount = 0; | |
| if (appliedCoupon) { | |
| const c = promotionsData.coupons.find(x => x.id === appliedCoupon.id); | |
| if (c?.descuento) discountAmount = subtotal * (parseFloat(c.descuento) / 100); | |
| } | |
| const total = Math.max(0, subtotal - discountAmount); | |
| return { subtotal, discountAmount, total }; | |
| }; | |
| const updateAllPricesUI = () => { | |
| const { subtotal, discountAmount, total } = calculatePrices(); | |
| document.getElementById('subtotal-price').textContent = formatPrice(subtotal); | |
| document.getElementById('discount-price').textContent = formatPrice(discountAmount); | |
| document.getElementById('total-price').textContent = formatPrice(total); | |
| const list = document.getElementById('quote-summary-list'); | |
| const prices = document.getElementById('quote-summary-prices'); | |
| list.innerHTML = ''; prices.innerHTML = ''; | |
| if (selection.length === 0) { | |
| list.innerHTML = '<p class="text-gray-500">Aún no has seleccionado nada.</p>'; | |
| } else { | |
| selection.forEach(it => { | |
| const div = document.createElement('div'); | |
| if (it.type === 'tour') { | |
| const t = toursData.find(x => x.id === it.id); | |
| if (t) div.innerHTML = `<p class="font-semibold">${escapeHTML(t.name)} (${it.quantity}p)</p>`; | |
| } else if (it.type === 'coupon') { | |
| const c = promotionsData.coupons.find(x => x.id === it.id); | |
| if (c) div.innerHTML = `<p class="font-semibold text-green-600">Cupón: ${escapeHTML(c.nombre)}</p>`; | |
| } | |
| list.appendChild(div); | |
| }); | |
| prices.innerHTML = ` | |
| <p>Subtotal: <span class="float-right font-bold">${formatPrice(subtotal)}</span></p> | |
| <p>Descuento: <span class="float-right font-bold text-red-500">- ${formatPrice(discountAmount)}</span></p> | |
| <p class="text-lg mt-2">Total: <span class="float-right font-bold">${formatPrice(total)}</span></p> | |
| `; | |
| } | |
| updateFinalQuoteMessage(); | |
| }; | |
| /* ===================== RENDER TOURS ===================== */ | |
| const displayTours = (arr) => { | |
| const grid = document.getElementById('tours-grid'); | |
| const pag = document.getElementById('tours-pagination'); | |
| grid.innerHTML=''; pag.innerHTML=''; | |
| if (!arr.length) { | |
| grid.innerHTML = `<p class="text-center text-gray-500 col-span-full">No se encontraron tours que coincidan con tu búsqueda.</p>`; | |
| return; | |
| } | |
| const start = (currentPage-1)*toursPerPage, end = start + toursPerPage; | |
| const page = arr.slice(start, end); | |
| page.forEach(tour => { | |
| const card = document.createElement('div'); | |
| card.className = 'tour-card bg-white rounded-lg shadow-lg overflow-hidden flex flex-col'; | |
| card.dataset.id = tour.id; | |
| card.innerHTML = ` | |
| <div class="relative"> | |
| <img src="${tour.image}" alt="${escapeHTML(tour.name)}" class="w-full h-48 object-cover" onerror="this.src='https://placehold.co/800x400/e2e8f0/64748b?text=Imagen+no+disponible';"> | |
| </div> | |
| <div class="p-4 flex flex-col flex-grow"> | |
| <h3 class="text-xl font-semibold text-gray-800 mb-2 clamp clamp-2" title="${escapeHTML(tour.name)}">${escapeHTML(tour.name)}</h3> | |
| <p class="tour-desc text-sm text-gray-600 mb-2 flex-grow clamp clamp-3 clamp-fade"></p> | |
| <button type="button" class="read-toggle text-xs underline text-gray-700 self-start mb-2">Leer más</button> | |
| <p class="text-lg font-bold text-gray-800 mb-4 tour-price">${formatPrice(parseFloat(tour.precio))} por persona</p> | |
| <div class="mt-auto"> | |
| <button class="w-full bg-gray-800 hover:bg-gray-700 text-white px-4 py-2 rounded-full transition-colors view-details-button" data-id="${tour.id}" data-type="tour"><i class="fas fa-info-circle mr-2"></i>Ver Detalles</button> | |
| <button class="w-full px-4 py-2 rounded-full mt-2 transition-colors tour-select-button" data-id="${tour.id}" data-type="tour"></button> | |
| </div> | |
| </div>`; | |
| grid.appendChild(card); | |
| const descEl = card.querySelector('.tour-desc'); | |
| descEl.textContent = toPreview(tour.description); | |
| requestAnimationFrame(() => { | |
| const btn = card.querySelector('.read-toggle'); | |
| const overflow = descEl.scrollHeight > descEl.clientHeight + 1; | |
| if (overflow) btn.style.display='inline-flex'; | |
| else descEl.classList.remove('clamp-fade'); | |
| }); | |
| }); | |
| const totalPages = Math.ceil(arr.length/toursPerPage); | |
| if (totalPages>1) { | |
| for (let i=1;i<=totalPages;i++){ | |
| const b = document.createElement('button'); | |
| b.textContent = i; b.className = 'pagination-button'; | |
| if (i===currentPage) b.classList.add('active'); | |
| b.onclick = () => { currentPage=i; displayTours(arr); }; | |
| pag.appendChild(b); | |
| } | |
| } | |
| updateButtonStates(); | |
| updateTourPricesOnScreen(); | |
| }; | |
| const renderQuoteBuilder = () => { | |
| const c = document.getElementById('quote-items-container'); | |
| c.innerHTML = selection.length ? '' : '<p class="text-center text-gray-500">Tu carrito está vacío.</p>'; | |
| selection.forEach(it => { | |
| const div = document.createElement('div'); | |
| if (it.type==='tour'){ | |
| const t = toursData.find(x=>x.id===it.id); | |
| if (t){ | |
| div.className='p-4 border rounded-lg flex flex-col sm:flex-row items-center justify-between bg-gray-50'; | |
| div.innerHTML=` | |
| <div class="flex-grow mb-4 sm:mb-0"> | |
| <p class="font-semibold text-gray-800">${escapeHTML(t.name)}</p> | |
| <p class="text-sm text-gray-600">${formatPrice(parseFloat(t.precio))} por persona</p> | |
| </div> | |
| <div class="flex items-center"> | |
| <span class="mr-2">Personas:</span> | |
| <input type="number" data-item-id="${it.id}" value="${it.quantity||1}" min="1" class="quantity-input w-16 p-2 rounded border text-center"> | |
| <button type="button" class="remove-item-button text-gray-400 hover:text-red-500 ml-4 transition-colors" data-item-id="${it.id}" data-item-type="tour"><i class="fas fa-trash-alt"></i></button> | |
| </div>`; | |
| } | |
| } else if (it.type==='coupon'){ | |
| const cp = promotionsData.coupons.find(x=>x.id===it.id); | |
| if (cp){ | |
| div.className='p-4 border-2 border-dashed border-green-500 rounded-lg bg-green-50 flex items-center justify-between'; | |
| div.innerHTML=` | |
| <div> | |
| <p class="font-bold text-green-800">Cupón: ${escapeHTML(cp.nombre)}</p> | |
| <p class="text-sm text-gray-600">Código: <span class="font-mono">${escapeHTML(cp.codigo)}</span></p> | |
| </div> | |
| <button type="button" class="remove-item-button text-gray-400 hover:text-red-500 transition-colors" data-item-id="${it.id}" data-item-type="coupon"><i class="fas fa-trash-alt"></i></button>`; | |
| } | |
| } | |
| c.appendChild(div); | |
| }); | |
| updateAllPricesUI(); | |
| }; | |
| /* ===================== FETCH ===================== */ | |
| const fetchTours = async () => { | |
| try { | |
| const res = await fetch('https://routicket.com/api/get_trip/dataSite.php?user_id=1419'); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| let raw = await res.text(); | |
| const data = JSON.parse(raw.replace(/[\u0000-\u001F\u007F-\u009F]/g,'')); | |
| toursData = data.map(t => ({ | |
| id: parseInt(t.id), | |
| name: normalizeText(t.title), | |
| description: normalizeText(t.description), | |
| image: t.thumbnailUrl, | |
| precio: t.precio | |
| })); | |
| document.getElementById('tours-loading-message').style.display = 'none'; | |
| displayTours(toursData); | |
| // Slider desde API (tours) | |
| buildSliderFromTours(toursData); | |
| } catch (e) { | |
| console.error(e); | |
| document.getElementById('tours-loading-message').textContent = 'Error al cargar tours.'; | |
| const wrap = document.querySelector('.slider-wrap'); | |
| if (wrap) wrap.style.display='none'; | |
| } | |
| }; | |
| const fetchPromotions = async () => { | |
| const grid = document.getElementById('promotions-grid'); | |
| try { | |
| const res = await fetch('https://routicket.com/viajes/admin/cupones/conexion.php?user_id=1419'); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| let raw = await res.text(); | |
| const data = JSON.parse(raw.replace(/[\u0000-\u001F\u007F-\u009F]/g,'')); | |
| promotionsData = { | |
| coupons: (data.coupons||[]).map(c => ({ ...c, nombre: normalizeText(c.nombre), descripcion: normalizeText(c.descripcion) })), | |
| flashSales: data.flashSales || [] | |
| }; | |
| grid.innerHTML=''; | |
| if (promotionsData.coupons.length){ | |
| promotionsData.coupons.forEach(coupon=>{ | |
| const card = document.createElement('div'); | |
| card.className='bg-white rounded-lg shadow-lg overflow-hidden flex flex-col items-center p-4'; | |
| card.innerHTML = ` | |
| <img src="${coupon.imagen}" alt="${escapeHTML(coupon.nombre)}" class="w-full h-48 object-cover rounded-lg mb-4" onerror="this.src='https://placehold.co/800x400/e2e8f0/64748b?text=Promo';"> | |
| <h3 class="text-xl font-semibold text-gray-800 mb-2 text-center clamp clamp-2" title="${escapeHTML(coupon.nombre)}">${escapeHTML(coupon.nombre)}</h3> | |
| <p class="promo-desc text-sm text-center text-gray-600 mb-1 clamp clamp-2 clamp-fade"></p> | |
| <button type="button" class="promo-toggle read-toggle text-xs underline text-gray-700 mx-auto mb-2">Leer más</button> | |
| <p class="text-lg font-bold text-red-500 mb-2">${parseFloat(coupon.descuento)}% OFF</p> | |
| <button class="w-full px-4 py-2 rounded-full mt-2 transition-colors coupon-select-button" data-id="${coupon.id}" data-type="coupon"></button>`; | |
| grid.appendChild(card); | |
| const p = card.querySelector('.promo-desc'); | |
| const btn = card.querySelector('.promo-toggle'); | |
| p.textContent = toPreview(coupon.descripcion); | |
| requestAnimationFrame(()=>{ | |
| const ov = p.scrollHeight > p.clientHeight + 1; | |
| if (!ov){ btn.style.display='none'; p.classList.remove('clamp-fade'); } | |
| }); | |
| }); | |
| } else { | |
| grid.innerHTML = '<p class="col-span-full text-center text-gray-500">No hay promociones disponibles en este momento.</p>'; | |
| } | |
| updateButtonStates(); | |
| } catch (e) { | |
| console.error(e); | |
| grid.innerHTML = `<p class="col-span-full text-center text-red-500">Error al cargar las promociones. Inténtalo de nuevo más tarde.</p>`; | |
| } | |
| }; | |
| /* ===================== FORM/ACCIONES ===================== */ | |
| const updateFinalQuoteMessage = () => { | |
| const { subtotal, discountAmount, total } = calculatePrices(); | |
| let itemsText = selection.map(it => { | |
| if (it.type==='tour'){ | |
| const t = toursData.find(x=>x.id===it.id); | |
| return t ? `TOUR: ${t.name}\n - Cantidad: ${it.quantity}\n - Subtotal: ${formatPrice(parseFloat(t.precio) * it.quantity, 'MXN')}` : ''; | |
| } | |
| if (it.type==='coupon'){ | |
| const c = promotionsData.coupons.find(x=>x.id===it.id); | |
| return c ? `CUPÓN: ${c.nombre} (${c.codigo})` : ''; | |
| } | |
| }).join('\n\n'); | |
| document.getElementById('final-quote-message').value = | |
| `Hola, mi nombre es [NOMBRE], correo [EMAIL] y teléfono [TELEFONO]. | |
| Quisiera cotizar: | |
| ${itemsText} | |
| -------------------- | |
| SUBTOTAL: ${formatPrice(subtotal, 'MXN')} | |
| DESCUENTO: ${formatPrice(discountAmount, 'MXN')} | |
| TOTAL: ${formatPrice(total, 'MXN')} | |
| -------------------- | |
| Gracias.`; | |
| }; | |
| const sendQuote = (platform) => { | |
| const name = document.getElementById('name').value; | |
| const email = document.getElementById('email').value; | |
| if (!name || !email) { alert("Por favor, completa tu nombre y correo electrónico."); return; } | |
| const phone = document.getElementById('phone').value; | |
| let message = document.getElementById('final-quote-message').value | |
| .replace('[NOMBRE]', name).replace('[EMAIL]', email).replace('[TELEFONO]', phone); | |
| if (platform==='whatsapp'){ | |
| window.open(`https://wa.me/529841452833?text=${encodeURIComponent(message)}`, '_blank'); | |
| } else if (platform==='email'){ | |
| window.location.href = `mailto:[email protected]?subject=Cotización de Tours&body=${encodeURIComponent(message)}`; | |
| } else if (platform==='paypal'){ | |
| const { total } = calculatePrices(); | |
| if (total<=0){ alert("El total debe ser mayor a 0 para pagar."); return; } | |
| const totalInUSD = total / exchangeRate; | |
| window.open(`https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&[email protected]&item_name=Pago%20de%20Tours&amount=${totalInUSD.toFixed(2)}¤cy_code=USD`, '_blank'); | |
| } | |
| }; | |
| // WhatsApp directo para un tour (desde slider Detalles si lo necesitas) | |
| const openWhatsAppForTour = (tour) => { | |
| const text = `Hola, me interesa el tour: ${tour.name}\nPrecio: ${formatPrice(parseFloat(tour.precio), 'MXN')}\n¿Me pueden dar más información?`; | |
| window.open(`https://wa.me/529841452833?text=${encodeURIComponent(text)}`,'_blank'); | |
| }; | |
| /* ===================== EVENTOS ===================== */ | |
| const handleSearch = () => { | |
| const term = normalizeText(document.getElementById('tour-search-input').value).toLowerCase(); | |
| const filtered = toursData.filter(t => | |
| t.name.toLowerCase().includes(term) || (t.description||'').toLowerCase().includes(term) | |
| ); | |
| currentPage=1; displayTours(filtered); | |
| }; | |
| const addEventListeners = () => { | |
| // Slider navegación | |
| document.getElementById('nextBtn').addEventListener('click', ()=>{ nextSlide(); startAutoplay(); }); | |
| document.getElementById('prevBtn').addEventListener('click', ()=>{ prevSlide(); startAutoplay(); }); | |
| window.addEventListener('resize', applySliderTruncation); | |
| // Slider acciones (delegación) | |
| document.getElementById('sliderTrack').addEventListener('click', e => { | |
| const btnReserve = e.target.closest('.slider-reserve'); | |
| const btnDetails = e.target.closest('.slider-details'); | |
| const btnContact = e.target.closest('.slider-contact'); | |
| if (btnReserve){ | |
| const id = parseInt(btnReserve.dataset.id); | |
| const exists = selection.find(it => it.id===id && it.type==='tour'); | |
| if (!exists) selection.push({ id, type:'tour', quantity:1 }); | |
| updateSelectionCount(); saveSelectionToLocalStorage(); updateAllPricesUI(); | |
| // Abre el carrito | |
| renderQuoteBuilder(); openModal('quote-builder-modal'); | |
| return; | |
| } | |
| if (btnDetails){ | |
| const id = parseInt(btnDetails.dataset.id); | |
| const t = toursData.find(x=>x.id===id); | |
| if (t){ | |
| const body = document.getElementById('modal-body-content'); | |
| body.innerHTML=''; | |
| const h = document.createElement('h3'); h.className='text-2xl font-semibold'; h.textContent=t.name; | |
| const img = document.createElement('img'); img.src=t.image; img.alt=t.name; img.className='rounded-lg my-4 w-full'; img.onerror=function(){ this.src='https://placehold.co/800x400'; }; | |
| body.appendChild(h); body.appendChild(img); | |
| renderRichTextInto(body, t.description); | |
| const price = document.createElement('p'); price.className='font-bold text-xl mt-4'; price.textContent=`Precio: ${formatPrice(parseFloat(t.precio))}`; | |
| body.appendChild(price); | |
| // CTA dentro del modal | |
| const ctas = document.createElement('div'); | |
| ctas.className = 'mt-4 flex flex-wrap gap-2'; | |
| ctas.innerHTML = ` | |
| <button class="btn btn-primary" id="modal-addtocart"><i class="fas fa-cart-plus"></i> Añadir al carrito</button> | |
| <button class="btn btn-outline" id="modal-wa"><i class="fab fa-whatsapp"></i> WhatsApp</button> | |
| `; | |
| body.appendChild(ctas); | |
| document.getElementById('modal-addtocart').onclick = () => { | |
| const exists = selection.find(it => it.id===id && it.type==='tour'); | |
| if (!exists) selection.push({ id, type:'tour', quantity:1 }); | |
| updateSelectionCount(); saveSelectionToLocalStorage(); updateAllPricesUI(); | |
| renderQuoteBuilder(); openModal('quote-builder-modal'); | |
| }; | |
| document.getElementById('modal-wa').onclick = () => openWhatsAppForTour(t); | |
| openModal('item-details-modal'); | |
| } | |
| return; | |
| } | |
| if (btnContact){ | |
| document.getElementById('contacto').scrollIntoView({ behavior:'smooth' }); | |
| return; | |
| } | |
| }); | |
| // Search | |
| document.getElementById('tour-search-input').addEventListener('keyup', handleSearch); | |
| // Tours: leer más/menos, ver detalles, añadir | |
| document.getElementById('tours-grid').addEventListener('click', e => { | |
| const toggleRead = e.target.closest('.read-toggle'); | |
| if (toggleRead){ | |
| const card = toggleRead.closest('.tour-card'); | |
| const desc = card.querySelector('.tour-desc'); | |
| const expanded = desc.dataset.expanded === 'true'; | |
| if (expanded){ | |
| desc.classList.add('clamp','clamp-3','clamp-fade'); | |
| desc.classList.remove('no-fade'); | |
| desc.dataset.expanded='false'; | |
| toggleRead.textContent='Leer más'; | |
| } else { | |
| desc.classList.remove('clamp','clamp-3','clamp-fade'); | |
| desc.classList.add('no-fade'); | |
| desc.dataset.expanded='true'; | |
| toggleRead.textContent='Leer menos'; | |
| } | |
| return; | |
| } | |
| const btn = e.target.closest('.tour-select-button, .view-details-button'); | |
| if (!btn) return; | |
| const { id, type } = btn.dataset; | |
| const itemId = parseInt(id); | |
| // Si el botón es 'Ver completo' (.view-details-button), redirige. | |
| if (btn.classList.contains('view-details-button')){ | |
| // Redirige a la nueva página con el ID del tour | |
| window.location.href = `ver-tour.php?id=${itemId}`; | |
| } else { | |
| // Lógica original para seleccionar/deseleccionar tours (.tour-select-button) | |
| const idx = selection.findIndex(it => it.id===itemId && it.type===type); | |
| if (idx>-1) selection.splice(idx,1); else selection.push({ id:itemId, type, quantity:1 }); | |
| updateSelectionCount(); saveSelectionToLocalStorage(); updateButtonStates(); updateAllPricesUI(); | |
| } | |
| }); | |
| // Promos: leer más/menos + seleccionar cupón | |
| document.getElementById('promotions-grid').addEventListener('click', e => { | |
| const promoToggle = e.target.closest('.promo-toggle'); | |
| if (promoToggle){ | |
| const card = promoToggle.closest('div'); | |
| const desc = card.querySelector('.promo-desc'); | |
| const expanded = desc.dataset.expanded === 'true'; | |
| if (expanded){ | |
| desc.classList.add('clamp','clamp-2','clamp-fade'); | |
| desc.classList.remove('no-fade'); | |
| desc.dataset.expanded='false'; | |
| promoToggle.textContent='Leer más'; | |
| } else { | |
| desc.classList.remove('clamp','clamp-2','clamp-fade'); | |
| desc.classList.add('no-fade'); | |
| desc.dataset.expanded='true'; | |
| promoToggle.textContent='Leer menos'; | |
| } | |
| return; | |
| } | |
| const btn = e.target.closest('.coupon-select-button'); | |
| if (!btn) return; | |
| const { id, type } = btn.dataset; const itemId = parseInt(id); | |
| const existing = selection.findIndex(it=>it.type==='coupon'); | |
| if (existing>-1) selection.splice(existing,1); | |
| const already = selection.findIndex(it => it.id===itemId && it.type===type); | |
| if (already===-1) selection.push({ id:itemId, type, quantity:1 }); | |
| updateSelectionCount(); saveSelectionToLocalStorage(); updateButtonStates(); updateAllPricesUI(); | |
| }); | |
| // Carrito | |
| document.getElementById('quote-builder-modal').addEventListener('click', e => { | |
| if (e.target.closest('.remove-item-button')){ | |
| const btn = e.target.closest('.remove-item-button'); | |
| const { itemId, itemType } = btn.dataset; | |
| selection = selection.filter(it => !(it.id===parseInt(itemId) && it.type===itemType)); | |
| renderQuoteBuilder(); updateSelectionCount(); saveSelectionToLocalStorage(); updateButtonStates(); | |
| } | |
| }); | |
| document.getElementById('quote-builder-modal').addEventListener('input', e => { | |
| if (e.target.classList.contains('quantity-input')){ | |
| const { itemId } = e.target.dataset; | |
| const t = selection.find(it => it.id===parseInt(itemId) && it.type==='tour'); | |
| if (t) t.quantity = parseInt(e.target.value) || 1; | |
| updateAllPricesUI(); saveSelectionToLocalStorage(); | |
| } | |
| }); | |
| // Botones varios | |
| document.getElementById('summary-button').addEventListener('click', ()=>{ renderQuoteBuilder(); openModal('quote-builder-modal'); }); | |
| document.getElementById('edit-quote-button').addEventListener('click', ()=>{ renderQuoteBuilder(); openModal('quote-builder-modal'); }); | |
| document.getElementById('clear-all-button').addEventListener('click', ()=>{ selection=[]; renderQuoteBuilder(); updateSelectionCount(); saveSelectionToLocalStorage(); updateButtonStates(); }); | |
| document.getElementById('quote-form').addEventListener('submit', e => { e.preventDefault(); closeModal('quote-builder-modal'); document.getElementById('contacto').scrollIntoView({ behavior:'smooth' }); }); | |
| document.getElementById('whatsapp-button').addEventListener('click', ()=>sendQuote('whatsapp')); | |
| document.getElementById('email-button').addEventListener('click', ()=>sendQuote('email')); | |
| document.getElementById('paypal-button').addEventListener('click', ()=>sendQuote('paypal')); | |
| // Moneda (desktop: actual, móvil: cambiar a) | |
| const toggleCurrency = () => { | |
| currentCurrency = (currentCurrency==='MXN') ? 'USD' : 'MXN'; | |
| document.getElementById('currency-label').textContent = currentCurrency; // Desktop actual | |
| document.getElementById('currency-label-mobile').textContent = (currentCurrency==='MXN') ? 'USD' : 'MXN'; // Móvil cambio | |
| updateTourPricesOnScreen(); updateAllPricesUI(); | |
| }; | |
| document.getElementById('currency-toggle-btn').addEventListener('click', toggleCurrency); | |
| document.getElementById('currency-toggle-btn-mobile').addEventListener('click', toggleCurrency); | |
| // Menú / Idioma | |
| document.getElementById('menu-button').addEventListener('click', ()=>document.getElementById('mobile-menu').classList.toggle('hidden')); | |
| document.getElementById('open-language-modal-btn').addEventListener('click', ()=>openModal('language-modal')); | |
| document.getElementById('open-language-modal-btn-mobile').addEventListener('click', ()=>openModal('language-modal')); | |
| }; | |
| /* ===================== MODALS ===================== */ | |
| const openModal = id => document.getElementById(id).style.display='block'; | |
| const closeModal = id => document.getElementById(id).style.display='none'; | |
| window.onclick = e => { if (e.target.classList.contains('modal')) e.target.style.display='none'; }; | |
| /* ===================== INIT ===================== */ | |
| document.addEventListener('DOMContentLoaded', async () => { | |
| // Estado previo | |
| loadSelectionFromLocalStorage(); | |
| // Data | |
| await fetchTours(); // -> también construye el slider desde API | |
| await fetchPromotions(); | |
| // UI inicial | |
| updateAllPricesUI(); | |
| updateSelectionCount(); | |
| addEventListeners(); | |
| // Etiquetas iniciales de moneda (MXN por defecto) | |
| document.getElementById('currency-label').textContent = currentCurrency; | |
| document.getElementById('currency-label-mobile').textContent = (currentCurrency==='MXN') ? 'USD' : 'MXN'; | |
| }); | |
| </script> | |
| <footer class="bg-gray-900 text-white px-5 py-10"> | |
| <div class="max-w-6xl mx-auto flex flex-wrap gap-6 justify-between text-left"> | |
| <div class="flex-1 min-w-[260px]"> | |
| <h4 class="text-xl mb-3 border-b-2 border-green-400 inline-block pb-1">Contacto</h4> | |
| <p class="mt-2"><a href="mailto:[email protected]" class="hover:underline"><i class="fas fa-envelope mr-2"></i>[email protected]</a></p> | |
| <div class="mt-5 space-y-3"> | |
| <div> | |
| <p class="mb-2">Silvia Díaz:</p> | |
| <a href="tel:+529841452833" class="bg-gray-700 px-4 py-2 rounded border border-gray-600 mr-2 inline-block font-semibold"><i class="fas fa-phone-alt"></i> Llamar</a> | |
| <a href="https://wa.me/529841452833" class="bg-emerald-500 px-4 py-2 rounded inline-block font-semibold"><i class="fab fa-whatsapp"></i> WhatsApp</a> | |
| </div> | |
| <div> | |
| <p class="mb-2">Daniel Jackson:</p> | |
| <a href="tel:+529842362322" class="bg-gray-700 px-4 py-2 rounded border border-gray-600 mr-2 inline-block font-semibold"><i class="fas fa-phone-alt"></i> Llamar</a> | |
| <a href="https://wa.me/529842362322" class="bg-emerald-500 px-4 py-2 rounded inline-block font-semibold"><i class="fab fa-whatsapp"></i> WhatsApp</a> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex-1 min-w-[180px]"> | |
| <h4 class="text-xl mb-3 border-b-2 border-green-400 inline-block pb-1">Síguenos</h4> | |
| <div class="flex items-center gap-4 text-2xl mt-3"> | |
| <a href="https://www.facebook.com/DaNaToursRivieraMaya" aria-label="Facebook" class="hover:text-white/80"><i class="fab fa-facebook-f"></i></a> | |
| <a href="https://instagram.com/dana_toursoficial" aria-label="Instagram" class="hover:text-white/80"><i class="fab fa-instagram"></i></a> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="border-t border-white/20 mt-6 pt-4 text-center text-sm text-white/70"> | |
| © 2025 Danatours. Todos los derechos reservados. | <a href="/admin" class="text-green-400 hover:underline">Admin</a> | |
| </div> | |
| </footer> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment