// ============================================================
// 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 = `
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)}
`;
}).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();
});