Semana de reserva anticipada
Aprovecha un 10% off en sesiones seleccionadas. Termina en
Servicios
Usamos cookies para mejorar tu experiencia y analizar el uso. Puedes aceptar todas o solo las necesarias.
'),
fetch('footer.html').then(r=>r.text()).catch(()=>'')
]);
document.querySelector('header').innerHTML = h;
document.querySelector('footer').innerHTML = f;
}
injectLayout();
const state = {
raw: [],
filtered: [],
currency: localStorage.getItem('currency') || 'CLP',
perPage: 9,
page: 1,
compare: new Set(JSON.parse(localStorage.getItem('compare') || '[]')),
favorites: JSON.parse(localStorage.getItem('favorites') || '[]'),
cart: JSON.parse(localStorage.getItem('cartItems') || '[]'),
promoDeadline: (()=> {
const saved = localStorage.getItem('promoDeadline');
if (saved) return +saved;
const d = new Date(); d.setDate(d.getDate()+5); d.setHours(23,59,59,999);
localStorage.setItem('promoDeadline', d.getTime());
return d.getTime();
})()
};
const els = {
grid: document.getElementById('grid'),
pager: document.getElementById('pager'),
search: document.getElementById('search'),
category: document.getElementById('category'),
priceMax: document.getElementById('priceMax'),
priceMaxLabel: document.getElementById('priceMaxLabel'),
resetBtn: document.getElementById('resetBtn'),
compareBtn: document.getElementById('compareBtn'),
currencyBtns: [...document.querySelectorAll('[data-currency]')],
detailsModal: document.getElementById('detailsModal'),
detailsTitle: document.getElementById('detailsTitle'),
detailsImg: document.getElementById('detailsImg'),
detailsDesc: document.getElementById('detailsDesc'),
detailsPrice: document.getElementById('detailsPrice'),
detailsFav: document.getElementById('detailsFav'),
detailsCart: document.getElementById('detailsCart'),
compareModal: document.getElementById('compareModal'),
compareBody: document.getElementById('compareBody'),
itemListSchema: document.getElementById('itemListSchema'),
themeToggle: document.getElementById('themeToggle'),
cookieBanner: document.getElementById('cookieBanner'),
cookieAccept: document.getElementById('cookieAccept'),
cookieReject: document.getElementById('cookieReject'),
dealCountdown: document.getElementById('dealCountdown'),
reserveModal: document.getElementById('reserveModal'),
reserveForm: document.getElementById('reserveForm'),
reserveService: document.getElementById('reserveService'),
openReserve: document.getElementById('openReserve'),
compareBtnEnabled: false
};
function money(v, cur){ return cur==='USD' ? `$${Number(v).toFixed(0)} USD` : `$${Number(v).toLocaleString('es-CL')} CLP`; }
function syncFavorites(){ localStorage.setItem('favorites', JSON.stringify(state.favorites)); }
function syncCart(){ localStorage.setItem('cartItems', JSON.stringify(state.cart)); }
function syncCompare(){ localStorage.setItem('compare', JSON.stringify([...state.compare])); }
function addToCart(id){
const item = state.cart.find(i=>i.id===id);
if(item){ item.qty += 1; } else { state.cart.push({id, qty:1}); }
syncCart();
}
function toggleFavorite(id){
const idx = state.favorites.indexOf(id);
if(idx>-1) state.favorites.splice(idx,1); else state.favorites.push(id);
syncFavorites();
render();
}
function applyFilters(){
const q = els.search.value.trim().toLowerCase();
const c = els.category.value;
const p = parseInt(els.priceMax.value || '0', 10);
state.filtered = state.raw.filter(it=>{
const matchQ = !q || it.title.toLowerCase().includes(q) || it.tags.join(' ').toLowerCase().includes(q) || it.category.toLowerCase().includes(q);
const matchC = !c || it.category===c;
const price = state.currency==='USD' ? it.priceUSD : it.priceCLP;
const matchP = !p || price <= p;
return matchQ && matchC && matchP;
});
state.page = 1;
render();
updateCompareButton();
updatePriceLabel();
updateURLHashParams();
}
function updateURLHashParams(){
const params = new URLSearchParams();
if(els.search.value) params.set('q', els.search.value);
if(els.category.value) params.set('cat', els.category.value);
if(els.priceMax.value) params.set('max', els.priceMax.value);
params.set('cur', state.currency);
history.replaceState(null, '', params.toString()? ('#'+params.toString()): location.pathname);
}
function hydrateFromHash(){
const hash = location.hash.startsWith('#') ? location.hash.slice(1):'';
const params = new URLSearchParams(hash);
const q = params.get('q'); const cat = params.get('cat'); const max = params.get('max'); const cur = params.get('cur');
if(q) els.search.value = q;
if(cat) els.category.value = cat;
if(max) els.priceMax.value = max;
if(cur && (cur==='CLP'||cur==='USD')) state.currency = cur;
}
function renderCategories(){
const cats = Array.from(new Set(state.raw.map(i=>i.category))).sort();
els.category.innerHTML = '' + cats.map(c=>``).join('');
}
function cardTemplate(it){
const fav = state.favorites.includes(it.id);
const price = state.currency==='USD' ? it.priceUSD : it.priceCLP;
const checked = state.compare.has(it.id) ? 'checked' : '';
return `
${it.title}
${it.shortDescription}
${money(price, state.currency)}
★ ${it.rating} (${it.reviews})
`;
}
function renderSkeleton(count=6){
els.grid.innerHTML = Array.from({length: count}).map(()=>`
`).join('');
}
function render(){
const start = (state.page-1)*state.perPage;
const items = state.filtered.slice(start, start+state.perPage);
els.grid.innerHTML = items.map(cardTemplate).join('') || `No hay resultados. Ajusta los filtros.
`;
const pages = Math.ceil(state.filtered.length / state.perPage) || 1;
els.pager.innerHTML = Array.from({length: pages}, (_,i)=>i+1).map(n=>
``
).join('');
// ItemList JSON-LD
const list = state.filtered.slice(0, state.perPage).map((it, idx)=>({
"@type":"Product",
"name": it.title,
"image": it.images[0],
"description": it.shortDescription,
"sku": it.sku,
"position": idx+1,
"offers": {
"@type":"Offer",
"priceCurrency": state.currency,
"price": (state.currency==='USD'?it.priceUSD:it.priceCLP).toFixed(0),
"availability": it.spotsAvailable>0 ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
"url": `https://cashgrid.pro/catalog.html#${it.slug}`
}
}));
els.itemListSchema.textContent = JSON.stringify({
"@context":"https://schema.org",
"@type":"ItemList",
"itemListElement": list
});
updateCompareButton();
highlightCurrencyBtn();
}
function updateCompareButton(){
const size = state.compare.size;
els.compareBtn.textContent = size>0 ? `Comparar (${size})` : 'Comparar';
els.compareBtn.disabled = size===0;
syncCompare();
}
function attachEvents(){
els.grid.addEventListener('click', (e)=>{
const favBtn = e.target.closest('[data-fav]');
const detBtn = e.target.closest('[data-details]');
const cartBtn = e.target.closest('[data-cart]');
if(favBtn){ toggleFavorite(+favBtn.dataset.fav); }
if(detBtn){ openDetails(+detBtn.dataset.details); }
if(cartBtn){ addToCart(+cartBtn.dataset.cart); }
});
els.grid.addEventListener('change', (e)=>{
const c = e.target.closest('[data-compare]');
if(c){ const id = +c.dataset.compare; if(e.target.checked) state.compare.add(id); else state.compare.delete(id); updateCompareButton(); }
});
els.pager.addEventListener('click', (e)=>{
const p = e.target.closest('[data-page]'); if(p){ state.page = +p.dataset.page; render(); window.scrollTo({top:0, behavior:'smooth'}); }
});
[els.search, els.priceMax].forEach(el=>el.addEventListener('input', debounce(applyFilters, 200)));
els.category.addEventListener('change', applyFilters);
els.resetBtn.addEventListener('click', ()=>{
els.search.value=''; els.category.value=''; els.priceMax.value=''; state.compare.clear(); applyFilters();
});
els.compareBtn.addEventListener('click', openCompare);
els.currencyBtns.forEach(b=>b.addEventListener('click', ()=>{
els.currencyBtns.forEach(x=>{x.classList.remove('bg-gray-900','text-white'); x.setAttribute('aria-pressed','false');});
b.classList.add('bg-gray-900','text-white');
b.setAttribute('aria-pressed','true');
state.currency = b.dataset.currency;
localStorage.setItem('currency', state.currency);
updatePriceLabel();
applyFilters();
}));
document.querySelectorAll('dialog [data-close]').forEach(btn=>{
btn.addEventListener('click', ()=> btn.closest('dialog').close());
});
els.themeToggle.addEventListener('click', ()=>{
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', isDark?'dark':'light');
els.themeToggle.textContent = isDark ? '☀' : '☾';
});
els.cookieAccept.addEventListener('click', ()=> setCookieConsent('all'));
els.cookieReject.addEventListener('click', ()=> setCookieConsent('necessary'));
els.openReserve.addEventListener('click', ()=> {
populateReserveServices();
els.reserveModal.showModal();
});
els.reserveForm.addEventListener('submit', onReserveSubmit);
window.addEventListener('hashchange', onHashChangeOpenDetails);
}
function highlightCurrencyBtn(){
els.currencyBtns.forEach(b=>{
const active = b.dataset.currency===state.currency;
b.classList.toggle('bg-gray-900', active);
b.classList.toggle('text-white', active);
b.setAttribute('aria-pressed', active?'true':'false');
});
}
function updatePriceLabel(){
els.priceMaxLabel.textContent = `Precio máx. (${state.currency})`;
els.priceMax.placeholder = state.currency==='USD' ? 'Ej. 500' : 'Ej. 500000';
}
function openDetails(id){
const it = state.raw.find(x=>x.id===id);
if(!it) return;
els.detailsTitle.textContent = it.title;
els.detailsImg.src = it.images[1] || it.images[0];
els.detailsImg.alt = it.title;
els.detailsDesc.textContent = it.description;
const price = state.currency==='USD'?it.priceUSD:it.priceCLP;
els.detailsPrice.textContent = money(price, state.currency);
els.detailsFav.onclick = ()=>{ toggleFavorite(id); els.detailsModal.close(); };
els.detailsCart.onclick = ()=>{ addToCart(id); els.detailsModal.close(); };
els.detailsModal.showModal();
location.hash = it.slug;
}
function onHashChangeOpenDetails(){
const slug = location.hash.replace('#','');
if(!slug) return;
const it = state.raw.find(x=>x.slug===slug);
if(it) openDetails(it.id);
}
function openCompare(){
if(state.compare.size===0){ return; }
const items = [...state.compare].slice(0,2).map(id=>state.raw.find(x=>x.id===id)).filter(Boolean);
els.compareBody.innerHTML = items.map(it=>`
${it.title}
${it.shortDescription}
- Categoría: ${it.category}
- Etiquetas: ${it.tags.join(', ')}
- Valoración: ★ ${it.rating} (${it.reviews})
- Cupos disponibles: ${it.spotsAvailable}
- Precio: ${money(state.currency==='USD'?it.priceUSD:it.priceCLP, state.currency)}
`).join('');
els.compareModal.showModal();
}
function debounce(fn, ms){ let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a), ms); }; }
function startCountdown(){
function tick(){
const now = Date.now();
let diff = state.promoDeadline - now;
if(diff < 0){ diff = 0; }
const d = Math.floor(diff / (1000*60*60*24));
const h = Math.floor((diff % (1000*60*60*24))/(1000*60*60));
const m = Math.floor((diff % (1000*60*60))/(1000*60));
const s = Math.floor((diff % (1000*60))/1000);
if(els.dealCountdown){
els.dealCountdown.textContent = `${String(d).padStart(2,'0')}d:${String(h).padStart(2,'0')}h:${String(m).padStart(2,'0')}m:${String(s).padStart(2,'0')}s`;
}
}
tick();
setInterval(tick, 1000);
}
function setCookieConsent(level){
const payload = {level, ts: Date.now()};
localStorage.setItem('cookieConsent', JSON.stringify(payload));
els.cookieBanner.classList.add('hidden');
}
function checkCookieBanner(){
const cc = localStorage.getItem('cookieConsent');
if(!cc){
els.cookieBanner.classList.remove('hidden');
}else{
els.cookieBanner.classList.add('hidden');
}
}
function populateReserveServices(){
els.reserveService.innerHTML = '' + state.raw.map(it=>``).join('');
}
function onReserveSubmit(e){
e.preventDefault();
const fd = new FormData(els.reserveForm);
const name = (fd.get('name')||'').toString().trim();
const email = (fd.get('email')||'').toString().trim();
const phone = (fd.get('phone')||'').toString().trim();
const service = (fd.get('service')||'').toString().trim();
const date = (fd.get('date')||'').toString().trim();
const terms = fd.get('terms') ? true : false;
let valid = true;
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const phoneOk = /^\+56\s?9\s?\d{4}\s?\d{4}$/.test(phone) || /^\+569\d{8}$/.test(phone);
const nameOk = name.length>=3;
const serviceOk = !!service;
const dateOk = !!date && new Date(date) >= new Date(new Date().toDateString());
const termsOk = terms;
const setErr = (key, show)=>{ const el = els.reserveForm.querySelector(`[data-err="${key}"]`); if(el){ el.classList.toggle('hidden', !show); } };
setErr('name', !nameOk);
setErr('email', !emailOk);
setErr('phone', !phoneOk);
setErr('service', !serviceOk);
setErr('date', !dateOk);
if(!termsOk){ /* could show message */ }
valid = emailOk && phoneOk && nameOk && serviceOk && dateOk && termsOk;
if(!valid) return;
const reservations = JSON.parse(localStorage.getItem('reservations') || '[]');
reservations.push({name,email,phone,service: +service, date, ts: Date.now()});
localStorage.setItem('reservations', JSON.stringify(reservations));
const ok = document.getElementById('reserveSuccess');
ok.classList.remove('hidden');
setTimeout(()=>{ ok.classList.add('hidden'); els.reserveModal.close(); els.reserveForm.reset(); }, 1200);
}
function focusSlugIfPresent(){
const slug = location.hash.replace('#','');
if(!slug) return;
const el = document.getElementById(slug);
if(el){ el.scrollIntoView({behavior:'smooth', block:'center'}); el.classList.add('ring-2','ring-indigo-500'); setTimeout(()=>el.classList.remove('ring-2','ring-indigo-500'), 1500); }
}
async function init(){
renderSkeleton(9);
try{
const data = await fetch('catalog.json').then(r=>r.json());
state.raw = data;
state.filtered = data.slice();
renderCategories();
hydrateFromHash();
attachEvents();
applyFilters();
highlightCurrencyBtn();
startCountdown();
checkCookieBanner();
populateReserveServices();
setTimeout(focusSlugIfPresent, 300);
}catch(e){
els.grid.innerHTML = `No se pudo cargar el catálogo. Intenta nuevamente.
`;
}
}
init();