601 lines
27 KiB
TypeScript
601 lines
27 KiB
TypeScript
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, 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;
|
||
}
|
||
|
||
const DAY_COLORS = [
|
||
'#EA580C',
|
||
'#2563EB',
|
||
'#059669',
|
||
'#7C3AED',
|
||
'#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 placesServiceRef = useRef<google.maps.places.PlacesService | 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 () => {
|
||
try {
|
||
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
|
||
if (!apiKey || apiKey === 'YOUR_GOOGLE_MAPS_API_KEY') {
|
||
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: [
|
||
{ 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,
|
||
});
|
||
|
||
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;
|
||
e.stop?.();
|
||
|
||
const placeId = e.placeId;
|
||
setAdded(false);
|
||
|
||
// Önce temel bilgiyi Google'dan çek, sonra panel aç
|
||
placesServiceRef.current?.getDetails(
|
||
{
|
||
placeId,
|
||
fields: ['place_id', 'name', 'formatted_address', 'geometry', 'rating', 'photos', 'types'],
|
||
},
|
||
(place, status) => {
|
||
if (status !== google.maps.places.PlacesServiceStatus.OK || !place?.geometry?.location) return;
|
||
|
||
const photoUrl = place.photos?.[0]?.getUrl({ maxWidth: 600 }) || '';
|
||
const category = (place.types?.[0] || 'point_of_interest').replace(/_/g, ' ');
|
||
|
||
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,
|
||
};
|
||
|
||
setSelectedPOI(poi);
|
||
fetchPlaceDetail(poi);
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
setGoogleMap(map);
|
||
}
|
||
} catch {
|
||
setError('Google Maps yüklenemedi.');
|
||
}
|
||
};
|
||
|
||
if (!googleMap) loadMap();
|
||
}, [googleMap, onAddPlace, fetchPlaceDetail]);
|
||
|
||
// ── Markers & polylines ───────────────────────────────────────────────────
|
||
useEffect(() => {
|
||
if (!googleMap || !itinerary?.days) return;
|
||
|
||
Object.values(markersRef.current).forEach(({ marker }) => marker.setMap(null));
|
||
markersRef.current = {};
|
||
polylinesRef.current.forEach(p => p.setMap(null));
|
||
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];
|
||
const dayPath: google.maps.LatLngLiteral[] = [];
|
||
|
||
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({
|
||
position,
|
||
map: googleMap,
|
||
title: item.name,
|
||
zIndex: isActive ? 1000 : 1,
|
||
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,
|
||
fillOpacity: 1,
|
||
strokeWeight: 2,
|
||
strokeColor: '#ffffff',
|
||
scale: isActive ? 1.8 : 1.4,
|
||
anchor: new google.maps.Point(12, 22),
|
||
labelOrigin: new google.maps.Point(12, 9),
|
||
},
|
||
animation: isActive ? google.maps.Animation.BOUNCE : undefined,
|
||
});
|
||
|
||
marker.addListener('click', () => {
|
||
onMarkerClick(item.place_id);
|
||
const photoUrl = item.photo_reference
|
||
? (item.photo_reference.startsWith('http') ? item.photo_reference : api.getPhotoUrl(item.photo_reference))
|
||
: '';
|
||
infoWindow.setContent(`
|
||
<div style="padding:8px;min-width:200px;font-family:sans-serif;">
|
||
${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;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>
|
||
</div>
|
||
</div>
|
||
`);
|
||
infoWindow.open(googleMap, marker);
|
||
});
|
||
|
||
markersRef.current[item.place_id] = { marker, dayIndex };
|
||
bounds.extend(position);
|
||
});
|
||
|
||
if (dayPath.length > 1) {
|
||
const polyline = new google.maps.Polyline({
|
||
path: dayPath, geodesic: true,
|
||
strokeColor: dayColor, strokeOpacity: 0.8, strokeWeight: 3,
|
||
});
|
||
polyline.setMap(googleMap);
|
||
polylinesRef.current.push(polyline);
|
||
}
|
||
});
|
||
|
||
if (Object.keys(markersRef.current).length > 0) {
|
||
googleMap.fitBounds(bounds, { top: 60, bottom: 60, left: 60, right: 60 });
|
||
}
|
||
}, [googleMap, itineraryKey]);
|
||
|
||
// ── 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,
|
||
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]);
|
||
|
||
// ── Render ────────────────────────────────────────────────────────────────
|
||
return (
|
||
<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>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div ref={mapRef} className="absolute inset-0 w-full h-full" />
|
||
|
||
{/* İ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 detayları görün</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 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)}>
|
||
<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)}>
|
||
<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);
|
||
}
|
||
}}>
|
||
<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>
|
||
);
|
||
} |