From 1973786e9d4d28acf077fc04ad93a3f86c8913d7 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 2 Mar 2026 19:53:49 +0000 Subject: [PATCH] Edit app-9xzmfic2e4g1/src/pages/TripDetailsPage.tsx via Editor --- .../src/pages/TripDetailsPage.tsx | 652 +++++++++++------- 1 file changed, 410 insertions(+), 242 deletions(-) diff --git a/app-9xzmfic2e4g1/src/pages/TripDetailsPage.tsx b/app-9xzmfic2e4g1/src/pages/TripDetailsPage.tsx index 19bf3cd..74d9c37 100644 --- a/app-9xzmfic2e4g1/src/pages/TripDetailsPage.tsx +++ b/app-9xzmfic2e4g1/src/pages/TripDetailsPage.tsx @@ -1,376 +1,544 @@ -import { useEffect, useState, useCallback, useMemo } from 'react'; +import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import api, { Trip, Place, ItineraryDay } from '@/db/api'; import { Timeline } from '@/components/trip/Timeline'; import { TripMap } from '@/components/trip/Map'; -import { Loader2, Share2, Download, Calendar, MapPin, Trash2, ChevronLeft, Zap, Plus, Compass, ChevronRight, Save, Wand2, Sparkles, LayoutGrid, RotateCcw, RotateCw } from 'lucide-react'; +import { + Loader2, Share2, MapPin, Trash2, Zap, + Plus, Sparkles, LayoutGrid, RotateCcw, RotateCw, + CheckCircle2, Clock, Navigation +} from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { format } from 'date-fns'; +import { format, addDays } from 'date-fns'; import { tr } from 'date-fns/locale'; import { motion, AnimatePresence } from 'framer-motion'; import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, + Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet'; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, + AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { cn } from '@/lib/utils'; +import { useAuth } from '@/contexts/AuthContext'; +// ──────────────────────────────────────────────────────────────────────────── +// Undo / Redo hook +// ──────────────────────────────────────────────────────────────────────────── +function useUndoRedo(initial: T) { + const stack = useRef([initial]); + const [ptr, setPtr] = useState(0); + + const current = stack.current[ptr]; + const canUndo = ptr > 0; + const canRedo = ptr < stack.current.length - 1; + + const push = useCallback((next: T) => { + stack.current = stack.current.slice(0, ptr + 1); + stack.current.push(next); + setPtr(p => p + 1); + }, [ptr]); + + const undo = useCallback(() => { if (canUndo) setPtr(p => p - 1); }, [canUndo]); + const redo = useCallback(() => { if (canRedo) setPtr(p => p + 1); }, [canRedo]); + + const jumpTo = useCallback((index: number) => { + if (index >= 0 && index < stack.current.length) setPtr(index); + }, []); + + return { current, push, undo, redo, canUndo, canRedo, jumpTo, ptr }; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Component +// ──────────────────────────────────────────────────────────────────────────── export default function TripDetailsPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const { user } = useAuth(); + const [trip, setTrip] = useState(null); const [loading, setLoading] = useState(true); const [activePlaceId, setActivePlaceId] = useState(null); const [isMapSheetOpen, setIsMapSheetOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); const [selectedDayIndex, setSelectedDayIndex] = useState(0); + const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved'); + const saveTimer = useRef | null>(null); - const loadTrip = useCallback(async (tripId: string) => { - try { - const data = await api.getTripById(tripId); - if (data) { - setTrip(data); - } else { - toast.error('Gezi bulunamadı'); - navigate('/explore'); - } - } catch (error) { - console.error(error); - toast.error('Gezi yüklenemedi'); - } finally { - setLoading(false); - } - }, [navigate]); + // History operates on the full itinerary object + const history = useUndoRedo(null); + // ── Load ────────────────────────────────────────────────────────────────── useEffect(() => { - if (id) { - loadTrip(id); - } - }, [id, loadTrip]); + if (!id) return; + (async () => { + try { + const data = await api.getTripById(id); + if (data) { + setTrip(data); + history.push(data.itinerary); + } else { + toast.error('Gezi bulunamadı'); + navigate('/explore'); + } + } catch { + toast.error('Gezi yüklenemedi'); + } finally { + setLoading(false); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); - const handleUpdateTrip = async (updatedTrip: Trip) => { - setTrip(updatedTrip); - try { - await api.updateTrip(updatedTrip.id, { itinerary: updatedTrip.itinerary }); - } catch (error) { - toast.error('Değişiklikler kaydedilemedi'); - } - }; + // ── Persist (debounced) ─────────────────────────────────────────────────── + const scheduleWrite = useCallback((t: Trip) => { + setSaveStatus('unsaved'); + if (saveTimer.current) clearTimeout(saveTimer.current); + saveTimer.current = setTimeout(async () => { + setSaveStatus('saving'); + try { + await api.updateTrip(t.id, { itinerary: t.itinerary }); + setSaveStatus('saved'); + } catch { + setSaveStatus('unsaved'); + toast.error('Değişiklikler kaydedilemedi'); + } + }, 1500); + }, []); + + // ── Apply an itinerary snapshot (used by all mutators + undo/redo) ──────── + const applyItinerary = useCallback((itinerary: Trip['itinerary'], pushToHistory = true) => { + setTrip(prev => { + if (!prev) return prev; + const next = { ...prev, itinerary }; + scheduleWrite(next); + return next; + }); + if (pushToHistory) history.push(itinerary); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [scheduleWrite]); + + // ── Undo / Redo ──────────────────────────────────────────────────────────── + const handleUndo = useCallback(() => { + if (!history.canUndo) return; + history.undo(); + const prev = history.current; + if (prev) applyItinerary(prev, false); + }, [history, applyItinerary]); + + const handleRedo = useCallback(() => { + if (!history.canRedo) return; + history.redo(); + const next = history.current; + if (next) applyItinerary(next, false); + }, [history, applyItinerary]); + + // Keyboard shortcuts + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (!(e.ctrlKey || e.metaKey)) return; + if (e.key === 'z' && !e.shiftKey) { e.preventDefault(); handleUndo(); } + if ((e.key === 'z' && e.shiftKey) || e.key === 'y') { e.preventDefault(); handleRedo(); } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [handleUndo, handleRedo]); + + // ── Itinerary mutators ──────────────────────────────────────────────────── + const withDays = useCallback((fn: (days: ItineraryDay[]) => ItineraryDay[]) => { + if (!trip) return; + applyItinerary({ ...trip.itinerary, days: fn([...trip.itinerary.days]) }); + }, [trip, applyItinerary]); const handleReorder = (dayIndex: number, newItems: Place[]) => { - if (!trip) return; - const newItinerary = { ...trip.itinerary }; - newItinerary.days[dayIndex].items = newItems; - handleUpdateTrip({ ...trip, itinerary: newItinerary }); + withDays(days => { + days[dayIndex] = { ...days[dayIndex], items: newItems }; + return days; + }); }; const handleAddPlace = (dayIndex: number, place: Place) => { - if (!trip) return; - const newItinerary = { ...trip.itinerary }; - newItinerary.days[dayIndex].items = [...newItinerary.days[dayIndex].items, place]; - handleUpdateTrip({ ...trip, itinerary: newItinerary }); + withDays(days => { + const day = days[dayIndex]; + const last = day.items[day.items.length - 1]; + + // Auto-schedule start_time after last item ends + let startMin = 9 * 60; + if (last) { + const [h, m] = (last.start_time || '09:00').split(':').map(Number); + startMin = h * 60 + m + (last.estimated_duration_minutes || 60) + 20; + } + const toHHMM = (mins: number) => + `${String(Math.floor(mins / 60) % 24).padStart(2, '0')}:${String(mins % 60).padStart(2, '0')}`; + + const newPlace = { + ...place, + start_time: toHHMM(startMin), + end_time: toHHMM(startMin + (place.estimated_duration_minutes || 60)), + }; + days[dayIndex] = { ...day, items: [...day.items, newPlace] }; + return days; + }); toast.success(`${place.name} rotaya eklendi`); }; const handleDeletePlace = (dayIndex: number, placeId: string) => { - if (!trip) return; - const newItinerary = { ...trip.itinerary }; - newItinerary.days[dayIndex].items = newItinerary.days[dayIndex].items.filter(i => i.place_id !== placeId); - handleUpdateTrip({ ...trip, itinerary: newItinerary }); + withDays(days => { + days[dayIndex] = { ...days[dayIndex], items: days[dayIndex].items.filter(i => i.place_id !== placeId) }; + return days; + }); toast.success('Durak kaldırıldı'); }; const handleUpdatePlaceNote = (dayIndex: number, placeId: string, note: string) => { - if (!trip) return; - const newItinerary = { ...trip.itinerary }; - const items = [...newItinerary.days[dayIndex].items]; - const itemIndex = items.findIndex(i => i.place_id === placeId); - if (itemIndex > -1) { - items[itemIndex] = { ...items[itemIndex], notes: note }; - newItinerary.days[dayIndex].items = items; - handleUpdateTrip({ ...trip, itinerary: newItinerary }); - } + withDays(days => { + const items = [...days[dayIndex].items]; + const idx = items.findIndex(i => i.place_id === placeId); + if (idx > -1) items[idx] = { ...items[idx], notes: note }; + days[dayIndex] = { ...days[dayIndex], items }; + return days; + }); }; const handleUpdateDayNote = (dayIndex: number, note: string) => { - if (!trip) return; - const newItinerary = { ...trip.itinerary }; - newItinerary.days[dayIndex] = { ...newItinerary.days[dayIndex], notes: note }; - handleUpdateTrip({ ...trip, itinerary: newItinerary }); + withDays(days => { + days[dayIndex] = { ...days[dayIndex], notes: note }; + return days; + }); }; const handleAddDay = () => { if (!trip) return; - const newItinerary = { ...trip.itinerary }; - const nextDayNum = newItinerary.days.length + 1; - newItinerary.days.push({ - day: nextDayNum, - items: [] - }); - handleUpdateTrip({ ...trip, itinerary: newItinerary }); - toast.success(`Gün ${nextDayNum} eklendi`); - setSelectedDayIndex(newItinerary.days.length - 1); + const nextNum = trip.itinerary.days.length + 1; + withDays(days => [...days, { day: nextNum, items: [] }]); + toast.success(`Gün ${nextNum} eklendi`); + setSelectedDayIndex(trip.itinerary.days.length); // new day }; - const handleShare = () => { - const url = window.location.href; - navigator.clipboard.writeText(url); - toast.success('Link kopyalandı'); + const handleShare = async () => { + try { + await navigator.clipboard.writeText(window.location.href); + toast.success('Link panoya kopyalandı'); + } catch { + toast.error('Kopyalama başarısız'); + } }; const handleDelete = async () => { if (!trip) return; - setIsDeleting(true); try { await api.deleteTrip(trip.id); toast.success('Gezi silindi'); navigate('/account'); - } catch (error) { + } catch { toast.error('Gezi silinemedi'); - setIsDeleting(false); } }; - const totalPlaces = useMemo(() => - trip?.itinerary.days.reduce((sum, day) => sum + (day.items?.length || 0), 0) || 0, - [trip] - ); + // ── Computed ────────────────────────────────────────────────────────────── + const getDayDate = useCallback((idx: number) => { + if (!trip) return ''; + try { + return format(addDays(new Date(trip.start_date), idx), 'd MMM', { locale: tr }); + } catch { + return ''; + } + }, [trip]); + const dayStats = useMemo(() => { + if (!trip) return null; + const day = trip.itinerary.days[selectedDayIndex]; + if (!day) return null; + const mins = day.items.reduce((s, p) => s + (p.estimated_duration_minutes || 60), 0); + const h = Math.floor(mins / 60), m = mins % 60; + return { + places: day.items.length, + duration: h > 0 ? `${h}s${m > 0 ? ` ${m}dk` : ''}` : `${m}dk`, + }; + }, [trip, selectedDayIndex]); + + // ── Render ──────────────────────────────────────────────────────────────── if (loading) { return ( -
- +
+
+
+ +
+

Rota yükleniyor...

); } if (!trip) return null; + const selectedDay = trip.itinerary.days[selectedDayIndex]; + return (
- {/* Sub-Header Navbar */} -
-
-
- - + +
-
-

- {trip.title} - -

+ +
+ +
+

{trip.title}

+ +
+ + {/* Save indicator */} + + {saveStatus === 'saving' && ( + + Kaydediliyor... + + )} + {saveStatus === 'saved' && ( + + Kaydedildi + + )} + {saveStatus === 'unsaved' && ( + + )} +
-
- - + )} + + - + + + + Geziyi sil? + + "{trip.title}" kalıcı olarak silinecek. Bu işlem geri alınamaz. + + + + Vazgeç + + Evet, Sil + + + + + +
+ + -
- - -
+ {/* ── Main ──────────────────────────────────────────────────────────── */}
- {/* Left Sidebar: Days Selection */} -
); -} +} \ No newline at end of file