Skip to content

Instantly share code, notes, and snippets.

@iDavidMorales
Created November 3, 2025 19:16
Show Gist options
  • Select an option

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

Select an option

Save iDavidMorales/cedab11c0826eb85630f00346a4b00dd to your computer and use it in GitHub Desktop.
<!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')">&times;</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')">&times;</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')">&times;</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
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)}&currency_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