Grouping markers and changing pin to tear shape
This commit is contained in:
@@ -21,12 +21,6 @@ const ItemMarkerInfo: React.FC<ItemMarkerInfoProps> = ({ item, onViewDetails })
|
|||||||
return 'Contact for pricing';
|
return 'Contact for pricing';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLocationDisplay = () => {
|
|
||||||
return item.city && item.state
|
|
||||||
? `${item.city}, ${item.state}`
|
|
||||||
: 'Location not specified';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: 'min(280px, 90vw)', maxWidth: '280px' }}>
|
<div style={{ width: 'min(280px, 90vw)', maxWidth: '280px' }}>
|
||||||
<div className="card border-0">
|
<div className="card border-0">
|
||||||
@@ -55,18 +49,14 @@ const ItemMarkerInfo: React.FC<ItemMarkerInfoProps> = ({ item, onViewDetails })
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="card-body p-3">
|
<div className="card-body p-3">
|
||||||
<h6 className="card-title mb-2 text-dark fw-bold text-truncate">
|
<div className="card-title mb-2 text-dark fw-bold text-truncate" style={{ fontSize: '0.875rem' }}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</h6>
|
|
||||||
|
|
||||||
<div className="mb-2">
|
|
||||||
<span className="text-primary fw-bold">
|
|
||||||
{getPriceDisplay()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted small mb-2">
|
<div className="mb-2">
|
||||||
<i className="bi bi-geo-alt"></i> {getLocationDisplay()}
|
<span className="text-primary fw-semibold" style={{ fontSize: '0.8rem' }}>
|
||||||
|
{getPriceDisplay()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{item.description && (
|
{item.description && (
|
||||||
|
|||||||
@@ -59,53 +59,59 @@ const SearchResultsMap: React.FC<SearchResultsMapProps> = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Create marker for an item
|
// Group items by location
|
||||||
const createItemMarker = useCallback(
|
const groupItemsByLocation = useCallback((items: Item[]) => {
|
||||||
async (item: Item, map: google.maps.Map) => {
|
const groups: Map<string, Item[]> = new Map();
|
||||||
if (!item.latitude || !item.longitude) return null;
|
|
||||||
|
|
||||||
const { AdvancedMarkerElement } = (await google.maps.importLibrary(
|
items.forEach((item) => {
|
||||||
"marker"
|
if (item.latitude && item.longitude) {
|
||||||
)) as google.maps.MarkerLibrary;
|
const key = `${item.latitude},${item.longitude}`;
|
||||||
|
const existing = groups.get(key) || [];
|
||||||
|
existing.push(item);
|
||||||
|
groups.set(key, existing);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Create marker element
|
return groups;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Create marker for a group of items at the same location
|
||||||
|
const createLocationMarker = useCallback(
|
||||||
|
async (items: Item[], map: google.maps.Map) => {
|
||||||
|
if (items.length === 0 || !items[0].latitude || !items[0].longitude) return null;
|
||||||
|
|
||||||
|
const { AdvancedMarkerElement, PinElement } =
|
||||||
|
(await google.maps.importLibrary(
|
||||||
|
"marker"
|
||||||
|
)) as google.maps.MarkerLibrary;
|
||||||
|
|
||||||
|
const itemCount = items.length;
|
||||||
const isMobile = window.innerWidth < 768;
|
const isMobile = window.innerWidth < 768;
|
||||||
const markerSize = isMobile ? 48 : 40;
|
|
||||||
|
|
||||||
const markerElement = document.createElement("div");
|
// Create teardrop pin marker with badge for multiple items
|
||||||
markerElement.className =
|
const pin = new PinElement({
|
||||||
"d-flex align-items-center justify-content-center";
|
background: "#0d6efd",
|
||||||
markerElement.style.cssText = `
|
borderColor: "#ffffff",
|
||||||
width: ${markerSize}px;
|
glyphColor: "#ffffff",
|
||||||
height: ${markerSize}px;
|
scale: isMobile ? 1.2 : 1.0,
|
||||||
background-color: #0d6efd;
|
glyph: itemCount > 1 ? `${itemCount}` : undefined,
|
||||||
border: 3px solid white;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
touch-action: manipulation;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add icon
|
|
||||||
const icon = document.createElement("i");
|
|
||||||
icon.className = "bi bi-geo-fill text-white";
|
|
||||||
icon.style.fontSize = "16px";
|
|
||||||
markerElement.appendChild(icon);
|
|
||||||
|
|
||||||
// Hover effects
|
|
||||||
markerElement.addEventListener("mouseenter", () => {
|
|
||||||
markerElement.style.transform = "scale(1.1)";
|
|
||||||
});
|
});
|
||||||
markerElement.addEventListener("mouseleave", () => {
|
|
||||||
markerElement.style.transform = "scale(1)";
|
// Add hover effects
|
||||||
|
pin.element.style.cursor = "pointer";
|
||||||
|
pin.element.style.transition = "transform 0.2s ease";
|
||||||
|
pin.element.addEventListener("mouseenter", () => {
|
||||||
|
pin.element.style.transform = "scale(1.1)";
|
||||||
|
});
|
||||||
|
pin.element.addEventListener("mouseleave", () => {
|
||||||
|
pin.element.style.transform = "scale(1)";
|
||||||
});
|
});
|
||||||
|
|
||||||
const marker = new AdvancedMarkerElement({
|
const marker = new AdvancedMarkerElement({
|
||||||
position: { lat: item.latitude, lng: item.longitude },
|
position: { lat: items[0].latitude, lng: items[0].longitude },
|
||||||
map: map,
|
map: map,
|
||||||
content: markerElement,
|
content: pin.element,
|
||||||
title: item.name,
|
title: itemCount > 1 ? `${itemCount} items` : items[0].name,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add click listener
|
// Add click listener
|
||||||
@@ -118,27 +124,50 @@ const SearchResultsMap: React.FC<SearchResultsMapProps> = ({
|
|||||||
headerDisabled: true,
|
headerDisabled: true,
|
||||||
maxWidth:
|
maxWidth:
|
||||||
window.innerWidth < 768
|
window.innerWidth < 768
|
||||||
? Math.min(300, window.innerWidth - 40)
|
? Math.min(320, window.innerWidth - 40)
|
||||||
: 300,
|
: 320,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create React container for info window content
|
// Create React container for info window content
|
||||||
const infoContainer = document.createElement("div");
|
const infoContainer = document.createElement("div");
|
||||||
const root = createRoot(infoContainer);
|
const root = createRoot(infoContainer);
|
||||||
|
|
||||||
root.render(
|
if (itemCount === 1) {
|
||||||
<ItemMarkerInfo
|
// Single item - show normal info
|
||||||
item={item}
|
root.render(
|
||||||
onViewDetails={() => {
|
<ItemMarkerInfo
|
||||||
infoWindow.close();
|
item={items[0]}
|
||||||
if (onItemSelect) {
|
onViewDetails={() => {
|
||||||
onItemSelect(item);
|
infoWindow.close();
|
||||||
} else {
|
if (onItemSelect) {
|
||||||
window.location.href = `/items/${item.id}`;
|
onItemSelect(items[0]);
|
||||||
}
|
} else {
|
||||||
}}
|
window.location.href = `/items/${items[0].id}`;
|
||||||
/>
|
}
|
||||||
);
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Multiple items - show list
|
||||||
|
root.render(
|
||||||
|
<div style={{ width: 'min(300px, 85vw)', maxHeight: '350px', overflowY: 'auto' }}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<ItemMarkerInfo
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onViewDetails={() => {
|
||||||
|
infoWindow.close();
|
||||||
|
if (onItemSelect) {
|
||||||
|
onItemSelect(item);
|
||||||
|
} else {
|
||||||
|
window.location.href = `/items/${item.id}`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
infoWindow.setContent(infoContainer);
|
infoWindow.setContent(infoContainer);
|
||||||
infoWindow.open(map, marker);
|
infoWindow.open(map, marker);
|
||||||
@@ -150,23 +179,22 @@ const SearchResultsMap: React.FC<SearchResultsMapProps> = ({
|
|||||||
[onItemSelect]
|
[onItemSelect]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add markers for all items
|
// Add markers for all items (grouped by location)
|
||||||
const addMarkersToMap = useCallback(
|
const addMarkersToMap = useCallback(
|
||||||
async (map: google.maps.Map, items: Item[]) => {
|
async (map: google.maps.Map, items: Item[]) => {
|
||||||
clearMarkers();
|
clearMarkers();
|
||||||
|
|
||||||
const validItems = items.filter(
|
const locationGroups = groupItemsByLocation(items);
|
||||||
(item) => item.latitude && item.longitude
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const item of validItems) {
|
const groupsArray = Array.from(locationGroups.values());
|
||||||
const marker = await createItemMarker(item, map);
|
for (const groupItems of groupsArray) {
|
||||||
|
const marker = await createLocationMarker(groupItems, map);
|
||||||
if (marker) {
|
if (marker) {
|
||||||
markersRef.current.push(marker);
|
markersRef.current.push(marker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[createItemMarker, clearMarkers]
|
[createLocationMarker, clearMarkers, groupItemsByLocation]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate map bounds to fit all markers with appropriate zoom levels
|
// Calculate map bounds to fit all markers with appropriate zoom levels
|
||||||
|
|||||||
Reference in New Issue
Block a user