Edit app-9xzmfic2e4g1/src/components/trip/Map.tsx via Editor

This commit is contained in:
Flatlogic Bot 2026-03-04 11:17:22 +00:00
parent fc0cb36f89
commit 78b491dc8d

View File

@ -1,9 +1,8 @@
import { useEffect, useRef, useState, useMemo } from 'react'; import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
import { ItineraryDay } from '@/db/api'; import { ItineraryDay, Place } from '@/db/api';
import { initGoogleMaps } from '@/lib/google-maps-loader'; import { initGoogleMaps } from '@/lib/google-maps-loader';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ZoomIn, ZoomOut, Maximize2, Navigation2, Map as MapIcon, Layers } from 'lucide-react'; import { ZoomIn, ZoomOut, Maximize2, Plus, Loader2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import api from '@/db/api'; import api from '@/db/api';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@ -11,31 +10,33 @@ interface MapProps {
itinerary: { days: ItineraryDay[] }; itinerary: { days: ItineraryDay[] };
activePlaceId: string | null; activePlaceId: string | null;
onMarkerClick: (id: string) => void; onMarkerClick: (id: string) => void;
onAddPlace?: (place: Place) => void; // yeni: haritadan ekleme
} }
// More standard and vibrant color palette for Wanderlog feel
const DAY_COLORS = [ const DAY_COLORS = [
'#EA580C', // Orange-600 '#EA580C',
'#2563EB', // Blue-600 '#2563EB',
'#059669', // Emerald-600 '#059669',
'#7C3AED', // Violet-600 '#7C3AED',
'#DB2777', // Pink-600 '#DB2777',
]; ];
export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) { export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: MapProps) {
const mapRef = useRef<HTMLDivElement>(null); const mapRef = useRef<HTMLDivElement>(null);
const [googleMap, setGoogleMap] = useState<google.maps.Map | null>(null); const [googleMap, setGoogleMap] = useState<google.maps.Map | null>(null);
const [error, setError] = useState<string | 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 markersRef = useRef<{ [key: string]: { marker: google.maps.Marker; dayIndex: number } }>({});
const polylinesRef = useRef<google.maps.Polyline[]>([]); const polylinesRef = useRef<google.maps.Polyline[]>([]);
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null); 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))), JSON.stringify(itinerary.days.map(d => d.items.map(i => i.place_id))),
[itinerary] [itinerary]
); );
// ── Map init ──────────────────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
const loadMap = async () => { const loadMap = async () => {
try { try {
@ -44,47 +45,135 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
setError('Google Maps API anahtarı eksik.'); setError('Google Maps API anahtarı eksik.');
return; return;
} }
await initGoogleMaps(apiKey); await initGoogleMaps(apiKey);
if (mapRef.current && !googleMap) { if (mapRef.current && !googleMap) {
const map = new google.maps.Map(mapRef.current, { const map = new google.maps.Map(mapRef.current, {
center: { lat: 38.6431, lng: 34.8347 }, center: { lat: 38.6431, lng: 34.8347 },
zoom: 12, zoom: 12,
styles: [ 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, mapTypeControl: false,
streetViewControl: false, streetViewControl: false,
fullscreenControl: false, fullscreenControl: false,
zoomControl: false, zoomControl: false,
clickableIcons: true, clickableIcons: true, // POI'lere tıklanabilir
}); });
setGoogleMap(map);
infoWindowRef.current = new google.maps.InfoWindow(); 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.'); setError('Google Maps yüklenemedi.');
} }
}; };
if (!googleMap) loadMap();
}, [googleMap]);
if (!googleMap) loadMap();
}, [googleMap, onAddPlace]);
// ── Markers & polylines ───────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
if (!googleMap || !itinerary?.days) return; if (!googleMap || !itinerary?.days) return;
Object.values(markersRef.current).forEach(({ marker }) => marker.setMap(null)); Object.values(markersRef.current).forEach(({ marker }) => marker.setMap(null));
markersRef.current = {}; markersRef.current = {};
polylinesRef.current.forEach(polyline => polyline.setMap(null)); polylinesRef.current.forEach(p => p.setMap(null));
polylinesRef.current = []; polylinesRef.current = [];
const bounds = new google.maps.LatLngBounds(); const bounds = new google.maps.LatLngBounds();
@ -111,7 +200,7 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
fontWeight: '900', fontWeight: '900',
}, },
icon: { 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, fillColor: dayColor,
fillOpacity: 1, fillOpacity: 1,
strokeWeight: 2, strokeWeight: 2,
@ -125,24 +214,23 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
marker.addListener('click', () => { marker.addListener('click', () => {
onMarkerClick(item.place_id); onMarkerClick(item.place_id);
if (infoWindowRef.current) { const photoUrl = item.photo_reference
const photoUrl = item.photo_reference ? ( ? (item.photo_reference.startsWith('http') ? item.photo_reference : api.getPhotoUrl(item.photo_reference))
item.photo_reference.startsWith('http') ? item.photo_reference : api.getPhotoUrl(item.photo_reference) : '';
) : '';
infoWindowRef.current?.setContent(`
infoWindowRef.current.setContent(` <div style="padding:8px;min-width:200px;font-family:sans-serif;">
<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;" />` : ''}
${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>
<h4 style="margin: 0 0 2px 0; font-size: 14px; font-weight: bold;">${item.name}</h4> <p style="margin:0 0 8px;font-size:11px;color:#6B7280;">${item.category}</p>
<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;">
<div style="display: flex; align-items: center; justify-content: space-between;"> <span style="font-size:11px;font-weight:bold;"> ${item.rating || 'N/A'}</span>
<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"
<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> style="color:#EA580C;font-size:11px;font-weight:bold;text-decoration:none;">Yol Tarifi</a>
</div>
</div> </div>
`); </div>
infoWindowRef.current.open(googleMap, marker); `);
} infoWindowRef.current?.open(googleMap, marker);
}); });
markersRef.current[item.place_id] = { marker, dayIndex }; markersRef.current[item.place_id] = { marker, dayIndex };
@ -167,30 +255,30 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
} }
}, [googleMap, itineraryKey]); }, [googleMap, itineraryKey]);
// ── Active marker highlight ───────────────────────────────────────────────
useEffect(() => { useEffect(() => {
if (googleMap && activePlaceId && markersRef.current[activePlaceId]) { if (!googleMap || !activePlaceId || !markersRef.current[activePlaceId]) return;
const { marker, dayIndex } = markersRef.current[activePlaceId];
googleMap.panTo(marker.getPosition()!); const { marker } = markersRef.current[activePlaceId];
googleMap.panTo(marker.getPosition()!);
Object.keys(markersRef.current).forEach(id => {
const { marker: m, dayIndex: dIdx } = markersRef.current[id]; Object.keys(markersRef.current).forEach(id => {
const color = DAY_COLORS[dIdx % DAY_COLORS.length]; const { marker: m, dayIndex: dIdx } = markersRef.current[id];
const isActive = id === activePlaceId; const color = DAY_COLORS[dIdx % DAY_COLORS.length];
const isActive = id === activePlaceId;
m.setIcon({ 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", 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, fillColor: color,
fillOpacity: 1, fillOpacity: 1,
strokeWeight: isActive ? 3 : 2, strokeWeight: isActive ? 3 : 2,
strokeColor: isActive ? '#000000' : '#ffffff', strokeColor: isActive ? '#000000' : '#ffffff',
scale: isActive ? 1.8 : 1.4, scale: isActive ? 1.8 : 1.4,
anchor: new google.maps.Point(12, 22), anchor: new google.maps.Point(12, 22),
labelOrigin: new google.maps.Point(12, 9), labelOrigin: new google.maps.Point(12, 9),
});
m.setZIndex(isActive ? 1000 : 1);
m.setAnimation(isActive ? google.maps.Animation.BOUNCE : null);
}); });
} m.setZIndex(isActive ? 1000 : 1);
m.setAnimation(isActive ? google.maps.Animation.BOUNCE : null);
});
}, [activePlaceId, googleMap]); }, [activePlaceId, googleMap]);
return ( return (
@ -202,25 +290,37 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick }: MapProps) {
) : ( ) : (
<> <>
<div ref={mapRef} className="absolute inset-0 w-full h-full" /> <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="absolute top-4 right-4 flex flex-col gap-2">
<div className="flex flex-col bg-white border rounded-lg shadow-sm overflow-hidden"> <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" /> <ZoomIn className="h-4 w-4" />
</Button> </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" /> <ZoomOut className="h-4 w-4" />
</Button> </Button>
</div> </div>
<Button variant="outline" size="icon" className="h-8 w-8 bg-white shadow-sm"
<Button variant="outline" size="icon" className="h-8 w-8 bg-white shadow-sm" onClick={() => { onClick={() => {
if (googleMap && Object.keys(markersRef.current).length > 0) { if (googleMap && Object.keys(markersRef.current).length > 0) {
const bounds = new google.maps.LatLngBounds(); const bounds = new google.maps.LatLngBounds();
Object.values(markersRef.current).forEach(({ marker }) => bounds.extend(marker.getPosition()!)); Object.values(markersRef.current).forEach(({ marker }) => bounds.extend(marker.getPosition()!));
googleMap.fitBounds(bounds); googleMap.fitBounds(bounds);
} }
}}> }}>
<Maximize2 className="h-4 w-4" /> <Maximize2 className="h-4 w-4" />
</Button> </Button>
</div> </div>