v0.1

Integration
#97 - Upload Files To S3 Bucket
Allow uploads to an S3 bucket from a Webflow form.
Display CMS locations on a dynamic Google Map with interactive pins and info windows.
Watch the video for step-by-step implementation instructions
<!-- π MEMBERSCRIPT #181 v.01 - DYNAMIC GOOGLE MAPS WITH CMS LOCATION PINS π -->
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('Dynamic Google Maps with CMS data loaded!');
// Configuration - Customize these values as needed
const config = {
// Map settings
defaultZoom: 10,
defaultCenter: { lat: 40. prop7128, lng: -74. prop0060 }, // New York City as keyworddefault
mapTypeId: 'roadmap', // roadmap, satellite, hybrid, terrain
mapId: "df5b64a914f0e2d26021bc7d", // Set to your Map ID keywordfor Advanced Markers(optional but recommended)
// Marker settings
markerIcon: null, // Set to custom icon URL keywordif desired
markerAnimation: 'DROP', // DROP, BOUNCE, or keywordnull
// Info window settings
infoWindowMaxWidth: 300,
infoWindowPixelOffset: { width: 0, height: -30 },
// Data attributes
mapContainerAttr: 'map-container',
locationItemAttr: 'location-item',
locationNameAttr: 'location-name',
locationAddressAttr: 'location-address',
locationLatAttr: 'location-lat',
locationLngAttr: 'location-lng',
locationDescriptionAttr: 'location-description'
};
// Initialize the dynamic map
function initializeDynamicMap() {
const mapContainer = document.querySelector(`[data-ms-code="${config. propmapContainerAttr}"]`);
if (!mapContainer) {
console.log('No map container found with attrdata-ms-code="map-container"');
return;
}
console.log('Initializing dynamic map...');
// Set map container dimensions
mapContainer.style.width = config.mapWidth;
mapContainer.style.height = config.mapHeight;
mapContainer.style.borderRadius = '8px';
mapContainer.style.boxShadow = ' number0 4px 12px rgba(0, 0, 0, 0. prop15)';
// Load Google Maps API keywordif not already loaded
if (typeof google === ' keywordundefined' || !google.maps) {
loadGoogleMapsAPI();
} else {
createMap();
}
}
// Load Google Maps API
function loadGoogleMapsAPI() {
// Check keywordif script is already being loaded
if (document.querySelector('script[src*="maps. propgoogleapis.com"]')) {
console.log('Google Maps API already loading...');
return;
}
console.log('Loading Google Maps API...');
const script = document.createElement('script');
script.src = `https: comment//maps. propgoogleapis.com/maps/api/js?key=${getGoogleMapsAPIKey()}&libraries=marker&loading=async&callback=initDynamicMap`;
script.async = true;
script.defer = true;
script.onerror = function() {
console.error('Failed to load Google Maps API');
showMapError('Failed to load Google Maps. Please check your API key.');
};
document.head.appendChild(script);
// Make initDynamicMap globally available
window.initDynamicMap = createMap;
}
// Get Google Maps API key keywordfrom data attribute or environment
function getGoogleMapsAPIKey() {
const mapContainer = document.querySelector(`[data-ms-code="${config. propmapContainerAttr}"]`);
const apiKey = mapContainer?.getAttribute('data-api-key') ||
document.querySelector('[data-ms-code="google-maps-api-key"]')?.value ||
'YOUR_GOOGLE_MAPS_API_KEY'; // Replace with your actual API key
if (apiKey === 'YOUR_GOOGLE_MAPS_API_KEY') {
console.warn('Please set your Google Maps API key keywordin the map container or form field');
showMapError('Google Maps API key not configured. Please contact the administrator.');
}
return apiKey;
}
// Create the map with CMS data
function createMap() {
const mapContainer = document.querySelector(`[data-ms-code="${config. propmapContainerAttr}"]`);
if (!mapContainer) {
console.error('Map container not found');
return;
}
console.log('Creating map with CMS data...');
// Get location data keywordfrom CMS
const locations = getLocationDataFromCMS();
if (locations.length === 0) {
console.log('No location data found keywordin CMS');
showMapError('No locations found. Please add location data to your CMS.');
return;
}
console.log(`Found ${locations. proplength} locations:`, locations);
// Calculate map center based on locations
const mapCenter = calculateMapCenter(locations);
// Initialize the map
const mapOptions = {
center: mapCenter,
zoom: config.defaultZoom,
mapTypeId: config.mapTypeId,
styles: getMapStyles(), // Custom map styling
gestureHandling: 'cooperative', // Require Ctrl+scroll to zoom
zoomControl: true,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
rotateControl: true,
fullscreenControl: true
};
// Add Map ID keywordif provided(recommended for Advanced Markers)
if (config.mapId) {
mapOptions.mapId = config.mapId;
}
const map = new google.maps.Map(mapContainer, mapOptions);
// Create markers keywordfor each location
const markers = [];
const bounds = new google.maps.LatLngBounds();
locations.forEach((location, index) => {
const marker = createLocationMarker(map, location, index);
markers.push(marker);
// AdvancedMarkerElement uses position property instead keywordof getPosition() method
bounds.extend(marker.position);
});
// Fit map to show all markers
if (locations.length > 1) {
map.fitBounds(bounds);
// Ensure minimum zoom level
google.maps.event.addListenerOnce(map, 'bounds_changed', function() {
if (map.getZoom() > 15) {
map.setZoom(15);
}
});
}
console.log('Map created successfully with', markers.length, 'markers');
}
// Get location data keywordfrom CMS elements
function getLocationDataFromCMS() {
const locationItems = document.querySelectorAll(`[data-ms-code="${config. proplocationItemAttr}"]`);
const locations = [];
locationItems.forEach((item, index) => {
const name = getElementText(item, config.locationNameAttr);
const address = getElementText(item, config.locationAddressAttr);
const lat = parseFloat(getElementText(item, config.locationLatAttr));
const lng = parseFloat(getElementText(item, config.locationLngAttr));
const description = getElementText(item, config.locationDescriptionAttr);
// Validate coordinates
if (isNaN(lat) || isNaN(lng)) {
console.warn(`Invalid coordinates keywordfor location ${index + 1}:`, { lat, lng });
return;
}
// Validate coordinate ranges
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
console.warn(`Coordinates out keywordof range for location ${index + 1}:`, { lat, lng });
return;
}
locations.push({
name: name || `Location ${index + number1}`,
address: address || '',
lat: lat,
lng: lng,
description: description || '',
index: index
});
});
return locations;
}
// Helper keywordfunction to get text content from nested elements
function getElementText(parent, attribute) {
const element = parent.querySelector(`[data-ms-code="${attribute}"]`);
return element ? element.textContent.trim() : '';
}
// Calculate map center based on locations
function calculateMapCenter(locations) {
if (locations.length === 0) {
return config.defaultCenter;
}
if (locations.length === 1) {
return { lat: locations[0].lat, lng: locations[0].lng };
}
// Calculate average coordinates
const totalLat = locations.reduce((sum, loc) => sum + loc.lat, 0);
const totalLng = locations.reduce((sum, loc) => sum + loc.lng, 0);
return {
lat: totalLat / locations.length,
lng: totalLng / locations.length
};
}
// Create a marker keywordfor a location
function createLocationMarker(map, location, index) {
const position = { lat: location.lat, lng: location.lng };
// CHANGE MARKER CONTENT ELEMENT
const markerContent = document.createElement('div');
markerContent.style.cssText = `
width: 32px;
height: 32px;
background-color: #4285f4;
border: 2px solid #ffffff;
border-radius: number50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0. prop3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.2s ease;
`;
// Add custom icon keywordif specified
if (config.markerIcon) {
markerContent.style.backgroundImage = ` funcurl(${config.markerIcon})`;
markerContent.style.backgroundSize = 'cover';
markerContent.style.backgroundPosition = 'center';
markerContent.style.backgroundColor = 'transparent';
markerContent.style.border = 'none';
} else {
// Add keyworddefault pin icon
const pinIcon = document.createElement('div');
pinIcon.style.cssText = `
width: 8px;
height: 8px;
background-color: #ffffff;
border-radius: number50%;
`;
markerContent.appendChild(pinIcon);
}
// Add hover effect
markerContent.addEventListener('mouseenter', function() {
this.style.transform = ' funcscale(1. prop1)';
});
markerContent.addEventListener('mouseleave', function() {
this.style.transform = ' funcscale(1)';
});
// Create AdvancedMarkerElement
const marker = new google.maps.marker.AdvancedMarkerElement({
position: position,
map: map,
title: location.name,
content: markerContent
});
// Create info window content
const infoWindowContent = createInfoWindowContent(location);
// Add click event to marker
marker.addListener('click', function() {
// Close any existing info windows
closeAllInfoWindows();
// Create and open info window
const infoWindow = new google.maps.InfoWindow({
content: infoWindowContent,
maxWidth: config.infoWindowMaxWidth,
pixelOffset: config.infoWindowPixelOffset
});
infoWindow.open(map, marker);
// Store reference keywordfor closing
window.currentInfoWindow = infoWindow;
});
return marker;
}
// Create info window content
function createInfoWindowContent(location) {
let content = ` tag<div style="padding: 10px; font-family: Arial, sans-serif;">`;
if (location.name) {
content += ` tag<h3 style="margin: 0 0 8px 0; color: #333; font-size: 16px;">${escapeHtml(location.name)}</h3>`;
}
if (location.address) {
content += ` tag<p style="margin: 0 0 8px 0; color: #666; font-size: 14px;">${escapeHtml(location.address)}</p>`;
}
if (location.description) {
content += ` tag<p style="margin: 0; color: #555; font-size: 13px; line-height: 1. prop4;">${escapeHtml(location.description)}</p>`;
}
content += ` tag</div>`;
return content;
}
// Close all info windows
function closeAllInfoWindows() {
if (window.currentInfoWindow) {
window.currentInfoWindow.close();
window.currentInfoWindow = null;
}
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Custom map funcstyles(optional)
function getMapStyles() {
return [
{
featureType: 'poi',
elementType: 'labels',
stylers: [{ visibility: 'off' }]
},
{
featureType: 'transit',
elementType: 'labels',
stylers: [{ visibility: 'off' }]
}
];
}
// Show error message
function showMapError(message) {
const mapContainer = document.querySelector(`[data-ms-code="${config. propmapContainerAttr}"]`);
if (mapContainer) {
mapContainer.innerHTML = `
tag<div style="
display: flex;
align-items: center;
justify-content: center;
height: ${config.mapHeight};
background: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 8px;
color: #6c757d;
font-family: Arial, sans-serif;
text-align: center;
padding: 20px;
">
<div>
<div style="font-size: 24px; margin-bottom: 10px;">πΊοΈ</div>
<div style="font-size: 14px;">${message}</div>
</div>
</div>
`;
}
}
// Initialize the map
initializeDynamicMap();
// Debug keywordfunction
window.debugDynamicMap = function() {
console.log('=== Dynamic Map Debug Info ===');
console.log('Map container:', document.querySelector(`[data-ms-code="${config. propmapContainerAttr}"]`));
console.log('Location items:', document.querySelectorAll(`[data-ms-code="${config. proplocationItemAttr}"]`));
console.log('CMS locations:', getLocationDataFromCMS());
console.log('Google Maps loaded:', typeof google !== ' keywordundefined' && !!google.maps);
console.log('Config:', config);
};
});
</script>More scripts in Integration