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 { 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>