Step-by-step guide to adding commute time visualization to your job board platform. Complete with React and Mapbox integration examples.

Help job seekers find their perfect role by commute time. In this guide, I'll show you how to integrate commute time visualization into your job board, allowing users to filter jobs based on their preferred travel time from home.
We'll create a commute-aware job search that lets users:
First, get your API keys:
bashnpm install mapbox-gl @mapbox/mapbox-gl-geocoder
Here's a complete React component for job search with commute time filtering:
typescriptimport React, { useState, useRef, useEffect } from 'react'; import mapboxgl from 'mapbox-gl'; import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'; import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'; import 'mapbox-gl/dist/mapbox-gl.css'; interface Job { id: string; title: string; company: string; coordinates: [number, number]; // [longitude, latitude] salary: string; } interface CommuteJobMapProps { jobs: Job[]; onJobsFilter: (jobs: Job[]) => void; } const CommuteJobMap: React.FC<CommuteJobMapProps> = ({ jobs, onJobsFilter }) => { const mapContainer = useRef<HTMLDivElement>(null); const map = useRef<mapboxgl.Map | null>(null); const [loading, setLoading] = useState(false); const [settings, setSettings] = useState({ homeAddress: '', maxCommuteTime: 30, transportMode: 'driving' as const }); // Initialize map useEffect(() => { if (!mapContainer.current) return; mapboxgl.accessToken = process.env.MAPBOX_TOKEN; map.current = new mapboxgl.Map({ container: mapContainer.current, style: 'mapbox://styles/mapbox/light-v11', center: [-122.4194, 37.7749], zoom: 11 }); // Add geocoder (address search) const geocoder = new MapboxGeocoder({ accessToken: mapboxgl.accessToken, mapboxgl: mapboxgl, placeholder: 'Enter your home address' }); map.current.addControl(geocoder); // Listen for address selection geocoder.on('result', (e) => { setSettings(prev => ({ ...prev, homeAddress: e.result.place_name })); }); return () => { map.current?.remove(); }; }, []); // Add job markers useEffect(() => { if (!map.current) return; jobs.forEach(job => { const marker = new mapboxgl.Marker() .setLngLat(job.coordinates) .setPopup( new mapboxgl.Popup({ offset: 25 }).setHTML(` <h3 class="font-semibold">${job.title}</h3> <p>${job.company}</p> <p class="text-sm">${job.salary}</p> `) ) .addTo(map.current!); }); }, [jobs]); const fetchCommuteArea = async () => { try { setLoading(true); const response = await fetch('https://radiusmapper.com/api/search', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.RADIUS_MAPPER_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ address: settings.homeAddress, travelTimeMinutes: settings.maxCommuteTime, transportMode: settings.transportMode, outputFormat: 'geojson' }) }); const data = await response.json(); // Add/update the commute area on the map if (map.current?.getSource('commute-area')) { (map.current.getSource('commute-area') as mapboxgl.GeoJSONSource) .setData(data.boundaryGeojson); } else { map.current?.addSource('commute-area', { type: 'geojson', data: data.boundaryGeojson }); map.current?.addLayer({ id: 'commute-area', type: 'fill', source: 'commute-area', paint: { 'fill-color': '#3bb2d0', 'fill-opacity': 0.3, 'fill-outline-color': '#3bb2d0' } }); } // Filter jobs within commute area const jobsWithinCommute = jobs.filter(job => isPointInPolygon( job.coordinates, data.boundaryGeojson.features[0].geometry.coordinates[0] ) ); onJobsFilter(jobsWithinCommute); } catch (error) { console.error('Error fetching commute area:', error); } finally { setLoading(false); } }; return ( <div className="relative"> {/* Commute Settings Panel */} <div className="absolute top-4 right-4 z-10 bg-white p-4 rounded-lg shadow-lg w-80"> <h3 className="text-lg font-semibold mb-4">Commute Preferences</h3> <div className="space-y-4"> <div> <label className="block text-sm font-medium mb-1"> Max Commute Time: {settings.maxCommuteTime} minutes </label> <input type="range" min="5" max="60" value={settings.maxCommuteTime} onChange={(e) => setSettings(prev => ({ ...prev, maxCommuteTime: parseInt(e.target.value) }))} className="w-full" /> </div> <div> <label className="block text-sm font-medium mb-1"> Transport Mode </label> <select value={settings.transportMode} onChange={(e) => setSettings(prev => ({ ...prev, transportMode: e.target.value as typeof settings.transportMode }))} className="w-full p-2 border rounded" > <option value="driving">Driving</option> <option value="transit">Public Transit</option> <option value="cycling">Cycling</option> <option value="walking">Walking</option> </select> </div> <button onClick={fetchCommuteArea} disabled={loading || !settings.homeAddress} className="w-full bg-blue-500 text-white p-2 rounded disabled:opacity-50" > {loading ? 'Calculating...' : 'Update Commute Area'} </button> </div> </div> {/* Map Container */} <div ref={mapContainer} className="w-full h-[600px]" /> </div> ); }; // Usage in your job board app const JobBoard = () => { const [jobs, setJobs] = useState<Job[]>([]); const [filteredJobs, setFilteredJobs] = useState<Job[]>([]); return ( <div className="container mx-auto p-4"> <div className="grid lg:grid-cols-[1fr,400px] gap-8"> <CommuteJobMap jobs={jobs} onJobsFilter={setFilteredJobs} /> <div className="space-y-4"> <h2 className="text-xl font-semibold"> {filteredJobs.length} Jobs Within Commute Range </h2> {filteredJobs.map(job => ( <JobCard key={job.id} job={job} /> ))} </div> </div> </div> ); };
For larger job datasets, optimize the filtering:
typescript// Create a spatial index for faster job filtering import KDBush from 'kdbush'; const jobIndex = new KDBush(jobs.map(job => ({ ...job, coordinates: job.coordinates }))); const getJobsInBounds = (bounds: mapboxgl.LngLatBounds) => { return jobIndex.range( bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth() ).map(id => jobs[id]); };
Add these enhancements to improve the job search experience:
typescriptconst calculateCommutePreferences = (job: Job, commuteDetails: CommuteDetails) => { const yearlyCommuteCost = estimateCommuteCost(commuteDetails); const effectiveSalary = job.salary - yearlyCommuteCost; return { ...job, commuteDetails, effectiveSalary }; };
typescriptconst sortByCommutePreference = (jobs: Job[]) => { return jobs.sort((a, b) => { // Prioritize jobs with better commute/salary ratio const aScore = a.salary / a.commuteDetails.timeInMinutes; const bScore = b.salary / b.commuteDetails.timeInMinutes; return bScore - aScore; }); };
typescriptmap.current.addSource('jobs', { type: 'geojson', data: { type: 'FeatureCollection', features: jobs.map(job => ({ type: 'Feature', geometry: { type: 'Point', coordinates: job.coordinates }, properties: { id: job.id, title: job.title } })) }, cluster: true, clusterMaxZoom: 14, clusterRadius: 50 });
typescriptconst commuteCache = new Map<string, GeoJSON.FeatureCollection>(); const getCachedCommuteArea = async (settings: CommuteSettings) => { const key = JSON.stringify(settings); if (commuteCache.has(key)) { return commuteCache.get(key); } const result = await fetchCommuteArea(settings); commuteCache.set(key, result); return result; };
Consider adding these features:
If you prefer Google Maps over Mapbox, see our companion guide on building commute maps with Google Maps. For a no-code alternative, you can use RadiusMapper embeds to add interactive maps without any API integration.
Enhance your job board's commute features with:
Check out our API docs for implementation details!
Building a commute-aware job board? Let me know if you have questions! 💼