My Bands Tonight
No events for this date
Β© 2025 Mybandstonight.com
'use strict';
const initializeApp = () => {
const WEB_APP_URL = 'https://script.google.com/macros/s/AKfycbzyOvGKB3fCxnLjqAwr7xdfZsDDm7btXQzLLIY5VkTQqHgbHyBswGjInd0CrJ6w0XXSKA/exec';
const IMAGE_SCRIPT_URL = 'https://script.google.com/macros/s/AKfycbyXh9c52fMIU1pb1L5-BdjpKU6GEVPiv6QUTZf2C2D4D_BohSFeOvNs1jVNCo-ws93lcg/exec';
const DEFAULT_BAND_PICTURE = "https://i.postimg.cc/X7Rc0MNC/temp-Image-Xyc3fg.avif";
const loadingOverlay = document.getElementById("loadingOverlay");
const calendarEl = document.querySelector(".calendar");
const calendarGrid = document.getElementById("calendarGrid");
const monthYear = document.getElementById("monthYear");
const notificationBanner = document.getElementById("notification-banner");
const eventCountDisplay = document.getElementById("event-count-display");
// NEW: Mobile Calendar Elements
const mobileCalendarBtn = document.getElementById('mobile-calendar-btn');
const calendarOverlay = document.getElementById('calendar-overlay');
const calendarCloseNub = document.getElementById('calendar-close-nub');
// --- LEAFLET CLICK PROPAGATION FIX ---
// This ensures that clicking the button doesn't click through to the map
if (mobileCalendarBtn) {
L.DomEvent.disableClickPropagation(mobileCalendarBtn);
L.DomEvent.disableScrollPropagation(mobileCalendarBtn);
}
// -------------------------------------
let allEvents = [], currentEvents = [], selectedDate = null, currentYear, currentMonth;
let lastEventCount = 0;
let animationTimeout = null;
/**
* Creates a stable, non-random ID from a string (djb2 hash).
* This ensures the same gig (name + date + venue) always gets the same ID.
*/
function generateStableId(str) {
let hash = 5381;
let i = str.length;
if (i === 0) return (hash >>> 0).toString();
while(i) {
hash = (hash * 33) ^ str.charCodeAt(--i);
}
// Return a positive integer as a string
return (hash >>> 0).toString();
}
const defaultCenter = [41.04, -72.3]; // Centered on East Hampton
const defaultZoom = 10.5;
const map = L.map("map", {
zoomControl: true,
dragging: true, scrollWheelZoom: true,
doubleClickZoom: true, boxZoom: true, keyboard: true,
touchZoom: true, tap: true, zoomSnap: 0.1,
}).setView(defaultCenter, defaultZoom);
map.invalidateSize();
map.zoomControl.setPosition('topright');
L.tileLayer("https://api.maptiler.com/maps/streets-v4/{z}/{x}/{y}.png?key=QrMP6spQY7XARU2oYBlJ", {
attribution: '\u003ca href=\"https://www.maptiler.com/copyright/\" target=\"_blank\"\u003e\u0026copy; MapTiler\u003c/a\u003e \u003ca href=\"https://www.openstreetmap.org/copyright\" target=\"_blank\"\u003e\u0026copy; OpenStreetMap contributors\u003c/a\u003e'
}).addTo(map);
const singlePinIcon = L.divIcon({
html: `
`,
className: 'svg-marker-background',
iconSize: [34, 34], // A good size for the map, adjust if needed
iconAnchor: [17, 17], // Anchors the icon at its bottom-center tip
});
let markerLayer = L.featureGroup().addTo(map);
let spiderfyLayer = L.featureGroup().addTo(map);
let currentSpiderfiedCluster = null;
let unspiderfyTimer = null;
let currentOpenPopup = null;
let popupCloseTimer = null;
const isMobile = () => window.innerWidth <= 768;
function scheduleUnspiderfy() { clearTimeout(unspiderfyTimer); unspiderfyTimer = setTimeout(unspiderfy, 400); }
function cancelUnspiderfy() { clearTimeout(unspiderfyTimer); }
function createCustomPopup(content, latLng, offset = [0, 0]) {
const popupContainer = document.createElement('div');
popupContainer.className = 'custom-popup-container';
const popup = document.createElement('div');
popup.className = 'custom-popup';
if (offset[0] !== 0) popup.classList.add('offset-left');
const popupContent = document.createElement('div');
popupContent.className = 'custom-popup-content';
popupContent.innerHTML = content;
const tipContainer = document.createElement('div');
tipContainer.className = 'custom-popup-tip-container';
const tip = document.createElement('div');
tip.className = 'custom-popup-tip';
tipContainer.appendChild(tip);
popup.appendChild(popupContent);
popup.appendChild(tipContainer);
popupContainer.appendChild(popup);
document.body.appendChild(popupContainer);
const mapBounds = document.getElementById('map').getBoundingClientRect();
const markerPoint = map.latLngToContainerPoint(latLng);
let left = mapBounds.left + markerPoint.x - (popup.offsetWidth / 2) + offset[0];
let top = mapBounds.top + markerPoint.y - popup.offsetHeight - 27;
popupContainer.style.left = left + 'px';
popupContainer.style.top = top + 'px';
setTimeout(() => {
popup.classList.add('visible');
}, 10);
popupContainer.addEventListener('mouseenter', () => { clearTimeout(popupCloseTimer); cancelUnspiderfy(); });
popupContainer.addEventListener('mouseleave', () => { popupCloseTimer = setTimeout(closeCustomPopup, 300); if (!isMobile()) { scheduleUnspiderfy(); } });
// Return the container, which is the element we attach listeners to
return popupContainer;
}
function closeCustomPopup() {
if (currentOpenPopup) {
currentOpenPopup.remove();
currentOpenPopup = null;
}
}
function showCustomPopup(content, latLng, eventData, offset = [0, 0]) {
closeCustomPopup();
currentOpenPopup = createCustomPopup(content, latLng, offset);
// This listener is for the main card (already fixed)
currentOpenPopup.addEventListener('click', (e) => {
e.stopPropagation();
// Do not close if the share button itself was clicked
if (e.target.closest('#popup-share-button')) {
return;
}
// 1. Instantly hide the small popup
const popupEl = currentOpenPopup.querySelector('.custom-popup');
if (popupEl) {
popupEl.style.transition = 'none';
popupEl.style.opacity = '0';
}
// 2. Use a tiny timeout to close/open
setTimeout(() => {
closeCustomPopup(); // 3. Remove from DOM
openGigDetailCard(eventData); // 4. Open detail card
}, 10);
});
// This is the listener for the SHARE button
const shareButton = currentOpenPopup.querySelector('#popup-share-button');
if (shareButton && eventData) {
// *** THIS IS THE FIX ***
shareButton.addEventListener('click', (e) => {
e.stopPropagation(); // Stop click from bubbling to the card
// 1. Instantly hide the small popup
const popupEl = currentOpenPopup.querySelector('.custom-popup');
if (popupEl) {
popupEl.style.transition = 'none';
popupEl.style.opacity = '0';
}
// 2. Use a tiny timeout to close/open
setTimeout(() => {
closeCustomPopup(); // 3. Remove from DOM
openShareModal(eventData); // 4. Open share modal
}, 10);
});
// *** END OF FIX ***
}
const imageElement = currentOpenPopup.querySelector('.custom-popup-content img');
if (imageElement) {
const startDynamicAnimation = (img) => {
const { naturalWidth, naturalHeight } = img;
if (!naturalWidth || !naturalHeight) return;
const aspectRatio = naturalHeight / naturalWidth;
const containerAspectRatio = 90 / 166; // approx 0.54
if (aspectRatio > containerAspectRatio) {
const BASE_DURATION = 3;
const SCALING_FACTOR = 7;
let duration = BASE_DURATION + (aspectRatio * SCALING_FACTOR);
duration = Math.max(5, Math.min(22, duration));
img.style.animation = `pan-image ${duration.toFixed(2)}s ease-in-out infinite`;
} else {
img.style.animation = 'none';
img.style.objectPosition = 'center';
}
};
if (imageElement.complete && imageElement.naturalHeight > 0) {
startDynamicAnimation(imageElement);
} else {
imageElement.onload = () => {
startDynamicAnimation(imageElement);
};
}
}
}
function formatTime12h(timeStr) {
if (!timeStr || typeof timeStr !== "string") return "";
timeStr = timeStr.trim().replace(/(\d{1,2}:\d{2}):\d{2}/, "$1");
if (timeStr.toUpperCase().includes("AM") || timeStr.toUpperCase().includes("PM")) { return timeStr.replace(/\s+/g, "").replace(/(AM|PM)/, " $1"); }
const match = timeStr.match(/^(\d{1,2}):(\d{2})$/);
if (!match) return timeStr;
let [, hourStr, minuteStr] = match;
let hour = parseInt(hourStr, 10);
const ampm = hour >= 12 ? "PM" : "AM";
hour = hour % 12;
if (hour === 0) hour = 12;
return `${hour}:${minuteStr} ${ampm}`;
}
function formatDateForDisplay(dateStr) {
if (!dateStr) return "";
const [year, month, day] = dateStr.split('-');
return `${month}/${day}/${year}`;
}
// --- SHARE MODAL LOGIC ---
const shareModalOverlay = document.getElementById('share-modal-overlay');
const shareModalCloseNub = document.getElementById('share-modal-close-nub');
const shareGigPreview = document.getElementById('share-gig-preview');
const shareBtnCopy = document.getElementById('share-btn-copy');
const copyBtnText = document.getElementById('copy-btn-text');
const shareBtnEmail = document.getElementById('share-btn-email');
const shareBtnSms = document.getElementById('share-btn-sms');
const shareBtnMore = document.getElementById('share-btn-more');
function openShareModal(gigInfo) {
const timeFormatted = gigInfo.time ? formatTime12h(gigInfo.time) : "";
const dateFormatted = gigInfo.date ? formatDateForDisplay(gigInfo.date) : "";
const pageUrl = window.location.href.split('#')[0];
const shareUrl = gigInfo.gigID
? `${pageUrl}#gig-${gigInfo.gigID}`
: pageUrl;
const shareText = `Check out this gig!\n\n${gigInfo.name}\n${gigInfo.venue}\n${dateFormatted}${timeFormatted ? ` at ${timeFormatted}` : ""}\n${gigInfo.address}\n\n${shareUrl}`;
const shareSubject = `Gig: ${gigInfo.name} at ${gigInfo.venue}`;
shareGigPreview.innerHTML = `You're sharing:
${gigInfo.name} at
${gigInfo.venue}`;
copyBtnText.textContent = "Copy Details";
copyBtnText.classList.remove("copy-confirmation");
shareBtnCopy.onclick = () => {
navigator.clipboard.writeText(shareText).then(() => {
copyBtnText.textContent = "Copied!";
copyBtnText.classList.add("copy-confirmation");
setTimeout(() => {
copyBtnText.textContent = "Copy Details";
copyBtnText.classList.remove("copy-confirmation");
}, 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
copyBtnText.textContent = "Failed!";
});
};
shareBtnEmail.href = `mailto:?subject=${encodeURIComponent(shareSubject)}&body=${encodeURIComponent(shareText)}`;
shareBtnSms.href = `sms:?body=${encodeURIComponent(shareText)}`;
if (navigator.share) {
shareBtnMore.style.display = 'flex';
shareBtnSms.classList.remove('span-full');
shareBtnMore.onclick = () => {
navigator.share({
title: shareSubject,
text: shareText,
url: shareUrl,
})
.then(() => console.log('Successful share'))
.catch((error) => console.log('Error sharing', error));
};
} else {
shareBtnMore.style.display = 'none';
shareBtnSms.classList.add('span-full');
}
shareModalOverlay.classList.add('visible');
}
function closeShareModal() {
shareModalOverlay.classList.remove('visible');
shareBtnEmail.href = "#";
shareBtnSms.href = "#";
}
shareModalOverlay.addEventListener('click', (event) => {
if (event.target === shareModalOverlay) {
closeShareModal();
}
});
shareModalCloseNub.addEventListener('click', closeShareModal);
// --- END SHARE MODAL LOGIC ---
// --- GIG DETAIL CARD LOGIC ---
const gigDetailOverlay = document.getElementById('gig-detail-overlay');
const gigDetailCard = document.getElementById('gig-detail-card');
const gigDetailCloseNub = document.getElementById('gig-detail-close-nub');
const gigDetailImage = document.getElementById('gig-detail-image');
const gigDetailName = document.getElementById('gig-detail-name');
const gigDetailDatetime = document.getElementById('gig-detail-datetime');
const gigDetailVenue = document.getElementById('gig-detail-venue');
const gigDetailAddress = document.getElementById('gig-detail-address');
const gigDetailPhone = document.getElementById('gig-detail-phone');
const gigDetailBandLink = document.getElementById('gig-detail-band-link');
const gigDetailVenueLink = document.getElementById('gig-detail-venue-link');
const gigDetailShareBtn = document.getElementById('gig-detail-share-btn');
function openGigDetailCard(gigInfo) {
gigDetailImage.src = gigInfo.imageUrl || DEFAULT_BAND_PICTURE;
gigDetailImage.alt = gigInfo.name;
gigDetailName.textContent = gigInfo.name;
const timeFormatted = gigInfo.time ? formatTime12h(gigInfo.time) : "Time not specified";
const dateFormatted = gigInfo.date ? formatDateForDisplay(gigInfo.date) : "Date not specified";
gigDetailDatetime.textContent = `π
${dateFormatted} at ${timeFormatted}`;
if (gigInfo.venue) {
gigDetailVenue.innerHTML = `π ${gigInfo.venueLink ? `
${gigInfo.venue}` : gigInfo.venue}`;
gigDetailVenue.style.display = 'flex';
} else {
gigDetailVenue.style.display = 'none';
}
if (gigInfo.address) {
gigDetailAddress.innerHTML = `πΊοΈ ${gigInfo.mapLink ? `
${gigInfo.address}` : gigInfo.address}`;
gigDetailAddress.style.display = 'flex';
} else {
gigDetailAddress.style.display = 'none';
}
if (gigInfo.phone) {
gigDetailPhone.textContent = `π ${gigInfo.phone}`;
gigDetailPhone.style.display = 'flex';
} else {
gigDetailPhone.style.display = 'none';
}
if (gigInfo.website) {
gigDetailBandLink.href = gigInfo.website;
gigDetailBandLink.style.display = 'block';
} else {
gigDetailBandLink.style.display = 'none';
}
if (gigInfo.venueLink) {
gigDetailVenueLink.href = gigInfo.venueLink;
gigDetailVenueLink.style.display = 'block';
} else {
gigDetailVenueLink.style.display = 'none';
}
gigDetailShareBtn.onclick = () => {
openShareModal(gigInfo);
};
const startDynamicAnimation = (img) => {
const { naturalWidth, naturalHeight } = img;
if (!naturalWidth || !naturalHeight) return;
const aspectRatio = naturalHeight / naturalWidth;
const containerWidth = gigDetailImage.clientWidth;
const containerAspectRatio = 250 / containerWidth;
if (aspectRatio > containerAspectRatio) {
const BASE_DURATION = 3;
const SCALING_FACTOR = 7;
let duration = BASE_DURATION + (aspectRatio * SCALING_FACTOR);
duration = Math.max(5, Math.min(22, duration));
img.style.animation = `pan-image ${duration.toFixed(2)}s ease-in-out infinite`;
} else {
img.style.animation = 'none';
img.style.objectPosition = 'center';
}
};
gigDetailImage.style.animation = 'none';
gigDetailImage.style.objectPosition = 'center';
if (gigDetailImage.complete && gigDetailImage.naturalHeight > 0) {
startDynamicAnimation(gigDetailImage);
} else {
gigDetailImage.onload = null;
gigDetailImage.onload = () => {
startDynamicAnimation(gigDetailImage);
};
}
gigDetailOverlay.classList.add('visible');
}
function closeGigDetailCard() {
gigDetailOverlay.classList.remove('visible');
gigDetailImage.style.animation = 'none';
gigDetailImage.onload = null;
}
gigDetailOverlay.addEventListener('click', (event) => {
if (event.target === gigDetailOverlay) {
closeGigDetailCard();
}
});
gigDetailCloseNub.addEventListener('click', closeGigDetailCard);
// --- END GIG DETAIL CARD LOGIC ---
function getMapFitBoundsPadding() {
if (isMobile()) {
return {
paddingTopLeft: [50, 300],
paddingBottomRight: [50, 80]
};
} else {
return {
paddingTopLeft: [340, 325],
paddingBottomRight: [225, 75]
};
}
}
function showEventsOnMap(eventMatches) {
unspiderfy();
markerLayer.clearLayers();
const successfulEvents = [];
for (const event of eventMatches) {
if (event.coords) {
event.marker = createEventMarker(event, false);
successfulEvents.push(event);
} else {
console.error(`Event "${event.name}" is missing coordinates.`);
}
}
clusterAndDisplay(successfulEvents);
if (markerLayer.getLayers().length > 0) {
const padding = getMapFitBoundsPadding();
map.fitBounds(markerLayer.getBounds(), { ...padding, maxZoom: 16 });
} else {
map.setView(defaultCenter, defaultZoom, { animate: false });
}
}
function clusterAndDisplay(events) {
markerLayer.clearLayers();
const CLUSTER_RADIUS_METERS = 15.24; // 50 feet is approx 15.24 meters
const clusteredEvents = [];
let remainingEvents = [...events];
while (remainingEvents.length > 0) {
const baseEvent = remainingEvents.shift();
const clusterGroup = [baseEvent];
const nextRemaining = [];
for (const otherEvent of remainingEvents) {
const distance = L.latLng(baseEvent.coords).distanceTo(L.latLng(otherEvent.coords));
if (distance <= CLUSTER_RADIUS_METERS) {
clusterGroup.push(otherEvent);
} else {
nextRemaining.push(otherEvent);
}
}
remainingEvents = nextRemaining;
clusteredEvents.push(clusterGroup);
}
for (const clusterGroup of clusteredEvents) {
if (clusterGroup.length > 1) {
markerLayer.addLayer(createClusterMarker(clusterGroup));
} else if (clusterGroup.length === 1) {
markerLayer.addLayer(clusterGroup[0].marker);
}
}
}
function createEventMarker(event, isImprecise) {
const timeFormatted = event.time ? formatTime12h(event.time) : "";
let eventNameHtml = `
${event.name}`;
const shareButtonHtml = `
`;
const headerHtml = ``;
const impreciseNotice = isImprecise ? `
π Location is approximate (town center)
` : "";
const confirmationNotice = ``;
const imageUrl = event.imageUrl || DEFAULT_BAND_PICTURE;
let imageHtml = `

`;
const popupContent = `${imageHtml}${headerHtml}
`;
const marker = L.marker(event.coords, { icon: singlePinIcon });
const showPopupAction = function(e) {
if (e) L.DomEvent.stopPropagation(e);
if (this._icon && L.DomUtil.hasClass(this._icon, 'marker-faded')) return;
cancelUnspiderfy();
clearTimeout(popupCloseTimer);
if (currentOpenPopup) closeCustomPopup();
showCustomPopup(popupContent, this.getLatLng(), event);
};
marker.on("mouseover", showPopupAction);
marker.on("click", showPopupAction);
marker.on("mouseout", () => {
popupCloseTimer = setTimeout(closeCustomPopup, 300);
if (!isMobile()) {
scheduleUnspiderfy();
}
});
return marker;
}
function createClusterMarker(clusterGroup) {
const center = clusterGroup[0].coords;
const clusterIcon = L.divIcon({
html: `
${clusterGroup.length}
`,
className: 'custom-cluster-pin',
iconSize: [40, 40],
iconAnchor: [20, 20]
});
const clusterMarker = L.marker(center, { icon: clusterIcon });
if (isMobile()) {
clusterMarker.on('click', (e) => {
L.DomEvent.stopPropagation(e);
cancelUnspiderfy();
spiderfy(clusterMarker, clusterGroup);
});
} else {
clusterMarker.on('mouseover', () => { cancelUnspiderfy(); spiderfy(clusterMarker, clusterGroup); });
clusterMarker.on('mouseout', () => { scheduleUnspiderfy(); });
}
return clusterMarker;
}
function spiderfy(clusterMarker, clusterGroup) {
if (currentSpiderfiedCluster && currentSpiderfiedCluster !== clusterMarker) unspiderfy();
if (currentSpiderfiedCluster === clusterMarker) return;
map.closePopup();
currentSpiderfiedCluster = clusterMarker;
markerLayer.removeLayer(clusterMarker);
const centerLatLng = clusterMarker.getLatLng();
markerLayer.eachLayer(layer => {
if (layer !== clusterMarker && centerLatLng.distanceTo(layer.getLatLng()) < 4828) {
if (layer._icon) L.DomUtil.addClass(layer._icon, 'marker-faded');
}
});
const centerPoint = map.latLngToLayerPoint(centerLatLng);
const count = clusterGroup.length;
const pinWidth = 34;
const pinPadding = isMobile() ? 10 : 15;
const totalWidth = (count * pinWidth) + ((count - 1) * pinPadding);
const startX = centerPoint.x - (totalWidth / 2);
const yOffset = 70;
clusterGroup.forEach((event, i) => {
const xPos = startX + (i * (pinWidth + pinPadding)) + (pinWidth / 2);
const yPos = centerPoint.y + yOffset;
const newLatLng = map.layerPointToLatLng(L.point(xPos, yPos));
const leg = L.polyline([centerLatLng, centerLatLng], { className: 'spider-leg', weight: 1.5 });
spiderfyLayer.addLayer(leg);
const marker = event.marker;
marker.setLatLng(centerLatLng).setOpacity(0);
spiderfyLayer.addLayer(marker);
setTimeout(() => {
marker.setLatLng(newLatLng).setOpacity(1);
leg.setLatLngs([centerLatLng, newLatLng]);
}, 10);
});
if (!isMobile()) {
spiderfyLayer.on('mouseover', cancelUnspiderfy).on('mouseout', scheduleUnspiderfy);
}
}
function unspiderfy() {
markerLayer.eachLayer(layer => { if (layer._icon) L.DomUtil.removeClass(layer._icon, 'marker-faded'); });
if (!currentSpiderfiedCluster) return;
const clusterToRestore = currentSpiderfiedCluster;
const centerLatLng = clusterToRestore.getLatLng();
spiderfyLayer.off();
currentSpiderfiedCluster = null;
const layersToRemove = [];
spiderfyLayer.eachLayer(layer => {
layersToRemove.push(layer);
if (layer instanceof L.Marker) layer.setLatLng(centerLatLng).setOpacity(0);
else if (layer instanceof L.Polyline) layer.setLatLngs([layer.getLatLngs()[0], centerLatLng]);
});
setTimeout(() => {
layersToRemove.forEach(layer => spiderfyLayer.removeLayer(layer));
if (!markerLayer.hasLayer(clusterToRestore)) markerLayer.addLayer(clusterToRestore);
}, 200);
}
function animateEventCount(newCount) {
clearTimeout(animationTimeout);
let currentDisplayCount = lastEventCount;
const animateDown = () => {
if (currentDisplayCount <= 0) {
currentDisplayCount = 0;
const bandText = newCount === 1 ? "Band" : "Bands";
eventCountDisplay.textContent = `${newCount} ${bandText} Tonight`;
animateUp();
return;
}
currentDisplayCount--;
const bandText = currentDisplayCount === 1 ? "Band" : "Bands";
eventCountDisplay.textContent = `${currentDisplayCount} ${bandText} Tonight`;
const downDuration = 150;
const stepTime = lastEventCount > 0 ? downDuration / lastEventCount : downDuration;
animationTimeout = setTimeout(animateDown, stepTime);
};
const animateUp = () => {
if (newCount === 0) {
eventCountDisplay.textContent = `0 Bands Tonight`;
lastEventCount = 0;
return;
}
if (currentDisplayCount >= newCount) {
lastEventCount = newCount;
const bandText = newCount === 1 ? "Band" : "Bands";
eventCountDisplay.textContent = `${newCount} ${bandText} Tonight`;
return;
}
currentDisplayCount++;
const bandText = currentDisplayCount === 1 ? "Band" : "Bands";
eventCountDisplay.textContent = `${currentDisplayCount} ${bandText} Tonight`;
const upDuration = 250;
const stepTime = newCount > 0 ? upDuration / newCount : upDuration;
animationTimeout = setTimeout(animateUp, stepTime);
};
if (lastEventCount > 0 && lastEventCount !== newCount) {
animateDown();
} else {
animateUp();
}
}
function preloadImages(urls) {
urls.forEach(url => {
const img = new Image();
img.src = url;
});
}
async function loadEventsForDate(dateString) {
selectedDate = dateString;
buildCalendar(currentYear, currentMonth);
loadingOverlay.style.opacity = '1';
loadingOverlay.style.display = "flex";
currentEvents = allEvents.filter((e) => e.date === selectedDate);
setTimeout(() => {
const imageUrls = new Set(
currentEvents
.map(event => event.imageUrl)
.filter(url => url && url !== DEFAULT_BAND_PICTURE)
);
if (imageUrls.size > 0) {
preloadImages([...imageUrls]);
}
}, 100);
const count = currentEvents.length;
animateEventCount(count);
showEventsOnMap(currentEvents);
loadingOverlay.style.transition = 'opacity 0.5s ease';
loadingOverlay.style.opacity = '0';
setTimeout(() => {
loadingOverlay.style.display = "none";
if (currentEvents.length === 0) {
setTimeout(() => {
notificationBanner.classList.add("show");
setTimeout(() => { notificationBanner.classList.remove("show"); }, 3000);
}, 500);
}
}, 500);
}
function buildCalendar(year, month) {
calendarGrid.innerHTML = "";
const dayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
dayLabels.forEach(label => { const el = document.createElement("div"); el.textContent = label; el.className = "day-label"; calendarGrid.appendChild(el); });
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
monthYear.textContent = `${new Date(year, month).toLocaleString("default", { month: "long" })} ${year}`;
for (let i = 0; i < firstDay; i++) {
const emptyCell = document.createElement("div");
emptyCell.setAttribute('aria-hidden', 'true');
calendarGrid.appendChild(emptyCell);
}
const today = new Date();
for (let day = 1; day <= daysInMonth; day++) {
const cell = document.createElement("button");
cell.className = "day";
cell.textContent = day;
const thisDate = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
cell.dataset.date = thisDate;
if (today.getDate() === day && today.getMonth() === month && today.getFullYear() === year) cell.classList.add("today");
if (selectedDate === thisDate) cell.classList.add("selected");
const fullDate = new Date(year, month, day);
cell.setAttribute('aria-label', fullDate.toLocaleString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }));
calendarGrid.appendChild(cell);
}
calendarGrid.onclick = (e) => {
if (e.target.classList.contains('day') && e.target.dataset.date) {
loadEventsForDate(e.target.dataset.date);
}
};
}
function setupNavigation() {
document.getElementById("prevMonth").onclick = () => { currentMonth--; if (currentMonth < 0) { currentMonth = 11; currentYear--; } buildCalendar(currentYear, currentMonth); };
document.getElementById("nextMonth").onclick = () => { currentMonth++; if (currentMonth > 11) { currentMonth = 0; currentYear++; } buildCalendar(currentYear, currentMonth); };
const toggleCalendar = () => {
if (isMobile()) {
const isOpen = calendarEl.classList.contains('open');
if (isOpen) {
// Close
calendarEl.classList.remove('open');
calendarOverlay.classList.remove('visible');
} else {
// Open
calendarEl.classList.add('open');
calendarOverlay.classList.add('visible');
}
}
};
// Update event listeners for the new mobile flow
if(mobileCalendarBtn) {
mobileCalendarBtn.onclick = toggleCalendar;
}
// Close nub listener
if(calendarCloseNub) {
calendarCloseNub.onclick = toggleCalendar;
}
// Overlay click listener
if(calendarOverlay) {
calendarOverlay.onclick = toggleCalendar;
}
// Keep desktop toggle working (though simplified)
eventCountDisplay.onclick = toggleCalendar;
document.getElementById('calendarNub').onclick = toggleCalendar;
document.getElementById('calendarNubContainer').onclick = toggleCalendar;
}
function debounce(func, wait) {
let timeout;
return function(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function refitMap() {
map.invalidateSize();
if (currentSpiderfiedCluster) return;
if (markerLayer.getLayers().length > 0) {
const padding = getMapFitBoundsPadding();
map.fitBounds(markerLayer.getBounds(), { ...padding, maxZoom: 16 });
}
}
function findBestImage(eventName, imageMap) {
if (!eventName) return DEFAULT_BAND_PICTURE;
const lowerCaseEventName = eventName.toLowerCase();
let bestMatchUrl = DEFAULT_BAND_PICTURE;
let longestMatchLength = 0;
for (const [bandName, imageUrl] of imageMap.entries()) {
if (lowerCaseEventName.includes(bandName) && bandName.length > longestMatchLength) {
bestMatchUrl = imageUrl;
longestMatchLength = bandName.length;
}
}
return bestMatchUrl;
}
async function fetchAndProcessData(sharedGigId = null) {
loadingOverlay.style.display = "flex";
try {
const [eventResponse, imageResponse] = await Promise.all([
fetch(WEB_APP_URL),
fetch(IMAGE_SCRIPT_URL)
]);
const imageData = await imageResponse.json();
if (imageData.error) {
console.error("Error from image script:", imageData.error);
throw new Error(imageData.error);
}
const imageMap = new Map();
for (const bandName in imageData) {
imageMap.set(bandName.toLowerCase(), imageData[bandName]);
}
const result = await eventResponse.json();
if (result.error) throw new Error(result.error);
allEvents = result.data.map(row => {
const idSourceString = `${row.name || ''}-${row.date || ''}-${row.venueName || ''}`;
return {
name: row.name || "",
address: row.address || "",
date: row.date || null,
time: row.time || "",
website: row.bandWebsite || "",
venue: row.venueName || "",
phone: row.venuePhone || "",
mapLink: row.mapLink || "",
venueLink: row.venueWebsite || "",
gigID: row.gigID || generateStableId(idSourceString),
coords: {
lat: parseFloat(row.lat),
lng: parseFloat(row.lng)
},
imageUrl: findBestImage(row.name, imageMap)
};
}).filter(e => e.coords && !isNaN(e.coords.lat) && !isNaN(e.coords.lng) && e.date);
let gigToOpen = null;
if (sharedGigId) {
gigToOpen = allEvents.find(event => event.gigID === sharedGigId);
}
if (gigToOpen) {
console.log("Found shared gig, opening details:", gigToOpen);
const [year, month, day] = gigToOpen.date.split('-').map(Number);
currentYear = year;
currentMonth = month - 1; // JS months are 0-indexed
setupNavigation();
await loadEventsForDate(gigToOpen.date);
openGigDetailCard(gigToOpen);
map.setView(gigToOpen.coords, 15);
} else {
if (sharedGigId) console.warn("Could not find gig with ID:", sharedGigId);
const today = new Date();
currentYear = today.getFullYear();
currentMonth = today.getMonth();
setupNavigation();
const todaysDateString = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
await loadEventsForDate(todaysDateString);
}
} catch (error) {
console.error("Error fetching or processing data:", error);
const loadingText = document.querySelector('#loadingOverlay .loading-text');
if (loadingText) {
loadingText.textContent = "Failed to load event data.";
loadingText.style.color = "#dc2626";
}
}
}
window.addEventListener('resize', debounce(refitMap, 250));
map.on('zoomend', () => {
unspiderfy();
if (currentEvents.length > 0) clusterAndDisplay(currentEvents.filter(e => e.coords && e.marker));
});
map.on('click', unspiderfy);
map.on('movestart', () => {
closeCustomPopup();
unspiderfy();
});
const hash = window.location.hash;
const match = hash.match(/^#gig-([a-zA-Z0-9_-]+)/);
const sharedGigId = match ? match[1] : null;
if (sharedGigId) {
console.log("Detected shared gig ID from URL hash:", sharedGigId);
}
fetchAndProcessData(sharedGigId);
// --- UNIFIED MENU LOGIC ---
const menuButton = document.getElementById('menu-button');
const hamburgerIcon = document.getElementById('hamburger-icon');
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
const mobileMenuPanel = document.getElementById('mobile-menu-panel');
const mobileMenuBackNub = document.getElementById('mobile-menu-back-nub');
function closeMobileMenu() {
mobileMenuOverlay.classList.remove('visible');
hamburgerIcon.classList.remove('open');
}
menuButton.addEventListener('click', (event) => {
event.stopPropagation();
hamburgerIcon.classList.toggle('open');
mobileMenuOverlay.classList.toggle('visible');
});
mobileMenuOverlay.addEventListener('click', (event) => {
if (event.target === mobileMenuOverlay) {
closeMobileMenu();
}
});
mobileMenuPanel.querySelectorAll('a').forEach(item => {
item.addEventListener('click', closeMobileMenu);
});
mobileMenuBackNub.addEventListener('click', (event) => {
event.stopPropagation();
closeMobileMenu();
});
};
window.addEventListener('DOMContentLoaded', initializeApp);