diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/app-9xzmfic2e4g1/src/components/planner/AccommodationSelector.tsx b/app-9xzmfic2e4g1/src/components/planner/AccommodationSelector.tsx index cb155ff..77996fe 100644 --- a/app-9xzmfic2e4g1/src/components/planner/AccommodationSelector.tsx +++ b/app-9xzmfic2e4g1/src/components/planner/AccommodationSelector.tsx @@ -2,6 +2,7 @@ import { memo } from 'react'; import { ACCOMMODATION_OPTIONS } from '@/constants/planner'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; +import { motion } from 'framer-motion'; interface AccommodationSelectorProps { selectedId: string; @@ -10,34 +11,48 @@ interface AccommodationSelectorProps { export const AccommodationSelector = memo(({ selectedId, onSelect }: AccommodationSelectorProps) => { return ( -
- {ACCOMMODATION_OPTIONS.map((option) => { +
+ {ACCOMMODATION_OPTIONS.map((option, index) => { const Icon = option.icon; const isSelected = selectedId === option.id; return ( - + + ); })}
); }); -AccommodationSelector.displayName = 'AccommodationSelector'; +AccommodationSelector.displayName = 'AccommodationSelector'; \ No newline at end of file diff --git a/app-9xzmfic2e4g1/src/components/planner/InterestsGrid.tsx b/app-9xzmfic2e4g1/src/components/planner/InterestsGrid.tsx index 2bb021c..446d572 100644 --- a/app-9xzmfic2e4g1/src/components/planner/InterestsGrid.tsx +++ b/app-9xzmfic2e4g1/src/components/planner/InterestsGrid.tsx @@ -2,6 +2,7 @@ import { memo } from 'react'; import { INTEREST_OPTIONS } from '@/constants/planner'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; +import { motion } from 'framer-motion'; interface InterestsGridProps { selectedInterests: string[]; @@ -10,34 +11,48 @@ interface InterestsGridProps { export const InterestsGrid = memo(({ selectedInterests, onToggle }: InterestsGridProps) => { return ( -
- {INTEREST_OPTIONS.map((interest) => { +
+ {INTEREST_OPTIONS.map((interest, index) => { const Icon = interest.icon; const isSelected = selectedInterests.includes(interest.id); return ( - + + ); })}
); }); -InterestsGrid.displayName = 'InterestsGrid'; +InterestsGrid.displayName = 'InterestsGrid'; \ No newline at end of file diff --git a/app-9xzmfic2e4g1/src/components/planner/TravelerInput.tsx b/app-9xzmfic2e4g1/src/components/planner/TravelerInput.tsx index cb24e6a..fabec91 100644 --- a/app-9xzmfic2e4g1/src/components/planner/TravelerInput.tsx +++ b/app-9xzmfic2e4g1/src/components/planner/TravelerInput.tsx @@ -1,6 +1,7 @@ import { memo } from 'react'; import { Button } from '@/components/ui/button'; import { Users, Minus, Plus } from 'lucide-react'; +import { motion } from 'framer-motion'; interface TravelerInputProps { value: number; @@ -9,31 +10,43 @@ interface TravelerInputProps { export const TravelerInput = memo(({ value, onChange }: TravelerInputProps) => { return ( -
-
- +
+
+
+ +
-

Kişi Sayısı

-

{value} Yetişkin

+

Kişi Sayısı

+

{value} Yetişkin Gezgin

-
+ +
- {value} + + + {value} + + - - + +
+ +
- - {/* Day Legend */} -
-
- {itinerary.days.map((day, index) => ( -
-
- - Gün {day.day} - - - {day.items.length} - -
- ))} -
-
)}
); -} +} \ No newline at end of file diff --git a/app-9xzmfic2e4g1/src/components/trip/PlaceSearch.tsx b/app-9xzmfic2e4g1/src/components/trip/PlaceSearch.tsx new file mode 100644 index 0000000..506040e --- /dev/null +++ b/app-9xzmfic2e4g1/src/components/trip/PlaceSearch.tsx @@ -0,0 +1,168 @@ +import { useEffect, useRef, useState } from 'react'; +import { Search, MapPin, Star, Plus, Loader2 } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Place } from '@/db/api'; +import { cn } from '@/lib/utils'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface PlaceSearchProps { + onPlaceSelect: (place: Place) => void; + className?: string; + placeholder?: string; +} + +export function PlaceSearch({ onPlaceSelect, className, placeholder = "Yeni bir durak ara..." }: PlaceSearchProps) { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const [loading, setLoading] = useState(false); + const containerRef = useRef(null); + const autocompleteService = useRef(null); + const placesService = useRef(null); + + useEffect(() => { + if (window.google && !autocompleteService.current) { + autocompleteService.current = new window.google.maps.places.AutocompleteService(); + // PlacesService requires an HTML element, even if invisible + const dummyElement = document.createElement('div'); + placesService.current = new window.google.maps.places.PlacesService(dummyElement); + } + }, []); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleSearch = async (value: string) => { + setQuery(value); + if (!value || !autocompleteService.current) { + setResults([]); + setIsOpen(false); + return; + } + + setLoading(true); + autocompleteService.current.getPlacePredictions( + { + input: value, + locationBias: { lat: 38.6431, lng: 34.8347, radius: 50000 }, // Bias towards Cappadocia + componentRestrictions: { country: 'tr' } + }, + (predictions, status) => { + setLoading(false); + if (status === window.google.maps.places.PlacesServiceStatus.OK && predictions) { + setResults(predictions); + setIsOpen(true); + } else { + setResults([]); + setIsOpen(false); + } + } + ); + }; + + const handleSelectResult = (prediction: google.maps.places.AutocompletePrediction) => { + if (!placesService.current) return; + + setLoading(true); + placesService.current.getDetails( + { + placeId: prediction.place_id, + fields: ['name', 'formatted_address', 'geometry', 'rating', 'photos', 'types'] + }, + (place, status) => { + setLoading(false); + if (status === window.google.maps.places.PlacesServiceStatus.OK && place && place.geometry?.location) { + const newPlace: Place = { + place_id: prediction.place_id, + name: place.name || '', + lat: place.geometry.location.lat(), + lng: place.geometry.location.lng(), + rating: place.rating, + formatted_address: place.formatted_address || '', + photo_reference: place.photos?.[0]?.getUrl(), // Note: In a real app, you'd store the reference or proxy the URL + description: place.types?.join(', ') || 'Turistik Nokta', + category: (place.types?.[0] || 'point_of_interest').replace(/_/g, ' '), + estimated_duration_minutes: 60, + start_time: '10:00', + end_time: '11:00', + }; + onPlaceSelect(newPlace); + setQuery(''); + setIsOpen(false); + setResults([]); + } + } + ); + }; + + return ( +
+
+
+ +
+ handleSearch(e.target.value)} + onFocus={() => query && results.length > 0 && setIsOpen(true)} + placeholder={placeholder} + className="h-16 pl-14 pr-6 rounded-2xl bg-white dark:bg-white/5 border-2 border-gray-100 dark:border-white/10 focus:border-primary focus:ring-4 focus:ring-primary/10 transition-luxury text-lg font-medium italic" + /> + {loading && ( +
+ +
+ )} +
+ + + {isOpen && results.length > 0 && ( + +
+ {results.map((result) => ( + + ))} +
+
+ + Google Places Tarafından Desteklenmektedir + +
+
+ )} +
+
+ ); +} diff --git a/app-9xzmfic2e4g1/src/components/trip/Timeline.tsx b/app-9xzmfic2e4g1/src/components/trip/Timeline.tsx index c779169..0ce9515 100644 --- a/app-9xzmfic2e4g1/src/components/trip/Timeline.tsx +++ b/app-9xzmfic2e4g1/src/components/trip/Timeline.tsx @@ -1,9 +1,9 @@ import { ItineraryDay, Place } from '@/db/api'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { Star, Clock, MapPin, GripVertical, Car } from 'lucide-react'; +import { Star, Clock, MapPin, GripVertical, Car, Compass, Camera, Zap, Trash2, Edit3, MessageSquare, MoreVertical, Coffee, Sun, Sunset, Moon } from 'lucide-react'; import api from '@/db/api'; -import { useMemo } from 'react'; +import { useState } from 'react'; import { DndContext, closestCenter, @@ -22,23 +22,50 @@ import { } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { cn } from '@/lib/utils'; +import { motion } from 'framer-motion'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { PlaceSearch } from './PlaceSearch'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; interface TimelineProps { itinerary: { days: ItineraryDay[] }; onReorder: (dayIndex: number, newItems: Place[]) => void; + onAddPlace: (dayIndex: number, place: Place) => void; + onDeletePlace: (dayIndex: number, placeId: string) => void; + onUpdatePlaceNote: (dayIndex: number, placeId: string, note: string) => void; + onUpdateDayNote: (dayIndex: number, note: string) => void; onPlaceClick: (id: string) => void; activePlaceId: string | null; } -export function Timeline({ itinerary, onReorder, onPlaceClick, activePlaceId }: TimelineProps) { +export function Timeline({ + itinerary, + onReorder, + onAddPlace, + onDeletePlace, + onUpdatePlaceNote, + onUpdateDayNote, + onPlaceClick, + activePlaceId +}: TimelineProps) { return ( -
+
{itinerary.days.map((day, dayIndex) => ( @@ -47,13 +74,30 @@ export function Timeline({ itinerary, onReorder, onPlaceClick, activePlaceId }: ); } -function DaySection({ day, dayIndex, onReorder, onPlaceClick, activePlaceId }: { +function DaySection({ + day, + dayIndex, + onReorder, + onAddPlace, + onDeletePlace, + onUpdatePlaceNote, + onUpdateDayNote, + onPlaceClick, + activePlaceId +}: { day: ItineraryDay; dayIndex: number; onReorder: (dayIndex: number, newItems: Place[]) => void; + onAddPlace: (dayIndex: number, place: Place) => void; + onDeletePlace: (dayIndex: number, placeId: string) => void; + onUpdatePlaceNote: (dayIndex: number, placeId: string, note: string) => void; + onUpdateDayNote: (dayIndex: number, note: string) => void; onPlaceClick: (id: string) => void; activePlaceId: string | null; }) { + const [isEditingDayNote, setIsEditingDayNote] = useState(false); + const [dayNote, setDayNote] = useState(day.notes || ''); + const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -73,48 +117,56 @@ function DaySection({ day, dayIndex, onReorder, onPlaceClick, activePlaceId }: { } }; - // Calculate estimated travel times between places (memoized to prevent re-renders) - const travelTimes = useMemo(() => { - const times: { [key: number]: number } = {}; - day.items.forEach((_, index) => { - if (index > 0 && index < day.items.length) { - // Rough estimate: 5-15 minutes between places - times[index] = Math.floor(Math.random() * 10) + 5; - } - }); - return times; - }, [day.items.length]); + const handleSaveDayNote = () => { + onUpdateDayNote(dayIndex, dayNote); + setIsEditingDayNote(false); + }; return ( -
- {/* Day Header with gradient divider */} -
-
-

- Gün {day.day} - - {day.items.length} Mekan - -

-
- {(day.total_distance || day.total_duration) && ( -
- {day.total_distance && ( - - - {day.total_distance} - - )} - {day.total_duration && ( - - - {day.total_duration} - - )} + + {/* Day Notes */} +
+ {isEditingDayNote ? ( +
+