diff --git a/app-9xzmfic2e4g1/src/constants/planner.ts b/app-9xzmfic2e4g1/src/constants/planner.ts index f12c9e7..73b6012 100644 --- a/app-9xzmfic2e4g1/src/constants/planner.ts +++ b/app-9xzmfic2e4g1/src/constants/planner.ts @@ -1,529 +1,41 @@ -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 LOADING_STEPS = [ + { label: 'Seyahat verileri toplanıyor...', progress: 10 }, + { label: 'Kapadokya rotaları oluşturuluyor...', progress: 35 }, + { label: 'En iyi deneyimler seçiliyor...', progress: 60 }, + { label: 'Ulaşım rotaları hesaplanıyor...', progress: 85 }, + { label: 'Son dokunuşlar yapılıyor...', progress: 100 }, +]; -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'; +export const TRAVEL_TYPE_OPTIONS = [ + { id: 'leisure', label: 'Dinlenme' }, + { id: 'adventure', label: 'Macera' }, + { id: 'culture', label: 'Kültür' }, + { id: 'family', label: 'Aile' }, +]; -// ─── 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), -}); +export const BUDGET_OPTIONS = [ + { id: 'budget', label: 'Ekonomik' }, + { id: 'moderate', label: 'Orta' }, + { id: 'luxury', label: 'Lüks' }, +]; -type FormValues = z.infer; +export const TRANSPORT_OPTIONS = [ + { id: 'car', label: 'Araç Kiralama' }, + { id: 'taxi', label: 'Özel Transfer' }, + { id: 'public', label: 'Toplu Taşıma' }, +]; -// ─── 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 ACCOMMODATION_OPTIONS = [ + { id: 'hotel', label: 'Otel' }, + { id: 'cave', label: 'Mağara Otel' }, + { id: 'boutique', label: 'Butik' }, +]; -// ─── 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; - } -} - -// ─── 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 +export const INTEREST_OPTIONS = [ + { id: 'history', label: 'Tarih' }, + { id: 'nature', label: 'Doğa' }, + { id: 'gastronomy', label: 'Gastronomi' }, + { id: 'photography', label: 'Fotoğrafçılık' }, + { id: 'adventure', label: 'Macera' }, + { id: 'art', label: 'Sanat' }, +];