diff --git a/app-9xzmfic2e4g1/src/constants/planner.ts b/app-9xzmfic2e4g1/src/constants/planner.ts index 8921aed..f12c9e7 100644 --- a/app-9xzmfic2e4g1/src/constants/planner.ts +++ b/app-9xzmfic2e4g1/src/constants/planner.ts @@ -1,23 +1,529 @@ -import { Hotel, Home, Navigation, Plane, Trees, Building2, Camera, Bike, UtensilsCrossed } from 'lucide-react'; +import { useState, useMemo, useCallback, memo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import api from '@/db/api'; +import { Label } from '@/components/ui/label'; +import { Form, FormField, FormItem, FormMessage } from '@/components/ui/form'; +import { toast } from 'sonner'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod' +import * as z from 'zod'; +import { format, differenceInDays } from 'date-fns'; +import { + Loader2, ArrowRight, ArrowLeft, Sparkles, + MapPin, Calendar, Users, Coffee, Heart, + Car, Wallet, CheckCircle2, ChevronRight, + PersonStanding, +} from 'lucide-react'; +import { parseApiError } from '@/utils/errorHandler'; +import { retryWithBackoff, withTimeout } from '@/utils/retryWithBackoff'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; -export const ACCOMMODATION_OPTIONS = [ - { id: 'hotel', label: 'Otel', icon: Hotel }, - { id: 'airbnb', label: 'Airbnb / Adres', icon: Home }, - { id: 'center', label: 'Merkezden Başla', icon: Navigation }, +import { LOADING_STEPS, TRAVEL_TYPE_OPTIONS, BUDGET_OPTIONS, TRANSPORT_OPTIONS, ACCOMMODATION_OPTIONS, INTEREST_OPTIONS } from '@/constants/planner'; +import { DateSelector } from '@/components/planner/DateSelector'; +import { TravelerInput } from '@/components/planner/TravelerInput'; +import { AccommodationSelector } from '@/components/planner/AccommodationSelector'; +import { InterestsGrid } from '@/components/planner/InterestsGrid'; +import { TravelTypeSelector } from '@/components/planner/TravelTypeSelector'; +import { TransportSelector } from '@/components/planner/TransportSelector'; +import { BudgetSelector } from '@/components/planner/BudgetSelector'; + +// ─── Schema ─────────────────────────────────────────────────────────────────── +const formSchema = z.object({ + dateRange: z.object({ + from: z.date({ required_error: 'Başlangıç tarihi gereklidir' }), + to: z.date({ required_error: 'Bitiş tarihi gereklidir' }), + }) + .refine(d => d.from >= new Date(new Date().setHours(0, 0, 0, 0)), { + message: 'Başlangıç tarihi bugünden önce olamaz', + }) + .refine(d => d.to > d.from, { + message: 'Bitiş tarihi başlangıç tarihinden sonra olmalıdır', + }) + .refine(d => { + const days = differenceInDays(d.to, d.from) + 1; + return days >= 1 && days <= 14; + }, { message: 'Seyahat süresi 1–14 gün arasında olmalıdır' }), + travelType: z.string().min(1, 'Seyahat tipi seçiniz'), + travelers: z.number().min(1).max(15), + accommodation: z.string(), + transport: z.string().min(1, 'Ulaşım tercihi seçiniz'), + budget: z.string().min(1, 'Bütçe aralığı seçiniz'), + interests: z.array(z.string()).min(1, 'En az 1 ilgi alanı seçiniz').max(6), +}); + +type FormValues = z.infer; + +// ─── Steps ──────────────────────────────────────────────────────────────────── +const STEPS = [ + { id: 'dates', title: 'Tarihler', icon: Calendar, description: 'Ne zaman gidiyorsunuz?' }, + { id: 'travelType', title: 'Seyahat Tipi', icon: PersonStanding, description: 'Nasıl bir seyahat?' }, + { id: 'travelers', title: 'Grup & Konaklama', icon: Users, description: 'Kiminle, nerede kalıyorsunuz?' }, + { id: 'transport', title: 'Ulaşım', icon: Car, description: 'Nasıl seyahat edeceksiniz?' }, + { id: 'budget', title: 'Bütçe', icon: Wallet, description: 'Ne kadar harcamayı planlıyorsunuz?' }, + { id: 'interests', title: 'İlgi Alanları', icon: Heart, description: 'Neleri keşfetmek istersiniz?' }, ] as const; -export const INTEREST_OPTIONS = [ - { id: 'balloon', label: 'Sıcak Hava Balonu', icon: Plane }, - { id: 'nature', label: 'Doğa ve Yürüyüş', icon: Trees }, - { id: 'history', label: 'Tarih ve Kültür', icon: Building2 }, - { id: 'photography', label: 'Fotoğraf Çekimi', icon: Camera }, - { id: 'adventure', label: 'Aktivite/Macera', icon: Bike }, - { id: 'gastronomy', label: 'Gastronomi', icon: UtensilsCrossed }, -] as const; +// ─── Summary label helpers ──────────────────────────────────────────────────── +function getSummaryLabel(stepId: string, values: Partial): string | null { + switch (stepId) { + case 'dates': + if (values.dateRange?.from && values.dateRange?.to) + return `${format(values.dateRange.from, 'd MMM')} – ${format(values.dateRange.to, 'd MMM')}`; + return null; + case 'travelType': + return TRAVEL_TYPE_OPTIONS.find(o => o.id === values.travelType)?.label ?? null; + case 'travelers': + return values.travelers ? `${values.travelers} kişi` : null; + case 'transport': + return TRANSPORT_OPTIONS.find(o => o.id === values.transport)?.label ?? null; + case 'budget': + return BUDGET_OPTIONS.find(o => o.id === values.budget)?.label ?? null; + case 'interests': + return values.interests?.length ? `${values.interests.length} seçildi` : null; + default: + return null; + } +} -export const LOADING_STEPS = [ - { label: 'Form hazırlanıyor...', progress: 0 }, - { label: 'Rotanız oluşturuluyor...', progress: 33 }, - { label: 'Mekanlar belirleniyor...', progress: 66 }, - { label: 'Son kontroller yapılıyor...', progress: 90 }, -] as const; +// ─── PlannerPage ────────────────────────────────────────────────────────────── +const PlannerPage = () => { + const { user } = useAuth(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [loadingStep, setLoadingStep] = useState(0); + const [currentStep, setCurrentStep] = useState(0); + const [datePickerOpen, setDatePickerOpen] = useState(false); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + dateRange: { from: undefined, to: undefined }, + travelType: '', + travelers: 2, + accommodation: 'center', + transport: '', + budget: '', + interests: [], + }, + }); + + const watchedValues = form.watch(); + + // ── Navigation ────────────────────────────────────────────────────────────── + const nextStep = async () => { + const stepId = STEPS[currentStep].id; + const fieldMap: Record = { + dates: 'dateRange', + travelType: 'travelType', + travelers: ['travelers', 'accommodation'], + transport: 'transport', + budget: 'budget', + interests: 'interests', + }; + const fields = fieldMap[stepId]; + const isValid = await form.trigger(Array.isArray(fields) ? fields : [fields]); + if (isValid && currentStep < STEPS.length - 1) setCurrentStep(p => p + 1); + }; + + const prevStep = () => { if (currentStep > 0) setCurrentStep(p => p - 1); }; + + const handleInterestToggle = useCallback((id: string) => { + const current = form.getValues('interests'); + const next = current.includes(id) ? current.filter(i => i !== id) : [...current, id]; + form.setValue('interests', next, { shouldValidate: true }); + }, [form]); + + const simulateLoadingSteps = useCallback(() => { + setLoadingStep(0); + const iv = setInterval(() => { + setLoadingStep(prev => { + if (prev < LOADING_STEPS.length - 1) return prev + 1; + clearInterval(iv); + return prev; + }); + }, 2500); + return iv; + }, []); + + // ── Submit ────────────────────────────────────────────────────────────────── + const onSubmit = async (data: FormValues) => { + setLoading(true); + const iv = simulateLoadingSteps(); + try { + const startDate = format(data.dateRange.from, 'yyyy-MM-dd'); + const endDate = format(data.dateRange.to, 'yyyy-MM-dd'); + const result = await retryWithBackoff( + () => withTimeout( + api.generateItinerary({ + startDate, endDate, + interests: data.interests, + dailySchedule: 'moderate', + preferences: `Type:${data.travelType}, Accommodation:${data.accommodation}, Transport:${data.transport}, Budget:${data.budget}, Travelers:${data.travelers}`, + }), + 45000, + new Error('Sunucu yanıt vermiyor, lütfen tekrar deneyin.') + ), + { maxRetries: 2, initialDelay: 1000, maxDelay: 5000 } + ); + clearInterval(iv); + if (user) { + const saved = await api.saveTrip({ + user_id: user.id, + title: 'Kapadokya Gezisi', + destination: 'Cappadocia', + start_date: startDate, + end_date: endDate, + preferences: { startDate, endDate, interests: data.interests, dailySchedule: 'moderate', preferences: '' }, + itinerary: result, + }); + navigate(`/trip/${saved.id}`); + toast.success('Rotanız hazır!'); + } else { + sessionStorage.setItem('pending_trip', JSON.stringify(result)); + navigate('/login', { state: { from: '/planner', message: 'Planınızı kaydetmek için giriş yapın' } }); + } + } catch (err) { + clearInterval(iv); + if (err instanceof Error && err.name === 'AbortError') return; + toast.error('Hata oluştu', { description: parseApiError(err).userMessage }); + } finally { + setLoading(false); + setLoadingStep(0); + } + }; + + const progress = ((currentStep + 1) / STEPS.length) * 100; + + // ── Loading screen ────────────────────────────────────────────────────────── + if (loading) { + return ( +
+
+ +
+
+ +
+ +
+
+ +
+ + +
+ + + {LOADING_STEPS[loadingStep].label} + + + +
+ +
+ +
+ Hazırlanıyor + {LOADING_STEPS[loadingStep].progress}% +
+
+ +

+ Size özel Kapadokya efsanesi kurguluyoruz... +

+
+
+ ); + } + + // ── Main layout ───────────────────────────────────────────────────────────── + return ( +
+ + {/* ── Sidebar ─────────────────────────────────────────────────────────── */} +
+ {/* Decorative orbs */} +
+
+ +
+ {/* Logo */} +
+
+ +
+ + Kapadokya Efsanesi + +
+ + {/* Headline */} +
+

+ ROTANIZI
TASARLAYIN +

+

+ "Size özel kurgulanmış seyahat mimarisi." +

+
+ + {/* Step list */} +
+ {STEPS.map((step, i) => { + const Icon = step.icon; + const isActive = i === currentStep; + const isCompleted = i < currentStep; + const summaryLabel = getSummaryLabel(step.id, watchedValues); + + return ( + + {/* Step indicator */} +
+ {isCompleted + ? + : + } +
+ + {/* Labels */} +
+
+ {String(i + 1).padStart(2, '0')} +
+
+ {step.title} +
+
+ + {/* Summary pill */} + {isCompleted && summaryLabel && ( + + {summaryLabel} + + )} + + {isActive && ( + + )} +
+ ); + })} +
+ + {/* Progress bar */} +
+
+ İlerleme + {Math.round(progress)}% +
+
+ +
+
+
+
+ + {/* ── Main Form Area ──────────────────────────────────────────────────── */} +
+
+
+ + + + + {/* Step header */} +
+
+ Adım {currentStep + 1}/{STEPS.length} + + {STEPS[currentStep].title} +
+

+ {STEPS[currentStep].description} +

+
+ + {/* Step content */} +
+ + {/* Step 0 — Dates */} + {currentStep === 0 && ( + ( + + + + + + )} /> + )} + + {/* Step 1 — Travel Type */} + {currentStep === 1 && ( + ( + + + + + + )} /> + )} + + {/* Step 2 — Travelers & Accommodation */} + {currentStep === 2 && ( +
+ ( + + + + + + )} /> + ( + + + + + + )} /> +
+ )} + + {/* Step 3 — Transport */} + {currentStep === 3 && ( + ( + + + + + + )} /> + )} + + {/* Step 4 — Budget */} + {currentStep === 4 && ( + ( + + + + + + )} /> + )} + + {/* Step 5 — Interests */} + {currentStep === 5 && ( + ( + +
+ + {field.value.length}/6 Seçildi +
+ + +
+ )} /> + )} +
+
+
+ + {/* Navigation */} +
+ + + {currentStep < STEPS.length - 1 ? ( + + ) : ( + + )} +
+
+ +
+
+
+ ); +}; + +export default memo(PlannerPage); \ No newline at end of file