diff --git a/src/lib/components/NewMemoryPopup.svelte b/src/lib/components/NewMemoryPopup.svelte index 2625cfc..b7323de 100644 --- a/src/lib/components/NewMemoryPopup.svelte +++ b/src/lib/components/NewMemoryPopup.svelte @@ -255,8 +255,6 @@ {#if showImageError} diff --git a/src/lib/components/WorldMap.svelte b/src/lib/components/WorldMap.svelte index 7a764be..5c4073a 100644 --- a/src/lib/components/WorldMap.svelte +++ b/src/lib/components/WorldMap.svelte @@ -15,40 +15,46 @@ lat: number; lng: number; }; + startDate: string; + endDate: string; } - async function getPastTripLocations(): Promise { + function formatDate(dateStr: string): string { + const date = new Date(dateStr); + return `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1) + .toString() + .padStart(2, '0')}.${date.getFullYear()}`; + } + + async function getPastTripLocations(): Promise> { try { const tripsRef = ref(db, 'trips'); const snapshot = await get(tripsRef); - - if (!snapshot.exists()) return []; + if (!snapshot.exists()) return {}; - // Get today's date at midnight for comparison const today = new Date(); today.setHours(0, 0, 0, 0); - // Create a Set to store unique locations - const uniqueLocations = new Map(); + const locationMap: Record = {}; - // Filter past trips and extract unique destinations Object.values(snapshot.val()).forEach((trip: any) => { const endDate = new Date(trip.endDate); if (endDate < today && trip.destination?.location) { - const locationKey = `${trip.destination.location.lat},${trip.destination.location.lng}`; - if (!uniqueLocations.has(locationKey)) { - uniqueLocations.set(locationKey, { - name: trip.destination.name, - location: trip.destination.location - }); - } + const key = `${trip.destination.location.lat},${trip.destination.location.lng}`; + if (!locationMap[key]) locationMap[key] = []; + locationMap[key].push({ + name: trip.destination.name, + location: trip.destination.location, + startDate: trip.startDate, + endDate: trip.endDate + }); } }); - return Array.from(uniqueLocations.values()); + return locationMap; } catch (error) { console.error('Error fetching past trips:', error); - return []; + return {}; } } @@ -63,7 +69,6 @@ const width = mapContainer.clientWidth; const height = mapContainer.clientHeight; - // Create SVG const svg = d3.select(mapContainer) .append('svg') .attr('width', '100%') @@ -71,30 +76,24 @@ .attr('viewBox', `0 0 ${width} ${height}`) .attr('preserveAspectRatio', 'xMidYMid meet') as d3.Selection; - // Add a group for all map elements that will be transformed const g = svg.append('g'); - // Create projection const projection = d3.geoMercator() .scale(width / (2 * Math.PI)) - .translate([width / 2, height / 1.6]); // position the map, horizontally centered but is slighty upward + .translate([width / 2, height / 1.6]); const path = d3.geoPath().projection(projection); try { - // Get past trip locations - const pastLocations = await getPastTripLocations(); + const locationMap = await getPastTripLocations(); if (!mounted) return; - // Load world map data const response = await fetch('https://unpkg.com/world-atlas@2/countries-110m.json'); if (!mounted) return; const world = await response.json(); - - // Convert TopoJSON to GeoJSON + const countries = feature(world, world.objects.countries) as any; - // Draw the map g.append('g') .selectAll('path') .data(countries.features) @@ -106,7 +105,8 @@ .attr('stroke', Colors.gray.light50) .attr('stroke-width', '0.5'); - // Add markers for past trip locations + const pastLocations = Object.values(locationMap).map(trips => trips[0]); + g.selectAll('circle') .data(pastLocations) .enter() @@ -116,8 +116,69 @@ .attr('r', 5) .attr('class', 'marker') .attr('fill', Colors.planner.med400) + .on('click', function (event, d) { + d3.selectAll('.trip-label').remove(); + d3.selectAll('.trip-marker').remove(); + d3.selectAll('.trip-line').remove(); + event.stopPropagation(); + + const key = `${d.location.lat},${d.location.lng}`; + const trips = locationMap[key]; + + if (trips && trips.length > 0) { + const baseX = projection([d.location.lng, d.location.lat])![0]; + const baseY = projection([d.location.lng, d.location.lat])![1]; + + trips.sort((a, b) => new Date(a.endDate).getTime() - new Date(b.endDate).getTime()); + + if (trips.length > 1) { + g.append('line') + .attr('x1', baseX) + .attr('y1', baseY + 5) + .attr('x2', baseX) + .attr('y2', baseY - (trips.length - 1) * 24 + 6) + .attr('stroke', Colors.planner.med400) + .attr('stroke-width', 2) + .attr('class', 'trip-line'); + } + + trips.forEach((trip, idx) => { + const offsetY = idx * 24; + + const markerGroup = g.append('g') + .attr('class', 'trip-group') + .on('mouseover', function () { + d3.select(this).select('circle').transition().duration(200).attr('r', 7); + d3.select(this).select('text').transition().duration(200).attr('fill', Colors.planner.med400); + }) + .on('mouseout', function () { + d3.select(this).select('circle').transition().duration(200).attr('r', 5); + d3.select(this).select('text').transition().duration(200).attr('fill', Colors.black); + }); + + markerGroup.append('circle') + .attr('cx', baseX) + .attr('cy', baseY - offsetY) + .attr('r', 5) + .attr('fill', Colors.planner.med400) + .attr('class', 'trip-marker'); + + markerGroup.append('text') + .attr('x', baseX + 10) + .attr('y', baseY - offsetY + 4) + .attr('font-size', '12px') + .attr('class', 'trip-label') + .text(`${formatDate(trip.startDate)} - ${formatDate(trip.endDate)}`); + }); + } + }); + + svg.on('click', () => { + d3.selectAll('.trip-label').remove(); + d3.selectAll('.trip-marker').remove(); + d3.selectAll('.trip-line').remove(); + }); - // Add zoom behavior const zoom = d3.zoom() .scaleExtent([1, 8]) .on('zoom', (event) => { @@ -168,24 +229,31 @@ } :global(.marker:hover) { - r: 8; + r: 7; cursor: pointer; } + :global(.trip-label), + :global(.trip-marker), + :global(.trip-line) { + transition: all 0.2s ease; + pointer-events: all; + } + :global(.dark .map-wrapper) { background-color: var(--black); } :global(.dark .country) { - fill: #121212; - stroke: var(--gray-700); + fill: #121212; + stroke: var(--gray-700); } :global(.dark .country:hover) { - fill: #5a1c05; + fill: #5a1c05; } :global(.dark .marker) { - fill: var(--memory-500); + fill: var(--memory-500); } - \ No newline at end of file +