diff --git a/app-9xzmfic2e4g1/src/components/trip/Map.tsx b/app-9xzmfic2e4g1/src/components/trip/Map.tsx index 042857e..d1051d2 100644 --- a/app-9xzmfic2e4g1/src/components/trip/Map.tsx +++ b/app-9xzmfic2e4g1/src/components/trip/Map.tsx @@ -2,15 +2,16 @@ 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, Plus, Loader2 } from 'lucide-react'; +import { ZoomIn, ZoomOut, Maximize2, Plus, Star, Clock, ChevronRight, Lightbulb, CheckCircle2, Loader2, X, MessageSquare } from 'lucide-react'; import api from '@/db/api'; import { cn } from '@/lib/utils'; +import { motion, AnimatePresence } from 'framer-motion'; interface MapProps { itinerary: { days: ItineraryDay[] }; activePlaceId: string | null; onMarkerClick: (id: string) => void; - onAddPlace?: (place: Place) => void; // yeni: haritadan ekleme + onAddPlace?: (place: Place) => void; } const DAY_COLORS = [ @@ -21,21 +22,102 @@ const DAY_COLORS = [ '#DB2777', ]; +interface PlaceDetail { + place_id: string; + name: string; + summary?: string; + rating?: number; + total_ratings?: number; + is_open_now?: boolean | null; + opening_hours?: string[] | null; + why_visit: string[]; + tips: string[]; + reviews: { author: string; rating: number; text: string; time: string }[]; +} + +interface SelectedPOI { + place_id: string; + name: string; + lat: number; + lng: number; + photoUrl: string; + category: string; + formatted_address?: string; + rating?: number; +} + export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: MapProps) { const mapRef = useRef(null); const [googleMap, setGoogleMap] = useState(null); const [error, setError] = useState(null); const markersRef = useRef<{ [key: string]: { marker: google.maps.Marker; dayIndex: number } }>({}); const polylinesRef = useRef([]); - const infoWindowRef = useRef(null); const placesServiceRef = useRef(null); - const [addingPlaceId, setAddingPlaceId] = useState(null); + + // Detail panel state + const [selectedPOI, setSelectedPOI] = useState(null); + const [placeDetail, setPlaceDetail] = useState(null); + const [detailLoading, setDetailLoading] = useState(false); + const [added, setAdded] = useState(false); + const [activeTab, setActiveTab] = useState<'about' | 'reviews'>('about'); const itineraryKey = useMemo(() => JSON.stringify(itinerary.days.map(d => d.items.map(i => i.place_id))), [itinerary] ); + // ── Reset panel on itinerary change ────────────────────────────────────── + useEffect(() => { + setSelectedPOI(null); + setPlaceDetail(null); + setAdded(false); + }, [itineraryKey]); + + // ── Fetch rich details ──────────────────────────────────────────────────── + const fetchPlaceDetail = useCallback(async (poi: SelectedPOI) => { + setDetailLoading(true); + setPlaceDetail(null); + setActiveTab('about'); + try { + const data = await api.getPlaceDetails({ + place_id: poi.place_id, + name: poi.name, + category: poi.category, + }); + setPlaceDetail(data); + } catch (e) { + console.error('Place detail fetch error:', e); + } finally { + setDetailLoading(false); + } + }, []); + + // ── Handle add ──────────────────────────────────────────────────────────── + const handleAdd = useCallback(() => { + if (!selectedPOI || !onAddPlace) return; + const place: Place = { + place_id: selectedPOI.place_id, + name: selectedPOI.name, + lat: selectedPOI.lat, + lng: selectedPOI.lng, + rating: selectedPOI.rating, + formatted_address: selectedPOI.formatted_address || '', + photo_reference: selectedPOI.photoUrl, + description: selectedPOI.category, + category: selectedPOI.category, + estimated_duration_minutes: 60, + start_time: '09:00', + end_time: '10:00', + }; + onAddPlace(place); + setAdded(true); + setTimeout(() => { + setSelectedPOI(null); + setPlaceDetail(null); + setAdded(false); + }, 1500); + }, [selectedPOI, onAddPlace]); + // ── Map init ────────────────────────────────────────────────────────────── useEffect(() => { const loadMap = async () => { @@ -59,99 +141,45 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: streetViewControl: false, fullscreenControl: false, zoomControl: false, - clickableIcons: true, // POI'lere tıklanabilir + clickableIcons: true, }); - 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 + if (!e.placeId) return; + e.stop?.(); const placeId = e.placeId; - infoWindowRef.current?.close(); + setAdded(false); - // Önce "yükleniyor" info window göster - const loadingContent = ` -
-
- Yer bilgisi yükleniyor... -
- - `; - infoWindowRef.current?.setContent(loadingContent); - infoWindowRef.current?.setPosition(e.latLng!); - infoWindowRef.current?.open(map); - - // Places Details çek + // Önce temel bilgiyi Google'dan çek, sonra panel aç placesServiceRef.current?.getDetails( - { placeId, fields: ['place_id', 'name', 'formatted_address', 'geometry', 'rating', 'photos', 'types'] }, + { + 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; - } + if (status !== google.maps.places.PlacesServiceStatus.OK || !place?.geometry?.location) return; - const photoUrl = place.photos?.[0]?.getUrl({ maxWidth: 400 }) || ''; - const btnId = `map-add-btn-${placeId}`; + const photoUrl = place.photos?.[0]?.getUrl({ maxWidth: 600 }) || ''; + const category = (place.types?.[0] || 'point_of_interest').replace(/_/g, ' '); - const content = ` -
- ${photoUrl ? ` -
- -
- ` : ''} -
-

${place.name}

-

${place.formatted_address || ''}

-
- ${place.rating ? `★ ${place.rating}` : ''} - -
-
-
- `; + const poi: SelectedPOI = { + place_id: place.place_id || placeId, + name: place.name || '', + lat: place.geometry.location.lat(), + lng: place.geometry.location.lng(), + photoUrl, + category, + formatted_address: place.formatted_address || '', + rating: place.rating, + }; - 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); - }); - }); + setSelectedPOI(poi); + fetchPlaceDetail(poi); } ); }); @@ -165,7 +193,7 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: }; if (!googleMap) loadMap(); - }, [googleMap, onAddPlace]); + }, [googleMap, onAddPlace, fetchPlaceDetail]); // ── Markers & polylines ─────────────────────────────────────────────────── useEffect(() => { @@ -177,6 +205,7 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: polylinesRef.current = []; const bounds = new google.maps.LatLngBounds(); + const infoWindow = new google.maps.InfoWindow(); itinerary.days.forEach((day, dayIndex) => { const dayColor = DAY_COLORS[dayIndex % DAY_COLORS.length]; @@ -185,7 +214,6 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: day.items.forEach((item, itemIndex) => { const position = { lat: item.lat, lng: item.lng }; dayPath.push(position); - const isActive = item.place_id === activePlaceId; const marker = new google.maps.Marker({ @@ -193,12 +221,7 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: map: googleMap, title: item.name, zIndex: isActive ? 1000 : 1, - label: { - text: (itemIndex + 1).toString(), - color: 'white', - fontSize: '11px', - fontWeight: '900', - }, + label: { text: (itemIndex + 1).toString(), color: 'white', fontSize: '11px', 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', fillColor: dayColor, @@ -217,20 +240,19 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: const photoUrl = item.photo_reference ? (item.photo_reference.startsWith('http') ? item.photo_reference : api.getPhotoUrl(item.photo_reference)) : ''; - - infoWindowRef.current?.setContent(` + infoWindow.setContent(`
- ${photoUrl ? `` : ''} -

${item.name}

-

${item.category}

+ ${photoUrl ? `` : ''} +

${item.name}

+

${item.category}

- ★ ${item.rating || 'N/A'} + ★ ${item.rating || 'N/A'} Yol Tarifi + style="color:#EA580C;font-size:11px;font-weight:bold;text-decoration:none;">Yol Tarifi →
`); - infoWindowRef.current?.open(googleMap, marker); + infoWindow.open(googleMap, marker); }); markersRef.current[item.place_id] = { marker, dayIndex }; @@ -239,11 +261,8 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: if (dayPath.length > 1) { const polyline = new google.maps.Polyline({ - path: dayPath, - geodesic: true, - strokeColor: dayColor, - strokeOpacity: 0.8, - strokeWeight: 3, + path: dayPath, geodesic: true, + strokeColor: dayColor, strokeOpacity: 0.8, strokeWeight: 3, }); polyline.setMap(googleMap); polylinesRef.current.push(polyline); @@ -258,18 +277,15 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: // ── Active marker highlight ─────────────────────────────────────────────── useEffect(() => { 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, + fillColor: color, fillOpacity: 1, strokeWeight: isActive ? 3 : 2, strokeColor: isActive ? '#000000' : '#ffffff', scale: isActive ? 1.8 : 1.4, @@ -281,8 +297,9 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: }); }, [activePlaceId, googleMap]); + // ── Render ──────────────────────────────────────────────────────────────── return ( -
+
{error ? (

{error}

@@ -291,18 +308,18 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: <>
- {/* Haritadan ekleme ipucu */} - {onAddPlace && ( + {/* İpucu */} + {onAddPlace && !selectedPOI && (
- Haritada bir yere tıklayarak plana ekleyin + Haritada bir yere tıklayarak detayları görün
)} - {/* Zoom controls */} -
+ {/* Zoom */} +
+ + {/* ── Detay Paneli ───────────────────────────────────────────────── */} + + {selectedPOI && ( + + {/* Fotoğraf header */} +
+ {selectedPOI.photoUrl ? ( + {selectedPOI.name} + ) : ( +
+ 🗺️ +
+ )} + {/* Gradient overlay */} +
+ + {/* Kapat */} + + + {/* Açık/Kapalı badge */} + {placeDetail?.is_open_now != null && ( +
+ {placeDetail.is_open_now ? '● Açık' : '● Kapalı'} +
+ )} + + {/* İsim */} +
+

+ {selectedPOI.category} +

+

+ {selectedPOI.name} +

+
+
+ + {/* Rating + tabs */} +
+
+ {(selectedPOI.rating || placeDetail?.rating) && ( +
+
+ {[1,2,3,4,5].map(i => ( + + ))} +
+ + {(selectedPOI.rating || placeDetail?.rating)?.toFixed(1)} + + {placeDetail?.total_ratings && ( + ({placeDetail.total_ratings.toLocaleString()}) + )} +
+ )} +
+ + {/* Tabs */} +
+ {(['about', 'reviews'] as const).map(tab => ( + + ))} +
+
+ + {/* İçerik */} +
+ {detailLoading ? ( +
+ +

Detaylar yükleniyor...

+
+ ) : placeDetail ? ( + + {activeTab === 'about' ? ( + + {/* Özet */} + {placeDetail.summary && ( +

{placeDetail.summary}

+ )} + + {/* Neden gitmelisiniz */} + {placeDetail.why_visit?.length > 0 && ( +
+

+ + Neden gitmelisiniz? +

+
+ {placeDetail.why_visit.map((reason, i) => ( +
+
+ {i + 1} +
+

{reason}

+
+ ))} +
+
+ )} + + {/* Gitmeden önce */} + {placeDetail.tips?.length > 0 && ( +
+

+ + Gitmeden önce bilin +

+
+ {placeDetail.tips.map((tip, i) => ( +
+ +

{tip}

+
+ ))} +
+
+ )} + + {/* Çalışma saatleri */} + {placeDetail.opening_hours?.length > 0 && ( +
+

+ + Çalışma Saatleri +

+
+ {placeDetail.opening_hours.map((h, i) => ( +

{h}

+ ))} +
+
+ )} +
+ ) : ( + + {placeDetail.reviews?.length > 0 ? ( + placeDetail.reviews.map((review, i) => ( +
+
+
+
+ {review.author?.[0]?.toUpperCase() || '?'} +
+
+

{review.author}

+

{review.time}

+
+
+
+ {[1,2,3,4,5].map(s => ( + + ))} +
+
+

{review.text}

+
+ )) + ) : ( +
+ +

Yorum bulunamadı

+
+ )} +
+ )} +
+ ) : null} +
+ + {/* Adres + Plana ekle butonu */} +
+ {selectedPOI.formatted_address && ( +

+ 📍 {selectedPOI.formatted_address} +

+ )} + {onAddPlace && ( + + )} +
+ + )} + )}