Edit app-9xzmfic2e4g1/src/components/trip/Map.tsx via Editor
This commit is contained in:
parent
abc2e658a0
commit
773d823e75
@ -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<HTMLDivElement>(null);
|
||||
const [googleMap, setGoogleMap] = useState<google.maps.Map | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
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);
|
||||
|
||||
// Detail panel state
|
||||
const [selectedPOI, setSelectedPOI] = useState<SelectedPOI | null>(null);
|
||||
const [placeDetail, setPlaceDetail] = useState<PlaceDetail | null>(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 = `
|
||||
<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
|
||||
// Ö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 = `
|
||||
<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>
|
||||
`;
|
||||
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(`
|
||||
<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>
|
||||
${photoUrl ? `<img src="${photoUrl}" style="width:100%;height:90px;object-fit:cover;border-radius:8px;margin-bottom:8px;" />` : ''}
|
||||
<h4 style="margin:0 0 2px;font-size:13px;font-weight:bold;">${item.name}</h4>
|
||||
<p style="margin:0 0 6px;font-size:10px;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>
|
||||
<span style="font-size:11px;font-weight:bold;color:#F59E0B;">★ ${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>
|
||||
style="color:#EA580C;font-size:11px;font-weight:bold;text-decoration:none;">Yol Tarifi →</a>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
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 (
|
||||
<div className="relative w-full h-full bg-gray-50">
|
||||
<div className="relative w-full h-full bg-gray-50 overflow-hidden">
|
||||
{error ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<p className="text-sm font-medium text-gray-500">{error}</p>
|
||||
@ -291,18 +308,18 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }:
|
||||
<>
|
||||
<div ref={mapRef} className="absolute inset-0 w-full h-full" />
|
||||
|
||||
{/* Haritadan ekleme ipucu */}
|
||||
{onAddPlace && (
|
||||
{/* İpucu */}
|
||||
{onAddPlace && !selectedPOI && (
|
||||
<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>
|
||||
<span className="text-[11px] font-bold text-gray-600">Haritada bir yere tıklayarak detayları görün</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zoom controls */}
|
||||
<div className="absolute top-4 right-4 flex flex-col gap-2">
|
||||
{/* Zoom */}
|
||||
<div className="absolute top-4 right-4 flex flex-col gap-2 z-10">
|
||||
<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)}>
|
||||
@ -324,6 +341,259 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }:
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ── Detay Paneli ───────────────────────────────────────────────── */}
|
||||
<AnimatePresence>
|
||||
{selectedPOI && (
|
||||
<motion.div
|
||||
initial={{ x: '100%', opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: '100%', opacity: 0 }}
|
||||
transition={{ type: 'spring', damping: 28, stiffness: 300 }}
|
||||
className="absolute top-0 right-0 bottom-0 w-[340px] bg-white shadow-2xl z-20 flex flex-col overflow-hidden"
|
||||
>
|
||||
{/* Fotoğraf header */}
|
||||
<div className="relative h-44 shrink-0 bg-gray-100">
|
||||
{selectedPOI.photoUrl ? (
|
||||
<img
|
||||
src={selectedPOI.photoUrl}
|
||||
alt={selectedPOI.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gradient-to-br from-orange-100 to-amber-50 flex items-center justify-center">
|
||||
<span className="text-4xl">🗺️</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
||||
|
||||
{/* Kapat */}
|
||||
<button
|
||||
onClick={() => { setSelectedPOI(null); setPlaceDetail(null); setAdded(false); }}
|
||||
className="absolute top-3 right-3 w-8 h-8 rounded-full bg-black/40 backdrop-blur-sm flex items-center justify-center text-white hover:bg-black/60 transition-all"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{/* Açık/Kapalı badge */}
|
||||
{placeDetail?.is_open_now != null && (
|
||||
<div className={cn(
|
||||
'absolute top-3 left-3 px-2.5 py-1 rounded-full text-[10px] font-black backdrop-blur-sm',
|
||||
placeDetail.is_open_now
|
||||
? 'bg-green-500/90 text-white'
|
||||
: 'bg-red-500/90 text-white'
|
||||
)}>
|
||||
{placeDetail.is_open_now ? '● Açık' : '● Kapalı'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* İsim */}
|
||||
<div className="absolute bottom-3 left-3 right-3">
|
||||
<p className="text-[10px] font-bold text-white/70 uppercase tracking-widest mb-0.5">
|
||||
{selectedPOI.category}
|
||||
</p>
|
||||
<h3 className="text-lg font-black text-white leading-tight line-clamp-2">
|
||||
{selectedPOI.name}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rating + tabs */}
|
||||
<div className="px-4 pt-3 pb-0 border-b shrink-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
{(selectedPOI.rating || placeDetail?.rating) && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{[1,2,3,4,5].map(i => (
|
||||
<Star key={i}
|
||||
className={cn('h-3.5 w-3.5', i <= Math.round(selectedPOI.rating || placeDetail?.rating || 0)
|
||||
? 'fill-amber-400 text-amber-400'
|
||||
: 'text-gray-200 fill-gray-200'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm font-black text-gray-800">
|
||||
{(selectedPOI.rating || placeDetail?.rating)?.toFixed(1)}
|
||||
</span>
|
||||
{placeDetail?.total_ratings && (
|
||||
<span className="text-xs text-gray-400">({placeDetail.total_ratings.toLocaleString()})</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1">
|
||||
{(['about', 'reviews'] as const).map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 text-[11px] font-black uppercase tracking-wider rounded-t-lg transition-all border-b-2',
|
||||
activeTab === tab
|
||||
? 'text-orange-600 border-orange-600'
|
||||
: 'text-gray-400 border-transparent hover:text-gray-600'
|
||||
)}
|
||||
>
|
||||
{tab === 'about' ? 'Hakkında' : 'Yorumlar'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* İçerik */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{detailLoading ? (
|
||||
<div className="flex flex-col items-center justify-center h-48 gap-3">
|
||||
<Loader2 className="h-8 w-8 text-orange-500 animate-spin" />
|
||||
<p className="text-xs font-bold text-gray-400">Detaylar yükleniyor...</p>
|
||||
</div>
|
||||
) : placeDetail ? (
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'about' ? (
|
||||
<motion.div
|
||||
key="about"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="p-4 space-y-5"
|
||||
>
|
||||
{/* Özet */}
|
||||
{placeDetail.summary && (
|
||||
<p className="text-sm text-gray-600 leading-relaxed">{placeDetail.summary}</p>
|
||||
)}
|
||||
|
||||
{/* Neden gitmelisiniz */}
|
||||
{placeDetail.why_visit?.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-[11px] font-black text-gray-900 uppercase tracking-widest flex items-center gap-1.5">
|
||||
<ChevronRight className="h-3.5 w-3.5 text-orange-500" />
|
||||
Neden gitmelisiniz?
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{placeDetail.why_visit.map((reason, i) => (
|
||||
<div key={i} className="flex items-start gap-2.5">
|
||||
<div className="w-5 h-5 rounded-full bg-orange-100 text-orange-600 flex items-center justify-center text-[10px] font-black shrink-0 mt-0.5">
|
||||
{i + 1}
|
||||
</div>
|
||||
<p className="text-[12px] text-gray-700 leading-relaxed">{reason}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gitmeden önce */}
|
||||
{placeDetail.tips?.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-[11px] font-black text-gray-900 uppercase tracking-widest flex items-center gap-1.5">
|
||||
<Lightbulb className="h-3.5 w-3.5 text-amber-500" />
|
||||
Gitmeden önce bilin
|
||||
</h4>
|
||||
<div className="space-y-1.5 bg-amber-50 rounded-xl p-3 border border-amber-100">
|
||||
{placeDetail.tips.map((tip, i) => (
|
||||
<div key={i} className="flex items-start gap-2">
|
||||
<span className="text-amber-500 mt-0.5 shrink-0 text-xs">•</span>
|
||||
<p className="text-[12px] text-gray-700 leading-relaxed">{tip}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Çalışma saatleri */}
|
||||
{placeDetail.opening_hours?.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-[11px] font-black text-gray-900 uppercase tracking-widest flex items-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5 text-blue-500" />
|
||||
Çalışma Saatleri
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{placeDetail.opening_hours.map((h, i) => (
|
||||
<p key={i} className="text-[11px] text-gray-500">{h}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="reviews"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="p-4 space-y-4"
|
||||
>
|
||||
{placeDetail.reviews?.length > 0 ? (
|
||||
placeDetail.reviews.map((review, i) => (
|
||||
<div key={i} className="space-y-2 pb-4 border-b border-gray-100 last:border-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-full bg-orange-100 flex items-center justify-center text-orange-600 font-black text-xs shrink-0">
|
||||
{review.author?.[0]?.toUpperCase() || '?'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-gray-800">{review.author}</p>
|
||||
<p className="text-[10px] text-gray-400">{review.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
{[1,2,3,4,5].map(s => (
|
||||
<Star key={s}
|
||||
className={cn('h-3 w-3', s <= review.rating
|
||||
? 'fill-amber-400 text-amber-400'
|
||||
: 'text-gray-200 fill-gray-200'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[12px] text-gray-600 leading-relaxed line-clamp-4">{review.text}</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-32 gap-2 text-gray-400">
|
||||
<MessageSquare className="h-8 w-8 opacity-30" />
|
||||
<p className="text-xs font-medium">Yorum bulunamadı</p>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Adres + Plana ekle butonu */}
|
||||
<div className="p-4 border-t bg-white shrink-0 space-y-3">
|
||||
{selectedPOI.formatted_address && (
|
||||
<p className="text-[11px] text-gray-400 leading-snug line-clamp-2">
|
||||
📍 {selectedPOI.formatted_address}
|
||||
</p>
|
||||
)}
|
||||
{onAddPlace && (
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
disabled={added}
|
||||
className={cn(
|
||||
'w-full h-11 rounded-xl font-black text-sm gap-2 transition-all',
|
||||
added
|
||||
? 'bg-green-500 hover:bg-green-500 text-white'
|
||||
: 'bg-orange-600 hover:bg-orange-700 text-white shadow-lg shadow-orange-200'
|
||||
)}
|
||||
>
|
||||
{added ? (
|
||||
<><CheckCircle2 className="h-4 w-4" /> Plana Eklendi!</>
|
||||
) : (
|
||||
<><Plus className="h-4 w-4" /> Güne Ekle</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user