Edit app-9xzmfic2e4g1/src/components/trip/Map.tsx via Editor
This commit is contained in:
parent
fc0cb36f89
commit
78b491dc8d
@ -1,9 +1,8 @@
|
||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { ItineraryDay } from '@/db/api';
|
||||
import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
||||
import { ItineraryDay, Place } from '@/db/api';
|
||||
import { initGoogleMaps } from '@/lib/google-maps-loader';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ZoomIn, ZoomOut, Maximize2, Navigation2, Map as MapIcon, Layers } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ZoomIn, ZoomOut, Maximize2, Plus, Loader2 } from 'lucide-react';
|
||||
import api from '@/db/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@ -11,31 +10,33 @@ interface MapProps {
|
||||
itinerary: { days: ItineraryDay[] };
|
||||
activePlaceId: string | null;
|
||||
onMarkerClick: (id: string) => void;
|
||||
onAddPlace?: (place: Place) => void; // yeni: haritadan ekleme
|
||||
}
|
||||
|
||||
// More standard and vibrant color palette for Wanderlog feel
|
||||
const DAY_COLORS = [
|
||||
'#EA580C', // Orange-600
|
||||
'#2563EB', // Blue-600
|
||||
'#059669', // Emerald-600
|
||||
'#7C3AED', // Violet-600
|
||||
'#DB2777', // Pink-600
|
||||
'#EA580C',
|
||||
'#2563EB',
|
||||
'#059669',
|
||||
'#7C3AED',
|
||||
'#DB2777',
|
||||
];
|
||||
|
||||
export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
|
||||
export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: MapProps) {
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const [googleMap, setGoogleMap] = useState<google.maps.Map | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [mapType, setMapType] = useState<'roadmap' | 'satellite' | 'terrain'>('roadmap');
|
||||
const markersRef = useRef<{ [key: string]: { marker: google.maps.Marker; dayIndex: number } }>({});
|
||||
const polylinesRef = useRef<google.maps.Polyline[]>([]);
|
||||
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null);
|
||||
const placesServiceRef = useRef<google.maps.places.PlacesService | null>(null);
|
||||
const [addingPlaceId, setAddingPlaceId] = useState<string | null>(null);
|
||||
|
||||
const itineraryKey = useMemo(() =>
|
||||
const itineraryKey = useMemo(() =>
|
||||
JSON.stringify(itinerary.days.map(d => d.items.map(i => i.place_id))),
|
||||
[itinerary]
|
||||
);
|
||||
|
||||
// ── Map init ──────────────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
const loadMap = async () => {
|
||||
try {
|
||||
@ -44,47 +45,135 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
|
||||
setError('Google Maps API anahtarı eksik.');
|
||||
return;
|
||||
}
|
||||
|
||||
await initGoogleMaps(apiKey);
|
||||
|
||||
|
||||
if (mapRef.current && !googleMap) {
|
||||
const map = new google.maps.Map(mapRef.current, {
|
||||
center: { lat: 38.6431, lng: 34.8347 },
|
||||
zoom: 12,
|
||||
styles: [
|
||||
// Standard clean map style
|
||||
{
|
||||
"featureType": "poi.business",
|
||||
"stylers": [{ "visibility": "off" }]
|
||||
},
|
||||
{
|
||||
"featureType": "poi.park",
|
||||
"elementType": "labels.text",
|
||||
"stylers": [{ "visibility": "on" }]
|
||||
}
|
||||
{ featureType: 'poi.business', stylers: [{ visibility: 'off' }] },
|
||||
{ featureType: 'poi.park', elementType: 'labels.text', stylers: [{ visibility: 'on' }] },
|
||||
],
|
||||
mapTypeControl: false,
|
||||
streetViewControl: false,
|
||||
fullscreenControl: false,
|
||||
zoomControl: false,
|
||||
clickableIcons: true,
|
||||
clickableIcons: true, // POI'lere tıklanabilir
|
||||
});
|
||||
setGoogleMap(map);
|
||||
|
||||
infoWindowRef.current = new google.maps.InfoWindow();
|
||||
placesServiceRef.current = new google.maps.places.PlacesService(map);
|
||||
|
||||
// ── POI tıklama ───────────────────────────────────────────────────
|
||||
if (onAddPlace) {
|
||||
map.addListener('click', (e: google.maps.MapMouseEvent & { placeId?: string }) => {
|
||||
if (!e.placeId) return; // sadece POI tıklamalarını yakala
|
||||
e.stop?.(); // varsayılan Google bilgi penceresini engelle
|
||||
|
||||
const placeId = e.placeId;
|
||||
infoWindowRef.current?.close();
|
||||
|
||||
// Önce "yükleniyor" info window göster
|
||||
const loadingContent = `
|
||||
<div style="padding:12px;min-width:180px;font-family:sans-serif;display:flex;align-items:center;gap:8px;">
|
||||
<div style="width:16px;height:16px;border:2px solid #EA580C;border-top-color:transparent;border-radius:50%;animation:spin 0.7s linear infinite;"></div>
|
||||
<span style="font-size:13px;color:#6B7280;">Yer bilgisi yükleniyor...</span>
|
||||
</div>
|
||||
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
|
||||
`;
|
||||
infoWindowRef.current?.setContent(loadingContent);
|
||||
infoWindowRef.current?.setPosition(e.latLng!);
|
||||
infoWindowRef.current?.open(map);
|
||||
|
||||
// Places Details çek
|
||||
placesServiceRef.current?.getDetails(
|
||||
{ placeId, fields: ['place_id', 'name', 'formatted_address', 'geometry', 'rating', 'photos', 'types'] },
|
||||
(place, status) => {
|
||||
if (status !== google.maps.places.PlacesServiceStatus.OK || !place || !place.geometry?.location) {
|
||||
infoWindowRef.current?.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const photoUrl = place.photos?.[0]?.getUrl({ maxWidth: 400 }) || '';
|
||||
const btnId = `map-add-btn-${placeId}`;
|
||||
|
||||
const content = `
|
||||
<div style="font-family:sans-serif;min-width:220px;max-width:260px;overflow:hidden;border-radius:8px;">
|
||||
${photoUrl ? `
|
||||
<div style="width:100%;height:110px;overflow:hidden;margin-bottom:0;">
|
||||
<img src="${photoUrl}" style="width:100%;height:100%;object-fit:cover;" />
|
||||
</div>
|
||||
` : ''}
|
||||
<div style="padding:10px 12px 12px;">
|
||||
<p style="margin:0 0 2px;font-size:14px;font-weight:800;color:#111827;line-height:1.3;">${place.name}</p>
|
||||
<p style="margin:0 0 8px;font-size:11px;color:#9CA3AF;">${place.formatted_address || ''}</p>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
|
||||
${place.rating ? `<span style="font-size:11px;font-weight:700;color:#F59E0B;">★ ${place.rating}</span>` : '<span></span>'}
|
||||
<button id="${btnId}"
|
||||
style="display:flex;align-items:center;gap:4px;background:#EA580C;color:white;border:none;border-radius:8px;padding:6px 12px;font-size:11px;font-weight:800;cursor:pointer;letter-spacing:0.05em;text-transform:uppercase;">
|
||||
+ Plana Ekle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
infoWindowRef.current?.setContent(content);
|
||||
|
||||
// Buton tıklamasını DOM'dan yakala
|
||||
google.maps.event.addListenerOnce(infoWindowRef.current!, 'domready', () => {
|
||||
const btn = document.getElementById(btnId);
|
||||
if (!btn) return;
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const newPlace: Place = {
|
||||
place_id: place.place_id || `map-${Date.now()}`,
|
||||
name: place.name || '',
|
||||
lat: place.geometry!.location!.lat(),
|
||||
lng: place.geometry!.location!.lng(),
|
||||
rating: place.rating,
|
||||
formatted_address: place.formatted_address || '',
|
||||
photo_reference: photoUrl,
|
||||
description: place.types?.join(', ') || 'Turistik Nokta',
|
||||
category: (place.types?.[0] || 'point_of_interest').replace(/_/g, ' '),
|
||||
estimated_duration_minutes: 60,
|
||||
start_time: '09:00',
|
||||
end_time: '10:00',
|
||||
};
|
||||
|
||||
// Buton feedback
|
||||
btn.textContent = '✓ Eklendi!';
|
||||
btn.style.background = '#059669';
|
||||
btn.style.pointerEvents = 'none';
|
||||
|
||||
onAddPlace(newPlace);
|
||||
|
||||
setTimeout(() => infoWindowRef.current?.close(), 1200);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setGoogleMap(map);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setError('Google Maps yüklenemedi.');
|
||||
}
|
||||
};
|
||||
if (!googleMap) loadMap();
|
||||
}, [googleMap]);
|
||||
|
||||
if (!googleMap) loadMap();
|
||||
}, [googleMap, onAddPlace]);
|
||||
|
||||
// ── Markers & polylines ───────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (!googleMap || !itinerary?.days) return;
|
||||
|
||||
Object.values(markersRef.current).forEach(({ marker }) => marker.setMap(null));
|
||||
markersRef.current = {};
|
||||
polylinesRef.current.forEach(polyline => polyline.setMap(null));
|
||||
polylinesRef.current.forEach(p => p.setMap(null));
|
||||
polylinesRef.current = [];
|
||||
|
||||
const bounds = new google.maps.LatLngBounds();
|
||||
@ -111,7 +200,7 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
|
||||
fontWeight: '900',
|
||||
},
|
||||
icon: {
|
||||
path: "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z",
|
||||
path: 'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z',
|
||||
fillColor: dayColor,
|
||||
fillOpacity: 1,
|
||||
strokeWeight: 2,
|
||||
@ -125,24 +214,23 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
|
||||
|
||||
marker.addListener('click', () => {
|
||||
onMarkerClick(item.place_id);
|
||||
if (infoWindowRef.current) {
|
||||
const photoUrl = item.photo_reference ? (
|
||||
item.photo_reference.startsWith('http') ? item.photo_reference : api.getPhotoUrl(item.photo_reference)
|
||||
) : '';
|
||||
|
||||
infoWindowRef.current.setContent(`
|
||||
<div style="padding: 8px; min-width: 200px; font-family: sans-serif;">
|
||||
${photoUrl ? `<img src="${photoUrl}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 8px; margin-bottom: 8px;" />` : ''}
|
||||
<h4 style="margin: 0 0 2px 0; font-size: 14px; font-weight: bold;">${item.name}</h4>
|
||||
<p style="margin: 0 0 8px 0; font-size: 11px; color: #6B7280;">${item.category}</p>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<span style="font-size: 11px; font-weight: bold;">★ ${item.rating || 'N/A'}</span>
|
||||
<a href="https://www.google.com/maps/dir/?api=1&destination=${item.lat},${item.lng}" target="_blank" style="color: #EA580C; font-size: 11px; font-weight: bold; text-decoration: none;">Yol Tarifi</a>
|
||||
</div>
|
||||
const photoUrl = item.photo_reference
|
||||
? (item.photo_reference.startsWith('http') ? item.photo_reference : api.getPhotoUrl(item.photo_reference))
|
||||
: '';
|
||||
|
||||
infoWindowRef.current?.setContent(`
|
||||
<div style="padding:8px;min-width:200px;font-family:sans-serif;">
|
||||
${photoUrl ? `<img src="${photoUrl}" style="width:100%;height:100px;object-fit:cover;border-radius:8px;margin-bottom:8px;" />` : ''}
|
||||
<h4 style="margin:0 0 2px;font-size:14px;font-weight:bold;">${item.name}</h4>
|
||||
<p style="margin:0 0 8px;font-size:11px;color:#6B7280;">${item.category}</p>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<span style="font-size:11px;font-weight:bold;">★ ${item.rating || 'N/A'}</span>
|
||||
<a href="https://www.google.com/maps/dir/?api=1&destination=${item.lat},${item.lng}" target="_blank"
|
||||
style="color:#EA580C;font-size:11px;font-weight:bold;text-decoration:none;">Yol Tarifi</a>
|
||||
</div>
|
||||
`);
|
||||
infoWindowRef.current.open(googleMap, marker);
|
||||
}
|
||||
</div>
|
||||
`);
|
||||
infoWindowRef.current?.open(googleMap, marker);
|
||||
});
|
||||
|
||||
markersRef.current[item.place_id] = { marker, dayIndex };
|
||||
@ -167,30 +255,30 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
|
||||
}
|
||||
}, [googleMap, itineraryKey]);
|
||||
|
||||
// ── Active marker highlight ───────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (googleMap && activePlaceId && markersRef.current[activePlaceId]) {
|
||||
const { marker, dayIndex } = markersRef.current[activePlaceId];
|
||||
googleMap.panTo(marker.getPosition()!);
|
||||
|
||||
Object.keys(markersRef.current).forEach(id => {
|
||||
const { marker: m, dayIndex: dIdx } = markersRef.current[id];
|
||||
const color = DAY_COLORS[dIdx % DAY_COLORS.length];
|
||||
const isActive = id === activePlaceId;
|
||||
|
||||
m.setIcon({
|
||||
path: "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z",
|
||||
fillColor: color,
|
||||
fillOpacity: 1,
|
||||
strokeWeight: isActive ? 3 : 2,
|
||||
strokeColor: isActive ? '#000000' : '#ffffff',
|
||||
scale: isActive ? 1.8 : 1.4,
|
||||
anchor: new google.maps.Point(12, 22),
|
||||
labelOrigin: new google.maps.Point(12, 9),
|
||||
});
|
||||
m.setZIndex(isActive ? 1000 : 1);
|
||||
m.setAnimation(isActive ? google.maps.Animation.BOUNCE : null);
|
||||
if (!googleMap || !activePlaceId || !markersRef.current[activePlaceId]) return;
|
||||
|
||||
const { marker } = markersRef.current[activePlaceId];
|
||||
googleMap.panTo(marker.getPosition()!);
|
||||
|
||||
Object.keys(markersRef.current).forEach(id => {
|
||||
const { marker: m, dayIndex: dIdx } = markersRef.current[id];
|
||||
const color = DAY_COLORS[dIdx % DAY_COLORS.length];
|
||||
const isActive = id === activePlaceId;
|
||||
m.setIcon({
|
||||
path: 'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z',
|
||||
fillColor: color,
|
||||
fillOpacity: 1,
|
||||
strokeWeight: isActive ? 3 : 2,
|
||||
strokeColor: isActive ? '#000000' : '#ffffff',
|
||||
scale: isActive ? 1.8 : 1.4,
|
||||
anchor: new google.maps.Point(12, 22),
|
||||
labelOrigin: new google.maps.Point(12, 9),
|
||||
});
|
||||
}
|
||||
m.setZIndex(isActive ? 1000 : 1);
|
||||
m.setAnimation(isActive ? google.maps.Animation.BOUNCE : null);
|
||||
});
|
||||
}, [activePlaceId, googleMap]);
|
||||
|
||||
return (
|
||||
@ -202,25 +290,37 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
|
||||
) : (
|
||||
<>
|
||||
<div ref={mapRef} className="absolute inset-0 w-full h-full" />
|
||||
|
||||
{/* Compact Map Controls */}
|
||||
|
||||
{/* Haritadan ekleme ipucu */}
|
||||
{onAddPlace && (
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 pointer-events-none">
|
||||
<div className="bg-white/90 backdrop-blur-md px-4 py-2 rounded-full shadow-lg border border-white/60 flex items-center gap-2">
|
||||
<Plus className="h-3.5 w-3.5 text-orange-500 shrink-0" />
|
||||
<span className="text-[11px] font-bold text-gray-600">Haritada bir yere tıklayarak plana ekleyin</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zoom controls */}
|
||||
<div className="absolute top-4 right-4 flex flex-col gap-2">
|
||||
<div className="flex flex-col bg-white border rounded-lg shadow-sm overflow-hidden">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-none border-b" onClick={() => googleMap?.setZoom((googleMap.getZoom() || 12) + 1)}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-none border-b"
|
||||
onClick={() => googleMap?.setZoom((googleMap.getZoom() || 12) + 1)}>
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-none" onClick={() => googleMap?.setZoom((googleMap.getZoom() || 12) - 1)}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-none"
|
||||
onClick={() => googleMap?.setZoom((googleMap.getZoom() || 12) - 1)}>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="icon" className="h-8 w-8 bg-white shadow-sm" onClick={() => {
|
||||
if (googleMap && Object.keys(markersRef.current).length > 0) {
|
||||
const bounds = new google.maps.LatLngBounds();
|
||||
Object.values(markersRef.current).forEach(({ marker }) => bounds.extend(marker.getPosition()!));
|
||||
googleMap.fitBounds(bounds);
|
||||
}
|
||||
}}>
|
||||
<Button variant="outline" size="icon" className="h-8 w-8 bg-white shadow-sm"
|
||||
onClick={() => {
|
||||
if (googleMap && Object.keys(markersRef.current).length > 0) {
|
||||
const bounds = new google.maps.LatLngBounds();
|
||||
Object.values(markersRef.current).forEach(({ marker }) => bounds.extend(marker.getPosition()!));
|
||||
googleMap.fitBounds(bounds);
|
||||
}
|
||||
}}>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user