Winter Deal Save up to 15% on select tours • Ends in 00:00:00
Toronto Walks
Favourites 0 Cart 0
Catalog About FAQ Contact
Choose theme
Log in
Sign up

Explore Toronto Tours

Search, filter, sort and compare the best walking tours in Toronto. Save your favourites and plan your perfect day.

0 tours
Tour cover

Tour Title

Category Language Provider Pickup ★★★★★ 4.8 2.5h $89.00

Features

    Accessibility

      Availability

        Plan this tour
        No results

        We couldn't find tours matching your filters

        Try adjusting your filters or clearing them to see more options.

        `; els.cards.appendChild(li); $('#clearNoResults')?.addEventListener('click', ()=>{ inputs.reset.click(); }); return; } for(const t of data){ const li=document.createElement('li'); li.className='v9m3x'; const favActive = state.favs.has(t.id) ? 'r1n6d' : ''; li.innerHTML = `
        Toronto Walks
        Favourites 0 Cart 0
        Catalog About FAQ Contact
        Choose theme
        Log in
        Sign up
        ${t.title} cover

        ${t.title}

        ${fmtPrice.format(t.price)} • ${t.durationHours}h Rating: ${t.rating.toFixed(1)}
        ${t.language} ${t.provider} ${t.pickup?'Pickup':'Meetup'}

        ${t.description.slice(0,110)}${t.description.length>110?'…':''}

        Add to plan
        `; els.cards.appendChild(li); } $$('#cards [data-details]').forEach(btn=>{ btn.addEventListener('click',()=>openModal(Number(btn.getAttribute('data-details')))); }); $$('#cards .f8n6r').forEach(btn=>{ btn.addEventListener('click',()=>{ const id=Number(btn.getAttribute('data-id')); const isFav=state.favs.has(id); if(isFav){ state.favs.delete(id); } else { state.favs.add(id); } localStorage.setItem('twt_favs', JSON.stringify(Array.from(state.favs))); btn.classList.toggle('r1n6d', state.favs.has(id)); updateFavCount(); if(state.favOnly){ applyFilters(); } }); }); } function renderPagination(pages){ els.pagination.innerHTML=''; if(pages<=1) return; const prev=document.createElement('button'); prev.textContent='Prev'; prev.disabled = state.page===1; prev.addEventListener('click',()=>{ if(state.page>1){ state.page--; render(); window.scrollTo({top:0,behavior:'smooth'});} }); els.pagination.appendChild(prev); const maxButtons = 7; let start = Math.max(1, state.page - Math.floor(maxButtons/2)); let end = Math.min(pages, start + maxButtons - 1); if(end - start + 1 < maxButtons){ start=Math.max(1,end-maxButtons+1); } for(let p=start;p<=end;p++){ const b=document.createElement('button'); b.textContent=String(p); b.setAttribute('data-active', p===state.page); b.addEventListener('click',()=>{ state.page=p; render(); window.scrollTo({top:0,behavior:'smooth'}); }); els.pagination.appendChild(b); } const next=document.createElement('button'); next.textContent='Next'; next.disabled = state.page===pages; next.addEventListener('click',()=>{ if(state.pagex.id===id) || state.filtered.find(x=>x.id===id); if(!t) return; els.modalImg.src = imgForCategory(t.category); els.modalTitle.textContent = t.title; els.modalCategory.textContent = t.category; els.modalLanguage.textContent = t.language; els.modalProvider.textContent = t.provider; els.modalPickup.textContent = t.pickup ? 'Pickup available' : 'Meetup only'; els.modalRating.textContent = `★ ${t.rating.toFixed(1)}`; els.modalDuration.textContent = `${t.durationHours}h`; els.modalPrice.textContent = fmtPrice.format(t.price); els.modalDesc.textContent = t.description; els.modalFeatures.innerHTML = (t.features||[]).map(f=>`
      • ${f}
      • `).join('') || '
      • No features listed
      • '; els.modalAccess.innerHTML = (t.accessibility||[]).map(f=>`
      • ${f}
      • `).join('') || '
      • No details
      • '; els.modalDates.innerHTML = (t.availability||[]).slice(0,8).map(d=>{ const date = new Date(d); const label = date.toLocaleString('en-CA',{dateStyle:'medium'}); return `
      • ${label}
      • `; }).join('') || '
      • No upcoming dates
      • '; els.modalFav.textContent = state.favs.has(t.id)?'Remove from favourites':'Add to favourites'; els.modalFav.onclick = ()=>{ if(state.favs.has(t.id)){ state.favs.delete(t.id); } else { state.favs.add(t.id); } localStorage.setItem('twt_favs', JSON.stringify(Array.from(state.favs))); els.modalFav.textContent = state.favs.has(t.id)?'Remove from favourites':'Add to favourites'; updateFavCount(); render(); }; els.modalShare.onclick = async ()=>{ const url = new URL(location.href); url.searchParams.set('tour', t.slug || String(t.id)); try{ await navigator.clipboard.writeText(url.toString()); els.modalShare.textContent='Link copied!'; setTimeout(()=>els.modalShare.textContent='Copy share link',1500); }catch{ /* ignore */ } }; els.modalStart.href = '#!'; els.modal.dataset.id = t.id; if(typeof els.modal.showModal === 'function'){ els.modal.showModal(); } else{ els.modal.setAttribute('open',''); } } els.modal.addEventListener('click', (e)=>{ const rect=els.modal.getBoundingClientRect(); if(e.clientYrect.bottom || e.clientXrect.right){ els.modal.close?.(); els.modal.removeAttribute('open'); } }); function populateDynamicOptions(){ const cats = Array.from(new Set(state.all.map(t=>t.category))).sort(); const langs = Array.from(new Set(state.all.map(t=>t.language))).sort(); const provs = Array.from(new Set(state.all.map(t=>t.provider))).sort(); inputs.category.innerHTML = ''+cats.map(c=>``).join(''); inputs.language.innerHTML = ''+langs.map(l=>``).join(''); inputs.provider.innerHTML = ''+provs.map(p=>``).join(''); } function restoreFromQuery(){ const u=new URL(location.href); const sp=u.searchParams; if(sp.get('q')) inputs.q.value=sp.get('q'); if(sp.get('category')) inputs.category.value=sp.get('category'); if(sp.get('language')) inputs.language.value=sp.get('language'); if(sp.get('date')) inputs.date.value=sp.get('date'); if(sp.get('priceMin')) inputs.priceMin.value=sp.get('priceMin'); if(sp.get('priceMax')) inputs.priceMax.value=sp.get('priceMax'); if(sp.get('durationMin')) inputs.durationMin.value=sp.get('durationMin'); if(sp.get('durationMax')) inputs.durationMax.value=sp.get('durationMax'); if(sp.get('ratingMin')) inputs.ratingMin.value=sp.get('ratingMin'); if(sp.get('provider')) inputs.provider.value=sp.get('provider'); if(sp.get('groupSize')) inputs.groupSize.value=sp.get('groupSize'); if(sp.get('pickup')) inputs.pickup.checked=(sp.get('pickup')==='true'); if(sp.get('accessibility')) inputs.accessibility.value=sp.get('accessibility'); if(sp.get('tags')) inputs.tags.value=sp.get('tags'); if(sp.get('favOnly')){ state.favOnly = sp.get('favOnly')==='true'; inputs.favOnly.setAttribute('aria-pressed',state.favOnly); if(state.favOnly) inputs.favOnly.classList.add('m7v5e'); } if(sp.get('sortBy')){ state.sortBy=sp.get('sortBy'); els.sortBy.value=state.sortBy; } if(sp.get('pageSize')){ state.pageSize=Number(sp.get('pageSize')); els.pageSize.value=String(state.pageSize); } const shareTour=sp.get('tour'); if(shareTour){ const t = state.all.find(x=>x.slug===shareTour || String(x.id)===shareTour); if(t) setTimeout(()=>openModal(t.id), 300); } } function syncQuery(){ const f=getFilters(); const u=new URL(location.href); Object.entries({ q:f.q, category:f.category, language:f.language, date:f.date, priceMin:f.priceMin, priceMax:f.priceMax, durationMin:f.durationMin, durationMax:f.durationMax, ratingMin:f.ratingMin, provider:f.provider, groupSize:f.groupSize, pickup:f.pickup||undefined, accessibility:f.accessibility, tags:f.tags.join(','), favOnly:f.favOnly||undefined, sortBy:state.sortBy, pageSize:state.pageSize }).forEach(([k,v])=>{ if(v===undefined || v===null || v===''){ u.searchParams.delete(k); } else u.searchParams.set(k, String(v)); }); history.replaceState(null,'',u.toString()); } // Event bindings form.addEventListener('submit',e=>{ e.preventDefault(); state.page=1; syncQuery(); applyFilters(); }); inputs.reset.addEventListener('click',()=>{ form.reset(); inputs.tags.value=''; state.favOnly=false; inputs.favOnly.setAttribute('aria-pressed','false'); inputs.favOnly.classList.remove('m7v5e'); state.sortBy='relevance'; els.sortBy.value='relevance'; state.pageSize=12; els.pageSize.value='12'; syncQuery(); applyFilters(); }); inputs.favOnly.addEventListener('click',()=>{ state.favOnly=!state.favOnly; inputs.favOnly.setAttribute('aria-pressed', String(state.favOnly)); inputs.favOnly.classList.toggle('m7v5e', state.favOnly); syncQuery(); applyFilters(); }); els.sortBy.addEventListener('change',()=>{ state.sortBy=els.sortBy.value; syncQuery(); applyFilters(); }); els.pageSize.addEventListener('change',()=>{ state.pageSize=Number(els.pageSize.value); syncQuery(); render(); }); inputs.q.addEventListener('input', debounce(()=>{ syncQuery(); applyFilters(); }, 300)); inputs.category.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.language.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.date.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.priceMin.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.priceMax.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.durationMin.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.durationMax.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.ratingMin.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.provider.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.groupSize.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.pickup.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.accessibility.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); inputs.tags.addEventListener('change',()=>{ syncQuery(); applyFilters(); }); // Mobile menu els.mobileMenu?.classList.remove('l6p9a'); els.mobileMenu?.addEventListener('click',()=>{ const expanded = els.mobileMenu.getAttribute('aria-expanded')==='true'; els.mobileMenu.setAttribute('aria-expanded', String(!expanded)); const navs = $$('.t5s0b'); navs.forEach(n=>{ if(window.getComputedStyle(n).display==='none'){ n.style.display='flex'; } else{ n.style.display='none'; } }); }); async function loadData(){ try{ const res = await fetch('./catalog.json',{cache:'no-store'}); if(!res.ok) throw new Error('Failed to load'); const data = await res.json(); // Expect schema as list state.all = Array.isArray(data)? data : (data.tours || []); }catch(e){ state.all = []; } populateDynamicOptions(); restoreFromQuery(); applyFilters(); } loadData(); })();