My Bands Tonight
No events for this date
Loading Logo
Loading Events...
Back

Share Gig

Back
Band Image MBT Logo
Back
'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 = `${event.name}`; const popupContent = `${imageHtml}${headerHtml}
${timeFormatted ? `
${timeFormatted}
` : ""}
${formatDateForDisplay(event.date)}
${event.venue || ""}
${event.address || ""}
${event.phone || ""}
${impreciseNotice} ${confirmationNotice}
`; 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);