Service Locations Map
This example demonstrates an interactive map widget using Leaflet.js to display service locations with popups, navigation, and real-time updates.
Key Features
Interactive Leaflet.js map with zoom and pan controls
Custom markers for each service location
Information popups with service details and images
Navigation integration to service detail screens
Automatic map bounds fitting all service locations
Real-time updates when service data changes
Responsive design that works on all devices
Error handling with user-friendly messages
Use Cases
Perfect for applications requiring:
Service location directories and finders
Store locators and branch finders
Event venue mapping
Real estate property maps
Delivery service coverage areas
Field service management
Tourism and travel guides
Implementation Overview
This widget uses the Other Tables Data pattern, fetching data from a "Service Locations" or "Services" datatable. Key implementation features:
Leaflet.js integration for professional mapping
GeoJSON coordinate handling for location data
Popup content generation with images and details
Automatic map bounds calculation
Real-time data synchronization
Code Example
<!DOCTYPE html>
<html lang="en">
<head>
<title>Service Locations Map</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js"
crossorigin
></script>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js"
crossorigin
></script>
<script
src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"
crossorigin
></script>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<link
href="https://unpkg.com/[email protected]/dist/leaflet.css"
rel="stylesheet"
/>
<style>
html,
body,
#root {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
.map-container {
height: 100%;
width: 100%;
position: relative;
}
.map-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.map-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fee;
color: #c33;
padding: 1rem;
border-radius: 8px;
border: 1px solid #fcc;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
text-align: center;
max-width: 300px;
}
.popup-content {
display: flex;
gap: 10px;
align-items: flex-start;
}
.popup-content img {
width: 80px;
height: 60px;
object-fit: cover;
border-radius: 4px;
}
.popup-text {
flex: 1;
min-width: 0;
}
.popup-text h4 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
font-weight: 600;
}
.popup-text p {
margin: 0.25rem 0;
font-size: 0.9rem;
color: #666;
}
.popup-button {
margin-top: 0.5rem;
padding: 0.25rem 0.5rem;
background: #3273dc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.popup-button:hover {
background: #2366d1;
}
@media (max-width: 768px) {
.popup-content {
flex-direction: column;
}
.popup-content img {
width: 100%;
height: 120px;
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const buzzyFrameAPI = new BuzzyFrameAPI();
const MAP_CONFIG = {
tileLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
maxZoom: 19,
defaultZoom: 10,
defaultCenter: [40.7128, -74.006],
};
function ServiceLocationsMap() {
const { useState, useEffect, useRef } = React;
const [services, setServices] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [map, setMap] = useState(null);
const [markersLayer, setMarkersLayer] = useState(null);
const [serviceDetailScreenID, setServiceDetailScreenID] =
useState(null);
const [serviceDataTableID, setServiceDataTableID] = useState(null);
const mapRef = useRef(null);
useEffect(() => {
async function initialiseAll() {
try {
console.log('[init] Calling buzzyFrameAPI.initialise()...');
const init = await buzzyFrameAPI.initialise();
const { resourceJSON } = init || {};
const serviceDataTable = resourceJSON?.children?.find(
child =>
child.title === 'Service Locations' ||
child.title === 'Services',
);
if (serviceDataTable) {
setServiceDataTableID(serviceDataTable._id);
buzzyFrameAPI.addMicroappListener({
microAppID: serviceDataTable._id,
listener: async () => {
console.log(
'[listener] Service data changed, refreshing...',
);
await fetchServices();
},
});
await fetchServices();
} else {
setError(
'Service data table not found. Please ensure there is a table named "Service Locations" or "Services".',
);
setLoading(false);
}
try {
const detailScreenID = await buzzyFrameAPI.getScreenID({
screenName: 'Service Detail',
});
setServiceDetailScreenID(detailScreenID);
} catch (err) {
console.warn('Service detail screen not found:', err);
}
} catch (err) {
setError('Error during initialisation: ' + (err.message || err));
console.error('[error]', err);
setLoading(false);
}
}
initialiseAll();
}, []);
useEffect(() => {
if (mapRef.current && !map) {
try {
const newMap = L.map(mapRef.current, {
scrollWheelZoom: false,
zoomControl: true,
});
L.tileLayer(MAP_CONFIG.tileLayer, {
maxZoom: MAP_CONFIG.maxZoom,
attribution: '© OpenStreetMap contributors',
}).addTo(newMap);
const newMarkersLayer = L.layerGroup().addTo(newMap);
setMap(newMap);
setMarkersLayer(newMarkersLayer);
} catch (err) {
setError('Error initializing map: ' + (err.message || err));
console.error('[error]', err);
}
}
}, [mapRef.current]);
async function fetchServices() {
if (!serviceDataTableID) return;
setLoading(true);
setError('');
console.log('[fetchServices] Fetching services...');
try {
const serviceData = await buzzyFrameAPI.fetchDataTableRows({
microAppID: serviceDataTableID,
subLimit: 50,
});
const validServices = serviceData.filter(
service => service.location,
);
console.log(
'[fetchServices] Got',
validServices.length,
'valid services',
);
setServices(validServices);
} catch (err) {
setError('Error fetching services: ' + (err.message || err));
console.error('[error]', err);
} finally {
setLoading(false);
}
}
useEffect(() => {
if (map && markersLayer && services.length > 0) {
markersLayer.clearLayers();
const bounds = L.latLngBounds();
let hasValidMarkers = false;
services.forEach(service => {
try {
const location =
typeof service.location === 'string' && service.location
? JSON.parse(service.location)
: service.location || {};
const coordinates = location?.coordinates;
if (
coordinates &&
Array.isArray(coordinates) &&
coordinates.length >= 2
) {
const [lng, lat] = coordinates;
if (
typeof lat === 'number' &&
typeof lng === 'number' &&
lat >= -90 &&
lat <= 90 &&
lng >= -180 &&
lng <= 180
) {
const marker = L.marker([lat, lng]);
const popupContent = createPopupContent(service);
marker.bindPopup(popupContent);
marker.bindTooltip(service.name || 'Service Location', {
permanent: false,
direction: 'top',
});
marker.addTo(markersLayer);
bounds.extend([lat, lng]);
hasValidMarkers = true;
}
}
} catch (err) {
console.warn(
'Error processing service location:',
service.name,
err,
);
}
});
if (hasValidMarkers) {
map.fitBounds(bounds, { padding: [20, 20] });
} else {
map.setView(MAP_CONFIG.defaultCenter, MAP_CONFIG.defaultZoom);
}
}
}, [map, markersLayer, services]);
function createPopupContent(service) {
const div = document.createElement('div');
div.className = 'popup-content';
const image =
service.image && service.image.length > 0
? `<img src="${service.image[0]}" alt="${
service.name || 'Service'
}" />`
: '';
const description = service.description || service.notes || '';
const address = service.address || '';
div.innerHTML = `
${image}
<div class="popup-text">
<h4>${service.name || 'Unnamed Service'}</h4>
${description ? `<p>${description}</p>` : ''}
${address ? `<p><strong>Address:</strong> ${address}</p>` : ''}
${
serviceDetailScreenID
? '<button class="popup-button" onclick="openServiceDetails(\'' +
service._id +
'\')">View Details</button>'
: ''
}
</div>
`;
return div;
}
function openServiceDetails(serviceID) {
if (serviceDetailScreenID) {
buzzyFrameAPI.navigate({
screenID: serviceDetailScreenID,
rowID: serviceID,
});
}
}
window.openServiceDetails = openServiceDetails;
return (
<div className="map-container">
{loading && (
<div className="map-loading">
<span
className="material-symbols-outlined"
style={{ marginRight: '0.5rem' }}>
map
</span>
Loading service locations...
</div>
)}
{error && (
<div className="map-error">
<span
className="material-symbols-outlined"
style={{
display: 'block',
fontSize: '2rem',
marginBottom: '0.5rem',
}}>
error
</span>
{error}
</div>
)}
<div
ref={mapRef}
style={{ height: '100%', width: '100%' }}
role="application"
aria-label="Interactive map showing service locations"
/>
</div>
);
}
ReactDOM.render(<ServiceLocationsMap />, document.getElementById('root'));
</script>
</body>
</html>
Key Concepts
Leaflet.js Map Integration
Professional Mapping: Uses industry-standard Leaflet.js for reliable map functionality
Custom Markers: Places markers at service locations with tooltips
Interactive Popups: Rich popups with images, descriptions, and action buttons
Map Controls: Zoom controls and pan functionality built-in
Location Data Handling
GeoJSON Support: Handles standard GeoJSON coordinate format [longitude, latitude]
Data Validation: Validates coordinates to ensure they're within valid ranges
Error Handling: Graceful handling of invalid or missing location data
Flexible Data Sources: Works with "Service Locations" or "Services" tables
Advanced Map Features
Auto-fitting Bounds: Automatically zooms to show all service locations
Responsive Popups: Popups adapt to mobile and desktop layouts
Navigation Integration: Popup buttons navigate to service detail screens
Real-time Updates: Map updates immediately when service data changes
User Experience
Loading States: Clear loading indicators while data is fetched
Error Messages: User-friendly error messages for various failure scenarios
Mobile Optimization: Touch-friendly controls and responsive design
Accessibility: ARIA labels and keyboard navigation support
This map widget demonstrates how code widgets can integrate sophisticated mapping libraries to create professional location-based applications with rich interactivity and real-time data updates.
Last updated