Upcoming Events

(() => { 'use strict'; // === Your published CSV (targets gid=0 tab) === const CSV_URL = "https://docs.google.com/spreadsheets/d/e/2PACX-1vR0N9M2sACtG-R3_j6SQoZQkxgrjWDHWYVAVrL7mKaXliu_SxrMOwcg8giDTX5l-4zO1k2H2B2Btxsd/pub?gid=0&single=true&output=csv"; // Timezone for calendar const TZ = "America/Regina"; const TZ_OFFSET = "-06:00"; // SK stays on CST // ---------- Helpers ---------- const $ = s => document.querySelector(s); const enc = s => encodeURIComponent(s||''); const esc = s => String(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); // Robust CSV (quotes/commas/newlines) function parseCSV(text){ const out=[], s=text.replace(/\r\n/g,'\n').replace(/\r/g,'\n'); let row=[], f="", i=0, q=false; while(iv==="")) out.pop(); return out; } // Header index with flexible matching function findIdx(headers, needles){ const lower = headers.map(h=>h.toLowerCase()); for (let i=0;iDASH_RX.test(raw)?raw.trim():(d?d.toLocaleDateString('en-CA',{month:'short',day:'numeric',year:'numeric'}):raw); // Times function parseTimeFlexible(s){ if(!s) return null; if(/all\s*day/i.test(s)) return {allDay:true}; const m12 = s.match(/(\d{1,2})(?::(\d{2}))?\s*(AM|PM)/i); if(m12){ let h=+m12[1], m=m12[2]?+m12[2]:0, ap=m12[3].toUpperCase(); if(ap==='PM' && h<12) h+=12; if(ap==='AM' && h===12) h=0; return {h,m,allDay:false}; } const m24 = s.match(/^(\d{1,2}):(\d{2})$/); if(m24) return {h:+m24[1], m:+m24[2], allDay:false}; return null; } function timesFrom({dateObj,startText,endText,locTimeText}){ const d=new Date(dateObj); const Y=d.getFullYear(), M=String(d.getMonth()+1).padStart(2,'0'), D=String(d.getDate()).padStart(2,'0'); const pref=`${Y}-${M}-${D}T`; const s=parseTimeFlexible(startText||""); const e=parseTimeFlexible(endText||""); if((s&&s.allDay)||(e&&e.allDay)||/all\s*day/i.test(locTimeText||"")){ return {allDay:true,start:`${pref}00:00`,end:`${pref}23:59`}; } if(s && !s.allDay){ const sh=String(s.h).padStart(2,'0'), sm=String(s.m).padStart(2,'0'); let eh, em; if(e && !e.allDay){ eh=e.h; em=e.m; } else { eh=s.h+2; em=s.m; if(eh>=24) eh-=24; } return {allDay:false,start:`${pref}${sh}:${sm}`,end:`${pref}${String(eh).padStart(2,'0')}:${String(em).padStart(2,'0')}`}; } // Try a range in "Location & Time" const re1=/(\d{1,2}:\d{2}\s*(AM|PM))\s*[\u2013\u2014-]\s*(\d{1,2}:\d{2}\s*(AM|PM))/i; const re2=/(\d{1,2}\s*(AM|PM))\s*[\u2013\u2014-]\s*(\d{1,2}\s*(AM|PM))/i; const m=(locTimeText||"").match(re1)||(locTimeText||"").match(re2); if(m){ const to24=t=>{const o=parseTimeFlexible(t);return {h:o.h,m:o.m};}; const s2=to24(m[1]), e2=to24(m[3]); return {allDay:false,start:`${pref}${String(s2.h).padStart(2,'0')}:${String(s2.m).padStart(2,'0')}`,end:`${pref}${String(e2.h).padStart(2,'0')}:${String(e2.m).padStart(2,'0')}`}; } return {allDay:false,start:`${pref}18:00`,end:`${pref}20:00`}; } // Calendar + Share const z=t=>t.replace(/[-:]/g,'').slice(0,15); const dayStr=l=>l.replace(/-/g,'').slice(0,8); const isoLocalToUtcZ=local=>new Date(local+TZ_OFFSET).toISOString().replace(/\.\d{3}Z$/,'Z'); function buildICS({title,description,location,start,end,allDay,url}){ const uid=(crypto&&crypto.randomUUID)?crypto.randomUUID():(Date.now()+"@eventsmoosejaw.ca"); const lines=["BEGIN:VCALENDAR","VERSION:2.0","PRODID:-//Events Moose Jaw//EN","CALSCALE:GREGORIAN","METHOD:PUBLISH","BEGIN:VEVENT", `UID:${uid}`,`SUMMARY:${title||""}`,`DESCRIPTION:${(description||"").replace(/\n/g,"\\n")}\\n${url||""}`,`LOCATION:${location||""}`, `DTSTAMP:${new Date().toISOString().replace(/[-:]/g,'').replace(/\.\d{3}Z$/,'Z')}`]; if(allDay){ const s=dayStr(start); const d=new Date(start+TZ_OFFSET); d.setDate(d.getDate()+1); const e=`${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}`; lines.push(`DTSTART;VALUE=DATE:${s}`,`DTEND;VALUE=DATE:${e}`); } else { lines.push(`DTSTART;TZID=${TZ}:${z(start)}`,`DTEND;TZID=${TZ}:${z(end)}`); } lines.push("END:VEVENT","END:VCALENDAR"); return lines.join("\r\n"); } function gcalUrl({title,description,location,start,end,allDay}){ const dates = allDay ? (()=>{const s=dayStr(start); const d=new Date(start+TZ_OFFSET); d.setDate(d.getDate()+1); const e=`${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}`; return `${s}/${e}`;})() : `${isoLocalToUtcZ(start).replace(/[-:]/g,'').replace('.000','')}/${isoLocalToUtcZ(end).replace(/[-:]/g,'').replace('.000','')}`; const p=new URLSearchParams({action:"TEMPLATE",text:title||"",dates,details:description||"",location:location||"",ctz:TZ}); return `https://calendar.google.com/calendar/render?${p.toString()}`; } function shareLinks({title,url,location}){ const text=`${title||"Event"} — ${location||""}`.trim(); const href=url||location||location?.href||""; return { web:{title:title||"Event",text,url:href}, email:`mailto:?subject=${enc(title||"Event")}&body=${enc((href?href+"\n\n":"")+text)}`, fb:`https://www.facebook.com/sharer/sharer.php?u=${enc(href)}`, x:`https://twitter.com/intent/tweet?text=${enc(text)}&url=${enc(href)}`, wa:`https://api.whatsapp.com/send?text=${enc(`${title||"Event"} ${href?("• "+href):""}`)}` }; } function colorFrom(str){let h=0;for(let i=0;i>(i*8))&255;c+=('00'+v.toString(16)).slice(-2);}return c;} // ---------- UI ---------- function renderCard(e){ const card=document.createElement('div'); card.className='card'; let html=`
📅 ${esc(e.dateDisplay)} — ${esc(e.name)}
`; if(e.category) html+=`
${esc(e.category)}
`; if(e.venue) html+=`
📍 ${esc(e.venue)}
`; if(e.desc) html+=`
${esc(e.desc)}
`; if(e.link) html+=``; card.innerHTML=html; const t=timesFrom({dateObj:e.parsedDate,startText:e.startText,endText:e.endText,locTimeText:e.locTimeText}); const data={title:e.name,description:e.desc,location:e.venue,start:t.start,end:t.end,allDay:t.allDay,url:e.link||location.href}; const actions=document.createElement('div'); actions.className='actions'; // Calendar const calWrap=document.createElement('div'); calWrap.className='menu'; const calBtn=document.createElement('button'); calBtn.className='btn'; calBtn.textContent='➕ Add to Calendar'; const calDd=document.createElement('div'); calDd.className='dd'; const aG=document.createElement('a'); aG.href=gcalUrl(data); aG.target='_blank'; aG.rel='noopener'; aG.textContent='Google Calendar'; const aI=document.createElement('a'); aI.href='#'; aI.textContent='Apple / Outlook (ICS)'; aI.addEventListener('click',ev=>{ev.preventDefault(); const blob=new Blob([buildICS(data)],{type:"text/calendar;charset=utf-8"}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=(data.title||"event").toLowerCase().replace(/[^a-z0-9]+/g,'-')+".ics"; document.body.appendChild(a); a.click(); setTimeout(()=>{URL.revokeObjectURL(a.href); a.remove();},0); calDd.style.display='none';}); calDd.appendChild(aG); calDd.appendChild(aI); calWrap.appendChild(calBtn); calWrap.appendChild(calDd); calBtn.addEventListener('click',()=>calDd.style.display=(calDd.style.display==='block'?'none':'block')); document.addEventListener('click',e3=>{ if(!calWrap.contains(e3.target)) calDd.style.display='none'; }); // Share const shareWrap=document.createElement('div'); shareWrap.className='menu'; const shareBtn=document.createElement('button'); shareBtn.className='btn'; shareBtn.textContent='🔗 Share'; const shareDd=document.createElement('div'); shareDd.className='dd'; const S=shareLinks(data); const tryNative=async()=>{ if(navigator.share){ try{ await navigator.share(S.web); return true; }catch(_){ return false; } } return false; }; [{label:"Share (native)",onClick:async(ev)=>{ev.preventDefault();const ok=await tryNative();if(!ok) window.open(S.email,'_blank');}}, {label:"Email",href:S.email},{label:"Facebook",href:S.fb},{label:"X (Twitter)",href:S.x},{label:"WhatsApp",href:S.wa} ].forEach(cfg=>{const a=document.createElement('a');a.textContent=cfg.label; if(cfg.href){a.href=cfg.href;a.target='_blank';a.rel='noopener';} if(cfg.onClick){a.addEventListener('click', cfg.onClick);} shareDd.appendChild(a);}); shareWrap.appendChild(shareBtn); shareWrap.appendChild(shareDd); shareBtn.addEventListener('click',()=>shareDd.style.display=(shareDd.style.display==='block'?'none':'block')); document.addEventListener('click',e3=>{ if(!shareWrap.contains(e3.target)) shareDd.style.display='none'; }); actions.appendChild(calWrap); actions.appendChild(shareWrap); card.appendChild(actions); return card; } function buildChips(categories){ const bar=$('#emj-bar'), chips=$('#emj-chips'); chips.innerHTML=''; const make=(label,val,active=false)=>{ const b=document.createElement('button'); b.className='chip'; if(active) b.classList.add('active'); b.textContent=label; if(val) b.style.background=colorFrom(val); b.addEventListener('click',()=>{ document.querySelectorAll('#emj .chip').forEach(x=>x.classList.remove('active')); b.classList.add('active'); state.cat=(val||'').toLowerCase(); applyFilters(); }); chips.appendChild(b); }; make('All','',true); categories.sort().forEach(c=>make(c,c,false)); bar.style.display=categories.length?'block':'none'; } // ---------- State & Filters ---------- const state={all:[],past:[],up:[],cat:'',archiveOpen:false,archiveLoaded:false}; function applyFilters(){ const q=$('#emj-q').value.trim().toLowerCase(); const cat=state.cat; const match=e=>(!q||e.search.includes(q))&&(!cat||(e.category||'').toLowerCase()===cat); $('#emj-up').querySelectorAll('.card').forEach(c=>c.remove()); state.up.filter(match).forEach(e=>$('#emj-up').appendChild(renderCard(e))); if(state.archiveLoaded){ $('#emj-arch').querySelectorAll('.card').forEach(c=>c.remove()); state.past.filter(match).forEach(e=>$('#emj-arch').appendChild(renderCard(e))); } } // ---------- Load & Render ---------- async function load(){ $('#emj-up').innerHTML='
Loading events…
'; let text; try{ const res=await fetch(CSV_URL,{cache:'no-store'}); if(!res.ok) throw new Error('HTTP '+res.status); text=await res.text(); }catch(e){ $('#emj-up').innerHTML='
Couldn’t load events. Check your published CSV link.
'; return; } const rows=parseCSV(text); if(!rows||rows.length<2){ $('#emj-up').innerHTML='
No events found.
'; return; } const headers=rows[0].map(h=>String(h||'').trim()); const idx={ date: findIdx(headers, ['date']), name: findIdx(headers, ['event name','name','title']), cat: findIdx(headers, ['category','type']), loct: findIdx(headers, ['location & time','location']), start: findIdx(headers, ['start time','start']), end: findI
Family

Community Ice Skating

Join the community for open ice skating at the civic center this Saturday.

Read more →
Music

Live Jazz at Downtown Café

Enjoy smooth jazz and great food every Friday evening downtown.

Read more →

Get This Week’s Events in Your Inbox

Explore Moose Jaw

Best Coffee Shops in Moose Jaw

Discover Moose Jaw’s coziest spots for your caffeine fix.

Top Parks & Trails for Spring

Perfect spots for walking, biking, and picnics in Moose Jaw.

About

Events Moose Jaw is your simple, ad-free community calendar.
We collect events from Facebook, posters, and community submissions so you don’t miss what’s happening in the Friendly City.

📧 [email protected]