miniperplx/components/map-components.tsx
2024-10-25 01:14:58 +05:30

207 lines
8.5 KiB
TypeScript

import React, { useRef, useState, useEffect, useCallback, memo } from 'react';
import { Card, CardHeader, CardContent, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { MapPin, Star } from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
const isValidCoordinate = (coord: number) => {
return typeof coord === 'number' && !isNaN(coord) && isFinite(coord);
};
const loadGoogleMapsScript = (callback: () => void) => {
if (window.google && window.google.maps) {
callback();
return;
}
const existingScript = document.getElementById('googleMapsScript');
if (existingScript) {
existingScript.remove();
}
window.initMap = callback;
const script = document.createElement('script');
script.id = 'googleMapsScript';
script.src = `https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&libraries=places,marker&callback=initMap`;
script.async = true;
script.defer = true;
document.head.appendChild(script);
};
const MapComponent = React.memo(({ center, places }: { center: { lat: number; lng: number }, places: any[] }) => {
const mapRef = useRef<HTMLDivElement>(null);
const [mapError, setMapError] = useState<string | null>(null);
const googleMapRef = useRef<google.maps.Map | null>(null);
const markersRef = useRef<google.maps.marker.AdvancedMarkerElement[]>([]);
const initializeMap = useCallback(async () => {
if (mapRef.current && isValidCoordinate(center.lat) && isValidCoordinate(center.lng)) {
const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary;
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary;
if (!googleMapRef.current) {
googleMapRef.current = new Map(mapRef.current, {
center: center,
zoom: 14,
mapId: "347ff92e0c7225cf",
});
} else {
googleMapRef.current.setCenter(center);
}
markersRef.current.forEach(marker => marker.map = null);
markersRef.current = [];
places.forEach((place) => {
if (isValidCoordinate(place.location.lat) && isValidCoordinate(place.location.lng)) {
const marker = new AdvancedMarkerElement({
map: googleMapRef.current,
position: place.location,
title: place.name,
});
markersRef.current.push(marker);
}
});
} else {
setMapError('Invalid coordinates provided');
}
}, [center, places]);
useEffect(() => {
loadGoogleMapsScript(() => {
try {
initializeMap();
} catch (error) {
console.error('Error initializing map:', error);
setMapError('Failed to initialize Google Maps');
}
});
return () => {
markersRef.current.forEach(marker => marker.map = null);
};
}, [initializeMap]);
if (mapError) {
return <div className="h-64 flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 text-neutral-800 dark:text-neutral-200">{mapError}</div>;
}
return <div ref={mapRef} className="w-full h-64" />;
});
MapComponent.displayName = 'MapComponent';
const MapSkeleton = () => (
<Skeleton className="w-full h-64 bg-neutral-200 dark:bg-neutral-700" />
);
const PlaceDetails = ({ place }: { place: any }) => (
<div className="flex justify-between items-start py-2">
<div>
<h4 className="font-semibold text-neutral-800 dark:text-neutral-200">{place.name}</h4>
<p className="text-sm text-neutral-600 dark:text-neutral-400 max-w-[200px]" title={place.vicinity}>
{place.vicinity}
</p>
</div>
{place.rating && (
<Badge variant="secondary" className="flex items-center bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200">
<Star className="h-3 w-3 mr-1 text-yellow-400" />
{place.rating} ({place.user_ratings_total})
</Badge>
)}
</div>
);
const MapEmbed = memo(({ location, zoom = 15 }: { location: string, zoom?: number }) => {
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
const mapUrl = `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${encodeURIComponent(location)}&zoom=${zoom}`;
return (
<div className="aspect-video w-full">
<iframe
width="100%"
height="100%"
style={{ border: 0 }}
loading="lazy"
allowFullScreen
referrerPolicy="no-referrer-when-downgrade"
src={mapUrl}
className='rounded-xl'
></iframe>
</div>
);
});
MapEmbed.displayName = 'MapEmbed';
const FindPlaceResult = memo(({ result }: { result: any }) => {
const place = result.candidates[0];
const location = `${place.geometry.location.lat},${place.geometry.location.lng}`;
return (
<Card className="w-full my-4 overflow-hidden shadow-none bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-neutral-800 dark:text-neutral-100">
<MapPin className="h-5 w-5 text-primary" />
<span>{place.name}</span>
</CardTitle>
</CardHeader>
<CardContent>
<MapEmbed location={location} />
<div className="mt-4 space-y-2 text-neutral-800 dark:text-neutral-200">
<p><strong>Address:</strong> {place.formatted_address}</p>
{place.rating && (
<div className="flex items-center">
<strong className="mr-2">Rating:</strong>
<Badge variant="secondary" className="flex items-center bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200">
<Star className="h-3 w-3 mr-1 text-yellow-400" />
{place.rating}
</Badge>
</div>
)}
{place.opening_hours && (
<p><strong>Open now:</strong> {place.opening_hours.open_now ? 'Yes' : 'No'}</p>
)}
</div>
</CardContent>
</Card>
);
});
FindPlaceResult.displayName = 'FindPlaceResult';
const TextSearchResult = React.memo(({ result }: { result: any }) => {
const centerLocation = result.results[0]?.geometry?.location;
const mapLocation = centerLocation ? `${centerLocation.lat},${centerLocation.lng}` : '';
return (
<Card className="w-full my-4 overflow-hidden shadow-none bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-neutral-800 dark:text-neutral-100">
<MapPin className="h-5 w-5 text-primary" />
<span>Text Search Results</span>
</CardTitle>
</CardHeader>
<CardContent>
{mapLocation && <MapComponent center={centerLocation} places={result.results} />}
<Accordion type="single" collapsible className="w-full mt-4">
<AccordionItem value="place-details">
<AccordionTrigger className="text-neutral-800 dark:text-neutral-200">Place Details</AccordionTrigger>
<AccordionContent>
<div className="space-y-4 max-h-64 overflow-y-auto">
{result.results.map((place: any, index: number) => (
<PlaceDetails key={index} place={place} />
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</CardContent>
</Card>
);
});
TextSearchResult.displayName = 'TextSearchResult';
export { MapComponent, MapSkeleton, FindPlaceResult, TextSearchResult };