Files
saborflow/js/main.js
2026-05-02 14:56:44 +07:00

216 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// Sabor Flow Da Nang — main.js
// Update SCHEDULE array monthly when the weekly program changes.
// ============================================================
const SCHEDULE = [
{ day: 'Monday', when: '8:30 pm', venue: 'Webe Coffee', social: 'BOM', music: '8 Bachata · 1 Salsa · 1 Kizomba', city: 'Da Nang', organizer: 'Luu Phuong & Shai', mapUrl: 'https://maps.app.goo.gl/sznmGXHmiiaWf1ke6' },
{ day: 'Tuesday', when: '9 pm', venue: 'La Riva', social: 'Latino Dancing', music: '2 Salsa · 2 Bachata · 2 Kizomba', city: 'Hoi An', organizer: 'David Tavares', mapUrl: 'https://maps.app.goo.gl/3ZyB8icd8oswRghE7' },
{ day: 'Tuesday', when: '9 pm', venue: 'Caliz Bar', social: 'Sensual Night', music: '3 Bachata · 2 Salsa · 2 Kizomba', city: 'Da Nang', organizer: 'Daisy Nguyen', mapUrl: 'https://maps.app.goo.gl/YagFKw2gcTJp9PJc7' },
{ day: 'Wednesday', when: '7:30 pm', venue: 'Ket Fai Bar', social: 'Bachata Kiz Night', music: 'Kizomba 7:30 · Bachata 9 pm', city: 'Da Nang', organizer: 'Sean Kim', mapUrl: 'https://maps.app.goo.gl/XYxY4UCnnJjCAomK6' },
{ day: 'Thursday', when: '9 pm', venue: 'Malibu', social: 'Salsa Nights', music: '2 Salsa · 2 Bachata', city: 'Da Nang', organizer: 'Lucho Giraldes', mapUrl: 'https://maps.app.goo.gl/hZttmc9UcymnKfWP8' },
{ day: 'Friday', when: '9 pm', venue: 'An Thuong By Night', social: 'Dance Unity Party', music: '2 Salsa · 2 Bachata · 2 Kizomba', city: 'Da Nang', organizer: 'Nadiya Yagfarova', mapUrl: 'https://maps.app.goo.gl/9cHcJcDgan9ntowt9' },
{ day: 'Saturday', when: '9 pm', venue: 'La Riva', social: 'Latino Dancing', music: '2 Salsa · 2 Bachata · 2 Kizomba', city: 'Hoi An', organizer: 'David Tavares', mapUrl: 'https://maps.app.goo.gl/3ZyB8icd8oswRghE7' },
{ day: 'Saturday', when: '9 pm', venue: 'An Thuong By Night', social: 'Bachata Party', music: 'Only Bachata', city: 'Da Nang', organizer: 'Vaclav & Kseniya', mapUrl: 'https://maps.app.goo.gl/9cHcJcDgan9ntowt9' },
{ day: 'Sunday', when: '9 pm', venue: 'Last Call', social: 'Latin Dance Social', music: '3 Bachata · 2 Salsa', city: 'Da Nang', organizer: 'Vaclav & Kseniya', mapUrl: 'https://maps.app.goo.gl/1tEE4itDEeErhEmG6' },
{ day: 'Sunday', when: '9 pm', venue: 'Corner Bar', social: 'Sunday Latin', music: '3 Bachata · 2 Salsa · 3 Kizomba', city: 'Da Nang', organizer: 'Daisy Nguyen', mapUrl: 'https://maps.app.goo.gl/QyuWvCg2DZoFiRyd6' },
];
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 `<span class="sfB-chip ${cls}">${segment}</span>`;
}).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'));
});
// close on backdrop click
mobileNav.addEventListener('click', e => {
if (e.target === mobileNav) mobileNav.classList.remove('open');
});
}
// ---- 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);
// Map each day name to how many days from today it is (06)
const dayOffset = {};
DAYS_ORDER.forEach(name => {
dayOffset[name] = (DAYS_ORDER.indexOf(name) - startIdx + 7) % 7;
});
const sorted = [...SCHEDULE].sort((a, b) => dayOffset[a.day] - dayOffset[b.day]);
list.innerHTML = sorted.map(row => {
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 cityClass = row.city === 'Hoi An' ? 'hoian' : 'danang';
const venueEl = row.mapUrl
? `<a href="${row.mapUrl}" target="_blank" rel="noopener">${row.venue}</a>`
: row.venue;
return `
<div class="sfB-row${isToday ? ' today' : ''}">
<div class="col-day">
<div class="day-line">
<span class="date-num">${dayNum}</span>
<span class="name">${row.day}</span>
</div>
${isToday ? '<span class="tonight-label">▶ Tonight</span>' : ''}
</div>
<div class="col-when">${row.when || ''}</div>
<div class="col-social">
${row.social}
<span class="sub">${row.organizer}</span>
</div>
<div class="col-music">${musicToChips(row.music)}</div>
<div class="col-venue">
<span class="pin">📍</span>
${venueEl}
</div>
<div><span class="col-city ${cityClass}">${row.city}</span></div>
</div>`;
}).join('');
}
// ---- 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}`;
return `
<a href="/schedule/" class="sfB-tcard">
<div class="day"><span class="day-date">${dayNum}</span> ${row.day}</div>
<div class="when">${row.when || ''}</div>
<h4>${row.social}</h4>
<div class="organizer">${row.organizer}</div>
<div class="at">@ ${row.venue} · ${row.city}</div>
<div class="chips">${musicToChips(row.music)}</div>
</a>
`;
}).join('');
}
// ---- Filter pills (studios.html / classes.html) ----
function initFilters() {
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;
document.querySelectorAll('.filterable-card').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);
// Each entry: [size, { top|bottom, left|right } in px]
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;
}
});
}
// ---- Init ----
document.addEventListener('DOMContentLoaded', () => {
initNav();
renderScheduleList();
renderScheduleTeaser();
initFilters();
const yearEl = document.getElementById('footer-year');
if (yearEl) yearEl.textContent = new Date().getFullYear();
lockPalmSizes();
});