// ============================================================ // Sabor Flow Da Nang — main.js // Update SCHEDULE array monthly when the weekly program changes. // ============================================================ const SCHEDULE = [ { day: 'Monday', name: 'Bachata on Mondays', when: '8:00 PM – 11:00 PM', fee: '100k VND', venue: 'Webe Coffee', address: '14–16 Nguyễn Hữu Thông, An Hải, Đà Nẵng', mapUrl: 'https://maps.app.goo.gl/DiRPZrr4wyW1Wy1eA', music: '8 Bachata · 1 Salsa · 1 Kizomba', city: 'Da Nang', organizer: ['Frog and Goose'], organizerUrl: ['https://www.instagram.com/fgbachata/'], offer: '1 drink included · Workshop included', vibe: 'Workshop (8PM–9:30PM) & Social', }, { day: 'Tuesday', name: 'SBK', when: '9:00 PM – 11:30 PM', fee: '100k VND', venue: 'Cáliz', address: '47–49 An Thượng 3, Ngũ Hành Sơn, Đà Nẵng', mapUrl: 'https://maps.app.goo.gl/JUBLZ5qHxkwXCe5u7', music: '2 Salsa · 3 Bachata · 2 Kizomba', city: 'Da Nang', organizer: ['Daisy Nguyen'], organizerUrl: ['https://www.facebook.com/minhtrang610'], offer: '50k for first 10 ladies', vibe: 'Elegant Wine Bar', }, { day: 'Wednesday', name: 'Kiz & Bachata', when: '7:30 PM – Late', fee: 'FREE', venue: 'Kết Fai', address: '138 Đỗ Bá, Ngũ Hành Sơn, Đà Nẵng', mapUrl: 'https://maps.app.goo.gl/W6nRJfwr2Vhy8iK66', music: 'Kizomba · Bachata', city: 'Da Nang', organizer: ['Sean Kim'], organizerUrl: ['https://www.facebook.com/happynowkkop'], offer: 'Buy 1 Cocktail, Get 1 Wine', vibe: 'Kizomba (7:30PM–9:15PM) & Bachata (9:30PM–Late) · Chill bar', }, { day: 'Thursday', name: 'Salsa & Bachata', when: '9:00 PM – 11:30 PM', fee: '100k VND', venue: 'Malibu Beach Club', address: 'Bãi tắm, 3 Võ Nguyên Giáp, An Hải, Đà Nẵng', mapUrl: 'https://maps.app.goo.gl/GZQC5micBRunbVNP8', music: '2 Salsa · 2 Bachata', city: 'Da Nang', organizer: ['Lucho Giraldes'], organizerUrl: ['https://www.facebook.com/luchogiraldespersonal'], offer: 'Beach club snacks available', vibe: 'Open-air Sea Breeze', }, { day: 'Friday', name: 'Salsa & Bachata', when: '8:00 PM – Late', fee: '100k VND', venue: 'An Thượng By Night', address: '100 Lã Xuân Oai, Ngũ Hành Sơn, Đà Nẵng', mapUrl: 'https://maps.app.goo.gl/R6mH4uQfAZtUWiH87', music: '2 Salsa · 2 Bachata · 2 Kizomba', city: 'Da Nang', organizer: ['Nadya Yafarova'], organizerUrl: ['https://www.facebook.com/yagfarova'], offer: 'Free for first 10 ladies', vibe: 'Community Family', }, { day: 'Saturday', name: '100% Kizomba', when: '8:00 PM – 11:00 PM', fee: '100k VND', venue: 'LiiDy Studio', address: '75 Thái Phiên, Hải Châu, Đà Nẵng', mapUrl: 'https://maps.app.goo.gl/GZ5UtDPFafW1BCSP9', music: 'Kizomba', city: 'Da Nang', organizer: ['Kate Bvote'], organizerUrl: ['https://www.facebook.com/kate.bovt'], offer: '1 Drinking water included', vibe: 'Deep Connection', }, { day: 'Sunday', name: 'SBK', when: '8:30 PM – Late', fee: '50k VND', venue: 'The Workshop', address: '35 Chế Lan Viên, Ngũ Hành Sơn, Đà Nẵng', mapUrl: 'https://maps.app.goo.gl/xxVwTocSQHKU1Ejd8', music: '2 Salsa · 3 Bachata · 3 Kizomba', city: 'Da Nang', organizer: ['Daisy Nguyen'], organizerUrl: ['https://www.facebook.com/minhtrang610'], offer: '1-hr Kiz session included', vibe: 'Festival style rotation', }, { day: 'Sunday', name: 'Salsa & Bachata', when: '8:00 PM – 11:30 PM', fee: '100k VND', venue: 'An Thượng By Night', address: '100 Lã Xuân Oai, Ngũ Hành Sơn, Đà Nẵng', mapUrl: 'https://maps.app.goo.gl/R6mH4uQfAZtUWiH87', music: '2 Salsa · 2 Bachata', city: 'Da Nang', organizer: ['Vaclav', 'Ksenia Tokareva'], organizerUrl: ['https://www.facebook.com/vasek.nemec.1', 'https://www.facebook.com/omks234'], offer: '1 drink included & 10% discount on meals', vibe: 'Workshop (8PM–9PM) & Social', }, ]; const DAYS_ORDER = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; function getTodayName() { return DAYS_ORDER[new Date().getDay()]; } // Parse "2 Salsa · 3 Bachata · 1 Kizomba" into sfB-chip spans function musicToChips(music) { const styleMap = { salsa: 'salsa', bachata: 'bachata', kizomba: 'kizomba', zouk: 'zouk' }; return music.split('·').map(s => s.trim()).filter(Boolean).map(segment => { let cls = ''; for (const [key, val] of Object.entries(styleMap)) { if (segment.toLowerCase().includes(key)) { cls = val; break; } } return `${segment}`; }).join(''); } // Build organizer name(s) as linked text function organizerLinks(organizer, organizerUrl, stopProp) { const stop = stopProp ? ' onclick="event.stopPropagation()"' : ''; return organizer.map((name, i) => { const url = organizerUrl && organizerUrl[i]; return url ? `${name}` : name; }).join(' & '); } // ---- Nav (mobile) ---- function initNav() { const hamburger = document.getElementById('sfB-hamburger'); const mobileNav = document.getElementById('sfB-mobile-nav'); const closeBtn = document.getElementById('sfB-mobile-nav-close'); if (!hamburger || !mobileNav) return; hamburger.addEventListener('click', () => mobileNav.classList.add('open')); if (closeBtn) closeBtn.addEventListener('click', () => mobileNav.classList.remove('open')); mobileNav.querySelectorAll('a').forEach(link => { link.addEventListener('click', () => mobileNav.classList.remove('open')); }); mobileNav.addEventListener('click', e => { if (e.target === mobileNav) mobileNav.classList.remove('open'); }); } // ---- Event detail modal ---- let _sortedSchedule = []; function openEventModal(row) { const modal = document.getElementById('sfB-ev-modal'); if (!modal) return; const now = new Date(); const offset = (DAYS_ORDER.indexOf(row.day) - now.getDay() + 7) % 7; const date = new Date(now); date.setDate(now.getDate() + offset); const dateLabel = `${date.getDate()}/${date.getMonth() + 1}`; document.getElementById('sfB-ev-modal-day').textContent = `${dateLabel} · ${row.day} · ${row.when}`; document.getElementById('sfB-ev-modal-name').textContent = row.name; const orgLinks = organizerLinks(row.organizer, row.organizerUrl, false); document.getElementById('sfB-ev-modal-body').innerHTML = `
Venue 📍 ${row.venue} ${row.address}
Music ${musicToChips(row.music)}
Host ${orgLinks}
Entry ${row.fee}
Offer ${row.offer}
Vibe ${row.vibe}
`; modal.classList.add('open'); document.body.style.overflow = 'hidden'; } function initEventModal() { const modal = document.getElementById('sfB-ev-modal'); if (!modal) return; function closeModal() { modal.classList.remove('open'); document.body.style.overflow = ''; } document.getElementById('sfB-ev-modal-close').addEventListener('click', closeModal); document.getElementById('sfB-ev-modal-overlay').addEventListener('click', closeModal); document.addEventListener('keydown', e => { if (e.key === 'Escape' && modal.classList.contains('open')) closeModal(); }); } // ---- Schedule: dynamic filter pills ---- function renderScheduleFilters(events) { const container = document.getElementById('sfB-schedule-pills'); if (!container) return; const styleOptions = [ { key: 'salsa', label: 'Salsa' }, { key: 'bachata', label: 'Bachata' }, { key: 'kizomba', label: 'Kizomba' }, { key: 'zouk', label: 'Zouk' }, ]; const cityOptions = [ { key: 'hoian', label: 'Hoi An', match: 'hoi an' }, ]; const activeStyles = styleOptions.filter(s => events.some(e => e.music.toLowerCase().includes(s.key)) ); const activeCities = cityOptions.filter(c => events.some(e => e.city.toLowerCase().includes(c.match)) ); container.innerHTML = [ '', ...activeStyles.map(s => ``), ...activeCities.map(c => ``), ].join(''); container.querySelectorAll('.filter-pill').forEach(pill => { pill.addEventListener('click', () => { container.querySelectorAll('.filter-pill').forEach(p => p.classList.remove('active')); pill.classList.add('active'); const f = pill.dataset.filter; document.querySelectorAll('#sfB-schedule-list .sfB-row').forEach(row => { if (f === 'all') { row.style.display = ''; return; } const music = row.dataset.music || ''; const city = row.dataset.city || ''; const matchCity = f === 'danang' ? 'da nang' : f === 'hoian' ? 'hoi an' : f; row.style.display = (music.includes(f) || city.includes(matchCity)) ? '' : 'none'; }); }); }); } // ---- Schedule: full list (schedule.html) ---- function renderScheduleList() { const list = document.getElementById('sfB-schedule-list'); if (!list) return; const now = new Date(); const todayName = DAYS_ORDER[now.getDay()]; const startIdx = DAYS_ORDER.indexOf(todayName); const dayOffset = {}; DAYS_ORDER.forEach(name => { dayOffset[name] = (DAYS_ORDER.indexOf(name) - startIdx + 7) % 7; }); _sortedSchedule = [...SCHEDULE].sort((a, b) => dayOffset[a.day] - dayOffset[b.day]); renderScheduleFilters(_sortedSchedule); list.innerHTML = _sortedSchedule.map((row, i) => { const isToday = row.day === todayName; const offset = dayOffset[row.day]; const date = new Date(now); date.setDate(now.getDate() + offset); const dayNum = `${date.getDate()}/${date.getMonth() + 1}`; const orgHtml = organizerLinks(row.organizer, row.organizerUrl, true); return `
${dayNum} ${row.day}
${isToday ? '▶ Tonight' : ''}
${row.when}
${row.name}
${orgHtml}
${musicToChips(row.music)}
📍 ${row.venue}
`; }).join(''); list.querySelectorAll('.sfB-row').forEach(el => { function openRow() { const idx = parseInt(el.dataset.idx, 10); openEventModal(_sortedSchedule[idx]); } el.addEventListener('click', openRow); el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openRow(); } }); }); } // ---- Home: "Coming up" teaser (4 cards starting from today) ---- function renderScheduleTeaser() { const container = document.getElementById('sfB-coming-row'); if (!container) return; const now = new Date(); const today = getTodayName(); const startIdx = DAYS_ORDER.indexOf(today); const order = [...DAYS_ORDER.slice(startIdx), ...DAYS_ORDER.slice(0, startIdx)]; const dayOffset = {}; DAYS_ORDER.forEach(name => { dayOffset[name] = (DAYS_ORDER.indexOf(name) - startIdx + 7) % 7; }); const sorted = [...SCHEDULE].sort( (a, b) => order.indexOf(a.day) - order.indexOf(b.day) ); container.innerHTML = sorted.slice(0, 4).map(row => { const offset = dayOffset[row.day]; const date = new Date(now); date.setDate(now.getDate() + offset); const dayNum = `${date.getDate()}/${date.getMonth() + 1}`; const orgText = row.organizer.join(' & '); return `
${dayNum} ${row.day}
${row.when || ''}

${row.name}

${orgText}
@ ${row.venue} · ${row.city}
${musicToChips(row.music)}
`; }).join(''); } // ---- Filter pills (studios.html / classes.html) ---- function initFilters() { const cards = document.querySelectorAll('.filterable-card'); if (!cards.length) return; const pills = document.querySelectorAll('.filter-pill'); if (!pills.length) return; pills.forEach(pill => { pill.addEventListener('click', () => { pills.forEach(p => p.classList.remove('active')); pill.classList.add('active'); const filter = pill.dataset.filter; cards.forEach(card => { if (filter === 'all') { card.classList.remove('hidden'); } else { const styles = (card.dataset.styles || '') .split(',') .map(s => s.trim().toLowerCase()); card.classList.toggle('hidden', !styles.includes(filter)); } }); }); }); } // ---- Palm fronds: lock sizes AND positions to px at load so nothing recalculates on mobile scroll ---- function lockPalmSizes() { const vw = window.innerWidth; const vh = window.innerHeight; const clamp = (val, min, max) => Math.min(Math.max(val, min), max); const config = { 'corner-tl': { size: clamp(vw * 0.40, 100, 620), pos: { top: vh * -0.08, left: vw * -0.15 } }, 'corner-tr': { size: clamp(vw * 0.40, 100, 620), pos: { top: vh * -0.08, right: vw * -0.10 } }, 'corner-bl': { size: clamp(vw * 0.40, 100, 620), pos: { bottom: vh * -0.08, left: vw * -0.15 } }, 'corner-br': { size: clamp(vw * 0.40, 100, 620), pos: { bottom: vh * -0.08, right: vw * -0.10 } }, 'corner-ml': { size: clamp(vw * 0.49, 120, 760), pos: { top: vh * 0.32, left: vw * -0.15 } }, 'corner-ml2': { size: clamp(vw * 0.45, 110, 700), pos: { top: vh * 0.62, left: vw * -0.10 } }, 'corner-mr': { size: clamp(vw * 0.44, 110, 680), pos: { top: vh * 0.22, right: vw * -0.10 } }, 'corner-mr2': { size: clamp(vw * 0.48, 115, 740), pos: { top: vh * 0.70, right: vw * -0.10 } }, }; document.querySelectorAll('.sfB-palm').forEach(el => { for (const cls of el.classList) { const cfg = config[cls]; if (!cfg) continue; const px = cfg.size + 'px'; el.style.width = px; el.style.height = px; for (const [prop, val] of Object.entries(cfg.pos)) { el.style[prop] = val + 'px'; } break; } }); } // ---- Fireflies ---- function initFireflies() { const container = document.createElement('div'); container.setAttribute('aria-hidden', 'true'); for (let i = 0; i < 15; i++) { const fly = document.createElement('div'); fly.className = 'firefly'; container.appendChild(fly); } document.body.prepend(container); } // ---- Init ---- document.addEventListener('DOMContentLoaded', () => { initFireflies(); initNav(); renderScheduleList(); renderScheduleTeaser(); initFilters(); initEventModal(); const yearEl = document.getElementById('footer-year'); if (yearEl) yearEl.textContent = new Date().getFullYear(); lockPalmSizes(); });