diff --git a/app-9xzmfic2e4g1/src/components/trip/Timeline.tsx b/app-9xzmfic2e4g1/src/components/trip/Timeline.tsx
index 0ce9515..c7f34c8 100644
--- a/app-9xzmfic2e4g1/src/components/trip/Timeline.tsx
+++ b/app-9xzmfic2e4g1/src/components/trip/Timeline.tsx
@@ -1,38 +1,66 @@
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, Compass, Camera, Zap, Trash2, Edit3, MessageSquare, MoreVertical, Coffee, Sun, Sunset, Moon } from 'lucide-react';
-import api from '@/db/api';
-import { useState } from 'react';
import {
- DndContext,
- closestCenter,
- KeyboardSensor,
- PointerSensor,
- useSensor,
- useSensors,
- DragEndEvent,
+ Star, Clock, MapPin, GripVertical, Car, Trash2, Edit3,
+ MessageSquare, MoreVertical, Sun, Sunset, Moon, Coffee,
+ Package, Wand2
+} from 'lucide-react';
+import api from '@/db/api';
+import { useState, useMemo } from 'react';
+import {
+ DndContext, closestCenter, KeyboardSensor, PointerSensor,
+ useSensor, useSensors, DragEndEvent,
} from '@dnd-kit/core';
import {
- arrayMove,
- SortableContext,
- sortableKeyboardCoordinates,
- verticalListSortingStrategy,
- useSortable,
+ arrayMove, SortableContext, sortableKeyboardCoordinates,
+ verticalListSortingStrategy, useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { cn } from '@/lib/utils';
-import { motion } from 'framer-motion';
+import { motion, AnimatePresence } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { PlaceSearch } from './PlaceSearch';
import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
+ DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+// ────────────────────────────────────────────────────────────────────────────
+// Helpers
+// ────────────────────────────────────────────────────────────────────────────
+function parseMinutes(time: string): number {
+ const [h = 0, m = 0] = time.split(':').map(Number);
+ return h * 60 + m;
+}
+
+function formatTime(mins: number): string {
+ return `${String(Math.floor(mins / 60) % 24).padStart(2, '0')}:${String(mins % 60).padStart(2, '0')}`;
+}
+
+/** Compute end_time from start_time + estimated_duration_minutes */
+function calcEndTime(item: Place): string {
+ const start = parseMinutes(item.start_time || '09:00');
+ return formatTime(start + (item.estimated_duration_minutes || 60));
+}
+
+/** Time-of-day segment label */
+function getSegment(time: string): 'morning' | 'afternoon' | 'evening' {
+ const mins = parseMinutes(time);
+ if (mins < 12 * 60) return 'morning';
+ if (mins < 18 * 60) return 'afternoon';
+ return 'evening';
+}
+
+const SEGMENT_META = {
+ morning: { label: 'Sabah', icon: Sun, color: 'text-amber-500' },
+ afternoon: { label: 'Öğleden Sonra', icon: Coffee, color: 'text-orange-500' },
+ evening: { label: 'Akşam', icon: Sunset, color: 'text-rose-500' },
+};
+
+// ────────────────────────────────────────────────────────────────────────────
+// Props
+// ────────────────────────────────────────────────────────────────────────────
interface TimelineProps {
itinerary: { days: ItineraryDay[] };
onReorder: (dayIndex: number, newItems: Place[]) => void;
@@ -42,328 +70,388 @@ interface TimelineProps {
onUpdateDayNote: (dayIndex: number, note: string) => void;
onPlaceClick: (id: string) => void;
activePlaceId: string | null;
+ dayStartDate?: string;
}
-export function Timeline({
- itinerary,
- onReorder,
- onAddPlace,
- onDeletePlace,
- onUpdatePlaceNote,
- onUpdateDayNote,
- onPlaceClick,
- activePlaceId
-}: TimelineProps) {
+// ────────────────────────────────────────────────────────────────────────────
+// Timeline root
+// ────────────────────────────────────────────────────────────────────────────
+export function Timeline(props: TimelineProps) {
return (
- {itinerary.days.map((day, dayIndex) => (
-
+ {props.itinerary.days.map((day, dayIndex) => (
+
))}
);
}
-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 || '');
+// ────────────────────────────────────────────────────────────────────────────
+// DaySection
+// ────────────────────────────────────────────────────────────────────────────
+function DaySection({
+ day, dayIndex,
+ onReorder, onAddPlace, onDeletePlace,
+ onUpdatePlaceNote, onUpdateDayNote,
+ onPlaceClick, activePlaceId,
+}: TimelineProps & { day: ItineraryDay; dayIndex: number }) {
+ const [isEditingNote, setIsEditingNote] = useState(false);
+ const [noteText, setNoteText] = useState(day.notes || '');
const sensors = useSensors(
useSensor(PointerSensor),
- useSensor(KeyboardSensor, {
- coordinateGetter: sortableKeyboardCoordinates,
- })
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
-
if (over && active.id !== over.id) {
- const oldIndex = day.items.findIndex((i) => i.place_id === active.id);
- const newIndex = day.items.findIndex((i) => i.place_id === over.id);
-
- const newItems = arrayMove(day.items, oldIndex, newIndex);
- onReorder(dayIndex, newItems);
+ const oldIdx = day.items.findIndex(i => i.place_id === active.id);
+ const newIdx = day.items.findIndex(i => i.place_id === over.id);
+ onReorder(dayIndex, arrayMove(day.items, oldIdx, newIdx));
}
};
- const handleSaveDayNote = () => {
- onUpdateDayNote(dayIndex, dayNote);
- setIsEditingDayNote(false);
- };
+ // Group items by time-of-day
+ const grouped = useMemo(() => {
+ const segments: Record = {};
+ for (const item of day.items) {
+ const seg = getSegment(item.start_time || '09:00');
+ if (!segments[seg]) segments[seg] = [];
+ segments[seg].push(item);
+ }
+ return segments;
+ }, [day.items]);
+
+ const segmentOrder: Array<'morning' | 'afternoon' | 'evening'> = ['morning', 'afternoon', 'evening'];
return (
-
- {/* Day Notes */}
-
- {isEditingDayNote ? (
+
+
+ {/* Day note */}
+
+ {isEditingNote ? (
) : (
-
setIsEditingDayNote(true)}
- className="flex items-start gap-3 p-3 rounded-xl hover:bg-gray-50 transition-all cursor-pointer group/note"
+
+
+ {day.notes ? (
+
"{day.notes}"
+ ) : (
+
+ Bugüne not ekle...
+
+ )}
+
)}
-
-
-
- BAŞLANGIÇ NOKTASI
+ {/* Empty state */}
+ {day.items.length === 0 && (
+
+
+
+
Henüz durak yok
+
Aşağıdan bir yer ekleyerek başlayın
+
+
+ )}
+
+ {/* Grouped items */}
+ {day.items.length > 0 && (
+
+ i.place_id)} strategy={verticalListSortingStrategy}>
+
+ {/* Vertical line */}
+
+
+ {segmentOrder.map(seg => {
+ const items = grouped[seg];
+ if (!items?.length) return null;
+ const meta = SEGMENT_META[seg];
+ const SegIcon = meta.icon;
+ const globalOffset = day.items.indexOf(items[0]);
+
+ return (
+
+ {/* Segment label */}
+
+
+ {meta.label}
+
+
+ {items.map((item) => {
+ const globalIndex = day.items.findIndex(i => i.place_id === item.place_id);
+ return (
+
+ {/* Timeline dot */}
+
+
+
+
onPlaceClick(item.place_id)}
+ onDelete={() => onDeletePlace(dayIndex, item.place_id)}
+ onUpdateNote={note => onUpdatePlaceNote(dayIndex, item.place_id, note)}
+ />
+
+ {/* Travel time connector */}
+ {globalIndex < day.items.length - 1 && (
+
+
+ ~15 dk sürüş
+
+ )}
+
+
+ );
+ })}
+
+ );
+ })}
+
+
+
+ )}
+
+ {/* AI suggestion card */}
+
+
+
+
+
+
+ Akıllı Tur Önerisi
+ %82 Uyum
+
+
+ Planınız Green Tour rotasıyla %82 uyumlu. Yeraltı şehri ve Ihlara Vadisi eklenebilir.
+
+
-
Göreme Merkez
-
- i.place_id)}
- strategy={verticalListSortingStrategy}
- >
-
-
-
-
-
- SABAH
-
-
- {day.items.map((item, index) => (
-
-
-
-
-
onPlaceClick(item.place_id)}
- onDelete={() => onDeletePlace(dayIndex, item.place_id)}
- onUpdateNote={(note) => onUpdatePlaceNote(dayIndex, item.place_id, note)}
- index={index}
- />
-
- {index < day.items.length - 1 && (
-
-
- ~15 DK SÜRÜŞ
-
- )}
-
-
- ))}
-
-
-
onAddPlace(dayIndex, place)}
- placeholder="Yeni bir durak ekle..."
- />
-
-
-
-
+ {/* Add place search */}
+
+
onAddPlace(dayIndex, place)}
+ placeholder="Yeni bir durak ekle..."
+ />
+
);
}
-function SortableItem({
- item,
- isActive,
- onClick,
- onDelete,
- onUpdateNote,
- index
-}: {
- item: Place;
- isActive: boolean;
- onClick: () => void;
+// ────────────────────────────────────────────────────────────────────────────
+// SortableItem — redesigned with larger photo
+// ────────────────────────────────────────────────────────────────────────────
+function SortableItem({
+ item, index, isActive, onClick, onDelete, onUpdateNote
+}: {
+ item: Place;
+ index: number;
+ isActive: boolean;
+ onClick: () => void;
onDelete: () => void;
onUpdateNote: (note: string) => void;
- index: number
}) {
const [isEditingNote, setIsEditingNote] = useState(false);
const [note, setNote] = useState(item.notes || '');
+ const [imgLoaded, setImgLoaded] = useState(false);
+ const [imgError, setImgError] = useState(false);
- const {
- attributes,
- listeners,
- setNodeRef,
- transform,
- transition,
- isDragging
- } = useSortable({ id: item.place_id });
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.place_id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 50 : 'auto',
- opacity: isDragging ? 0.5 : 1,
+ opacity: isDragging ? 0.4 : 1,
};
- const handleSaveNote = (e: React.MouseEvent) => {
- e.stopPropagation();
- onUpdateNote(note);
- setIsEditingNote(false);
- };
+ const photoUrl = useMemo(() => {
+ if (!item.photo_reference) return null;
+ return item.photo_reference.startsWith('http')
+ ? item.photo_reference
+ : api.getPhotoUrl(item.photo_reference);
+ }, [item.photo_reference]);
- const photoUrl = item.photo_reference ? (
- item.photo_reference.startsWith('http') ? item.photo_reference : api.getPhotoUrl(item.photo_reference)
- ) : null;
+ const endTime = calcEndTime(item);
return (
-
-
+
-
-
- {index + 1}
-
-
-
-
-
-
{item.name}
-
- {item.category}
-
-
-
-
-
-
-
-
-
-
-
- setIsEditingNote(true)} className="text-xs font-bold gap-2">
-
- Not Düzenle
-
- { e.stopPropagation(); onDelete(); }} className="text-xs font-bold gap-2 text-red-500">
-
- Kaldır
-
-
-
-
-
-
-
-
-
- {item.start_time || '09:00'} - 11:00
-
- {item.rating && (
-
-
- {item.rating}
-
- )}
-
-
- {isEditingNote ? (
-
e.stopPropagation()}>
-
- ) : (
- item.notes && (
-
- )
+ {/* Photo (16:9) */}
+ {photoUrl && !imgError && (
+
+ {!imgLoaded && (
+
+ )}
+

setImgLoaded(true)}
+ onError={() => setImgError(true)}
+ className={cn(
+ "w-full h-full object-cover transition-all duration-500",
+ imgLoaded ? "opacity-100 scale-100" : "opacity-0 scale-105"
)}
+ />
+ {/* Time badge on photo */}
+
+ {item.start_time || '09:00'} – {endTime}
+
+ {/* Rating badge */}
+ {item.rating && (
+
+
+ {item.rating}
+
+ )}
+ {/* Index bubble */}
+
+ {index + 1}
+
+
+ )}
+
+
+ {/* No photo: compact row layout */}
+ {(!photoUrl || imgError) && (
+
+
+ {index + 1}
+
+ {item.start_time && (
+
+ {item.start_time} – {endTime}
+
+ )}
+ {item.rating && (
+
+
+ {item.rating}
+
+ )}
+
+ )}
+
+ {/* Name + actions */}
+
+
+
{item.name}
+
+
+ {item.category}
+
+ {item.estimated_duration_minutes && (
+
+
+ {item.estimated_duration_minutes}dk
+
+ )}
+
+
+
+
+
e.stopPropagation()}
+ >
+
+
+
+ e.stopPropagation()}>
+
+
+
+ { e.stopPropagation(); setIsEditingNote(true); }} className="text-xs font-bold gap-2 rounded-lg">
+ Not Düzenle
+
+ { e.stopPropagation(); onDelete(); }}
+ className="text-xs font-bold gap-2 text-red-500 rounded-lg focus:text-red-600 focus:bg-red-50"
+ >
+ Kaldır
+
+
+
+
- {photoUrl && (
-
-

-
+ {/* Note */}
+
+ {isEditingNote && (
+ e.stopPropagation()}
+ >
+
+ )}
+
+
+ {!isEditingNote && item.notes && (
+
)}