// Halifax Now v3 — Run Clubs, Happy Hours, Patio Finder sections // ─── RUN CLUBS ────────────────────────────────────────────────────────────── function RunClubs() { const [detail, setDetail] = useState(null); if (detail) return setDetail(null)}/>; return ; } function RunClubsBrowse({ onSelect }) { const [dayFilter, setDayFilter] = useState('all'); const [vibeFilter, setVibeFilter] = useState('all'); const featured = D.RUN_CLUBS[0]; const DAYS = ['all','Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; const VIBES = ['all','chill','steady','social','fast','chaos','scenic']; const filtered = D.RUN_CLUBS.slice(1).filter(r => (dayFilter === 'all' || r.day === dayFilter) && (vibeFilter === 'all' || r.vibe === vibeFilter) ); return (
{/* featured */}
onSelect(featured)}/>
Featured Club

onSelect(featured)} style={{ fontFamily:"'Playfair Display',serif", fontWeight:900, fontSize:44, lineHeight:0.95, letterSpacing:'-0.02em', marginBottom:14, cursor:'pointer' }}>{featured.name}

{featured.blurb}

{[['📅',featured.day],['🕘',D.fmtTime(featured.time)],['📏',featured.distance],['⚡',featured.pace]].map(([i,v]) => ( {i} {v} ))}
{featured.members} members
{/* filters */}
Day: {DAYS.map(d => setDayFilter(d)}/>)}
Vibe: {VIBES.map(v => setVibeFilter(v)}/>)}
{/* grid */}
{filtered.map(club => (
onSelect(club)} style={{ padding:'18px 0', borderBottom:`1px solid ${T.ink}`, display:'grid', gridTemplateColumns:'76px 1fr', gap:14, cursor:'pointer', transition:'opacity 0.1s' }}>
{club.day.slice(0,3).toUpperCase()}
{D.fmtTime(club.time)}
{club.distance} · {club.pace}
{club.name}
📍 {club.meetAt}
))}
); } function RunClubDetail({ club, back }) { return (
{club.day}s · {D.fmtTime(club.time)} · {club.hood}

{club.name}

{club.blurb}

The Route

{club.route}

Upcoming Dates

{club.upcoming.map(d => { const { day, mon } = D.fmtDate(d); return (
{day}
{mon}
); })}
{/* sidebar */}
Club Details
{[['Distance',club.distance],['Pace',club.pace],['Meet at',club.meetAt],['Coffee after',club.coffee],['Neighbourhood',club.hood],['Members',`${club.members} runners`]].map(([k,v]) => (
{k}
{v}
))}
); } // ─── HAPPY HOURS ──────────────────────────────────────────────────────────── function HappyHours() { const [detail, setDetail] = useState(null); if (detail) return setDetail(null)}/>; return ; } function HappyHoursBrowse({ onSelect }) { const [tagFilter, setTagFilter] = useState('all'); const TAGS = ['all','pints','wine','cocktails','food','oysters','caesar']; function toMins(t) { return D.toMins(t); } const N = D.NOW_MINS; function status(h) { const s = toMins(h.starts), e = toMins(h.ends); if (!h.days.includes(D.TODAY_DOW)) return 'closed'; if (s <= N && e > N) return 'active'; if (s > N && s - N <= 120) return 'soon'; return 'later'; } const filtered = D.HAPPY_HOURS.filter(h => tagFilter === 'all' || h.tags.includes(tagFilter)); const active = filtered.filter(h => status(h) === 'active'); const soon = filtered.filter(h => status(h) === 'soon'); const later = filtered.filter(h => ['later','closed'].includes(status(h))); function HHCard({ h, st }) { const minsLeft = st === 'active' ? toMins(h.ends) - N : null; const urgent = minsLeft !== null && minsLeft < 45; return (
onSelect(h)} style={{ border:`2.5px solid ${T.ink}`, background:'#fff', boxShadow:`4px 4px 0 ${T.ink}`, overflow:'hidden', cursor:'pointer', opacity: st === 'later' || st === 'closed' ? 0.6 : 1 }}>
{st==='active'?(urgent?`⚠ Closes in ${minsLeft}m`:'● Open now'):st==='soon'?`Opens in ${toMins(h.starts)-N}m`:'Later today'} {D.fmtTime(h.starts)}–{D.fmtTime(h.ends)}
{h.hood}
{h.venue}
{h.deal}
{h.tags.map(t => {t})}
{h.note}
); } return (
{/* clock banner */}
4:45pm
Right now in Halifax
{D.HAPPY_HOURS.filter(h=>status(h)==='active').length} happy hours active · {D.HAPPY_HOURS.filter(h=>status(h)==='soon').length} opening soon
Thu Apr 23
Simulated time
Type: {TAGS.map(t => setTagFilter(t)}/>)}
{active.length > 0 && <>
● Active Now
{active.map(h => )}
} {soon.length > 0 && <>
Opening Soon
{soon.map(h => )}
} {later.length > 0 && <>
Later / Other Days
{later.map(h => )}
}
); } function HappyHourDetail({ hh, back }) { return (
{hh.hood} · {D.fmtTime(hh.starts)}–{D.fmtTime(hh.ends)}

{hh.venue}

{hh.deal}
{hh.note}
{hh.tags.map(t => {t})}
Hours & Days
{[['Happy hour', `${D.fmtTime(hh.starts)} – ${D.fmtTime(hh.ends)}`],['Days', hh.days.join(' · ')],['Address', hh.address],['Neighbourhood', hh.hood]].map(([k,v]) => (
{k}
{v}
))}
); } // ─── PATIO FINDER ────────────────────────────────────────────────────────── function PatioFinder() { const [detail, setDetail] = useState(null); if (detail) return setDetail(null)}/>; return ; } function PatiosBrowse({ onSelect }) { const [filters, setFilters] = useState({ dogs:false, covered:false, view:true, heated:false, reservations:false }); const toggle = k => setFilters(f => ({ ...f, [k]:!f[k] })); const activeF = Object.entries(filters).filter(([,v]) => v).map(([k]) => k); const filtered = activeF.length === 0 ? D.PATIOS : D.PATIOS.filter(p => activeF.every(f => p[f])); const ATTRS = { dogs:'🐕 Dog-friendly', covered:'⛱ Covered', view:'🌅 View', heated:'♨ Heated', reservations:'📅 Takes reservations' }; return (
{/* seasonal hero */}
☀️ Seasonal · Spring 2026

Patio
Finder

It's technically still jacket weather. Some of us are sitting outside anyway. Here's where to do it.

Today in Halifax
14°C
Partly cloudy · light wind
{D.PATIOS.length} patios in our guide
{/* filter bar */}
Filter: {Object.entries(ATTRS).map(([k,label]) => ( ))} {activeF.length > 0 && }
{filtered.length === 0 ?
No patios match those filters — try removing one.
:
{filtered.map(p => (
onSelect(p)} style={{ border:`2.5px solid ${T.ink}`, boxShadow:`4px 4px 0 ${T.ink}`, overflow:'hidden', background:'#fff', cursor:'pointer' }}>
{p.hood} · {p.size} patio
{p.venue}
{Object.keys(ATTRS).filter(k => p[k]).map(k => ( {ATTRS[k]} ))}

{p.note}

{p.vibe} {p.reservations && Book ahead}
))}
}
); } function PatioDetail({ patio, back }) { const ATTRS = { dogs:'🐕 Dog-friendly', covered:'⛱ Covered', view:'🌅 View', heated:'♨ Heated', reservations:'📅 Takes reservations' }; const amenities = Object.keys(ATTRS).filter(k => patio[k]); return (
{patio.hood} · {patio.size} patio

{patio.venue}

{patio.note}

What to know

{patio.reservations && }
{amenities.length > 0 && <>

Patio features

{amenities.map(k => ( {ATTRS[k]} ))}
}
Find it
{[['Address',patio.address],['Neighbourhood',patio.hood]].map(([k,v]) => (
{k}
{v}
))}
); } Object.assign(window, { RunClubs, HappyHours, PatioFinder });