From b7bc6b853a759a5ac970e82023417d2942c19373 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 4 Mar 2026 13:48:42 +0000 Subject: [PATCH] Revert to version 773d823 --- app-9xzmfic2e4g1/.env | 9 +- .../src/components/layout/Navbar.tsx | 97 +-- .../components/planner/LeadCaptureModal.tsx | 268 ------- app-9xzmfic2e4g1/src/components/trip/Map.tsx | 19 +- app-9xzmfic2e4g1/src/constants/planner.ts | 676 +++++++++++++----- app-9xzmfic2e4g1/src/contexts/AuthContext.tsx | 16 +- .../src/domain/lead/leadService.ts | 155 ---- app-9xzmfic2e4g1/src/pages/AccountPage.tsx | 144 ++-- app-9xzmfic2e4g1/src/pages/ExplorePage.tsx | 201 +++--- app-9xzmfic2e4g1/src/pages/LandingPage.tsx | 337 ++++----- app-9xzmfic2e4g1/src/pages/LoginPage.tsx | 36 +- app-9xzmfic2e4g1/src/types/lead.ts | 45 -- .../migrations/00007_lead_marketplace.sql | 98 --- .../migrations/00008_seed_operators.sql | 10 - .../00009_create_place_details_cache.sql | 6 - 15 files changed, 983 insertions(+), 1134 deletions(-) delete mode 100644 app-9xzmfic2e4g1/src/components/planner/LeadCaptureModal.tsx delete mode 100644 app-9xzmfic2e4g1/src/domain/lead/leadService.ts delete mode 100644 app-9xzmfic2e4g1/src/types/lead.ts delete mode 100644 app-9xzmfic2e4g1/supabase/migrations/00007_lead_marketplace.sql delete mode 100644 app-9xzmfic2e4g1/supabase/migrations/00008_seed_operators.sql delete mode 100644 app-9xzmfic2e4g1/supabase/migrations/00009_create_place_details_cache.sql diff --git a/app-9xzmfic2e4g1/.env b/app-9xzmfic2e4g1/.env index 2aa9d48..fe0e137 100644 --- a/app-9xzmfic2e4g1/.env +++ b/app-9xzmfic2e4g1/.env @@ -1,4 +1,5 @@ -VITE_SUPABASE_URL=https://bhaumxerateojqvleoyw.supabase.co -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJoYXVteGVyYXRlb2pxdmxlb3l3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjczNTQ4OTksImV4cCI6MjA4MjkzMDg5OX0.uyF3i17AaNd6CN5yWrGMR4vFsJ-boPrfKZYByXKBqUE -VITE_GOOGLE_MAPS_API_KEY=AIzaSyBbXWk7VhZfzn9txrAr9N-faAPuKy_LnKw -OPENAI_API_KEY=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 +VITE_SUPABASE_URL=https://ofqojaxiopqxahfvxpmx.supabase.co +VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9mcW9qYXhpb3BxeGFoZnZ4cG14Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIyODExMjAsImV4cCI6MjA4Nzg1NzEyMH0.CVyjWPp9ldCd5qxA4TbViD5MJ0axbEWfGr-1n1pPjn0 +VITE_GOOGLE_MAPS_API_KEY=AIzaSyCLPiqNWwFSUS0X15YvTdHZxrb-2LXoYlw +VITE_APP_ID=app-9xzmfic2e4g1 +VITE_FORM_ID=form-9xzmfic2e4g1 \ No newline at end of file diff --git a/app-9xzmfic2e4g1/src/components/layout/Navbar.tsx b/app-9xzmfic2e4g1/src/components/layout/Navbar.tsx index aa87a8e..fbcdbeb 100644 --- a/app-9xzmfic2e4g1/src/components/layout/Navbar.tsx +++ b/app-9xzmfic2e4g1/src/components/layout/Navbar.tsx @@ -2,7 +2,8 @@ import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '@/contexts/AuthContext'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { MapPin, History, User, LogOut, Search, Settings } from 'lucide-react'; +import { Separator } from '@/components/ui/separator'; +import { MapPin, History, Compass, User, LogOut, Search, Settings, FileText } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -23,6 +24,7 @@ export function Navbar() { const [searchOpen, setSearchOpen] = useState(false); const [scrolled, setScrolled] = useState(false); + // Scroll efekti için useEffect(() => { const handleScroll = () => { setScrolled(window.scrollY > 10); @@ -38,113 +40,117 @@ export function Navbar() { return ( <> + {/* Navbar yüksekliği kadar boşluk bırak */}
+ {/* Arama modalı */} ); diff --git a/app-9xzmfic2e4g1/src/components/planner/LeadCaptureModal.tsx b/app-9xzmfic2e4g1/src/components/planner/LeadCaptureModal.tsx deleted file mode 100644 index d2619a3..0000000 --- a/app-9xzmfic2e4g1/src/components/planner/LeadCaptureModal.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import * as z from 'zod'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { - Form, - FormField, - FormItem, - FormLabel, - FormControl, - FormMessage -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from '@/components/ui/select'; -import { Checkbox } from '@/components/ui/checkbox'; -import { leadService } from '@/domain/lead/leadService'; -import { LeadCategory } from '@/types/lead'; -import { toast } from 'sonner'; -import { Loader2, MessageCircle, Send } from 'lucide-react'; - -const leadFormSchema = z.object({ - full_name: z.string().min(2, 'İsim en az 2 karakter olmalıdır'), - phone: z.string().min(10, 'Geçerli bir telefon numarası giriniz'), - whatsapp_available: z.boolean(), - participants: z.number().min(1).max(50), - budget_range: z.string().optional(), - preferred_date: z.string().optional(), -}); - -type LeadFormValues = { - full_name: string; - phone: string; - whatsapp_available: boolean; - participants: number; - budget_range?: string; - preferred_date?: string; -}; - -interface LeadCaptureModalProps { - isOpen: boolean; - onClose: () => void; - category: LeadCategory; - categoryLabel: string; - userId?: string; - tripId?: string; - defaultParticipants?: number; - defaultDate?: string; - tripDurationDays?: number; -} - -export const LeadCaptureModal = ({ - isOpen, - onClose, - category, - categoryLabel, - userId, - tripId, - defaultParticipants = 2, - defaultDate, - tripDurationDays = 0 -}: LeadCaptureModalProps) => { - const [isSubmitting, setIsSubmitting] = useState(false); - - const form = useForm({ - resolver: zodResolver(leadFormSchema), - defaultValues: { - full_name: '', - phone: '', - whatsapp_available: true, - participants: defaultParticipants, - budget_range: 'moderate', - preferred_date: defaultDate || '', - }, - }); - - const onSubmit = async (data: LeadFormValues) => { - setIsSubmitting(true); - try { - await leadService.createLead({ - full_name: data.full_name, - phone: data.phone, - whatsapp_available: data.whatsapp_available, - participants: data.participants, - budget_range: data.budget_range, - preferred_date: data.preferred_date, - category, - user_id: userId, - trip_id: tripId, - }, tripDurationDays); - - toast.success('Talebiniz alındı!', { - description: 'En kısa sürede WhatsApp üzerinden sizinle iletişime geçilecektir.' - }); - onClose(); - } catch (error) { - console.error('Error creating lead:', error); - toast.error('Talep gönderilirken bir hata oluştu.'); - } finally { - setIsSubmitting(false); - } - }; - - const getTitle = () => { - switch (category) { - case 'balloon': return 'Balon Turu İçin Özel Teklif Alın'; - case 'atv': - case 'horse': return 'Aktiviteler İçin En İyi Fiyatı Alın'; - case 'hotel': return 'WhatsApp\'a Özel Otel İndirimi Alın'; - default: return `${categoryLabel} İçin Teklif Alın`; - } - }; - - const getDescription = () => { - switch (category) { - case 'balloon': return 'Kapadokya\'nın en güvenilir operatörlerinden size özel fiyat teklifleri hazırlayalım.'; - case 'hotel': return 'Seçtiğiniz tarihler için otellerden size özel indirimli fiyatlar alın.'; - default: return 'Yerel uzmanlarımız en iyi seçenekleri sizin için değerlendirsin.'; - } - }; - - return ( - - -
- - - {getTitle()} - - - {getDescription()} - - -
- -
- -
- ( - - Ad Soyad - - - - - - )} - /> - - ( - - Telefon / WhatsApp - - - - - - )} - /> - - ( - - Kişi Sayısı - - field.onChange(parseInt(e.target.value) || 0)} - onBlur={field.onBlur} - name={field.name} - ref={field.ref} - className="h-12 rounded-xl bg-zinc-50 border-zinc-100" - /> - - - - )} - /> - - ( - - Bütçe Aralığı - - - - )} - /> - - ( - - - - -
- - - WhatsApp üzerinden dönüş almak istiyorum - -
-
- )} - /> -
- - - - -
- -
-
- ); -}; \ No newline at end of file diff --git a/app-9xzmfic2e4g1/src/components/trip/Map.tsx b/app-9xzmfic2e4g1/src/components/trip/Map.tsx index 44b1da6..d1051d2 100644 --- a/app-9xzmfic2e4g1/src/components/trip/Map.tsx +++ b/app-9xzmfic2e4g1/src/components/trip/Map.tsx @@ -77,8 +77,19 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: const fetchPlaceDetail = useCallback(async (poi: SelectedPOI) => { setDetailLoading(true); setPlaceDetail(null); - setActiveTab("about"); - setDetailLoading(false); + setActiveTab('about'); + try { + const data = await api.getPlaceDetails({ + place_id: poi.place_id, + name: poi.name, + category: poi.category, + }); + setPlaceDetail(data); + } catch (e) { + console.error('Place detail fetch error:', e); + } finally { + setDetailLoading(false); + } }, []); // ── Handle add ──────────────────────────────────────────────────────────── @@ -493,14 +504,14 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }: )} {/* Çalışma saatleri */} - {placeDetail.opening_hours && placeDetail.opening_hours.length > 0 && ( + {placeDetail.opening_hours?.length > 0 && (

Çalışma Saatleri

- {placeDetail.opening_hours?.map((h, i) => ( + {placeDetail.opening_hours.map((h, i) => (

{h}

))}
diff --git a/app-9xzmfic2e4g1/src/constants/planner.ts b/app-9xzmfic2e4g1/src/constants/planner.ts index 4172daa..f12c9e7 100644 --- a/app-9xzmfic2e4g1/src/constants/planner.ts +++ b/app-9xzmfic2e4g1/src/constants/planner.ts @@ -1,169 +1,63 @@ +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 { - Hotel, Home, Navigation, Plane, Trees, Building2, Camera, - Bike, UtensilsCrossed, Users, Heart, Baby, - Car, Bus, Shuffle, Gem, Wallet, CreditCard, Crown, - PersonStanding, Calendar + 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', - description: 'Konforlu & servis odaklı', - icon: Hotel, - }, - { - id: 'airbnb', - label: 'Airbnb / Ev', - description: 'Yerel & özgün deneyim', - icon: Home, - }, - { - id: 'center', - label: 'Merkezi', - description: 'Her şeye yakın konum', - icon: Navigation, - }, -] as const; +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: 'solo', - label: 'Solo', - description: 'Kendi tempomda özgür', - icon: PersonStanding, - gradient: 'from-violet-500 to-purple-600', - bg: 'bg-violet-50', - border: 'border-violet-200', - text: 'text-violet-600', - }, - { - id: 'couple', - label: 'Çift', - description: 'Romantik & özel anlar', - icon: Heart, - gradient: 'from-rose-500 to-pink-600', - bg: 'bg-rose-50', - border: 'border-rose-200', - text: 'text-rose-600', - }, - { - id: 'family', - label: 'Aile', - description: 'Çocuklar dahil herkes', - icon: Baby, - gradient: 'from-amber-500 to-orange-500', - bg: 'bg-amber-50', - border: 'border-amber-200', - text: 'text-amber-600', - }, - { - id: 'friends', - label: 'Arkadaşlar', - description: 'Grup enerjisi & macera', - icon: Users, - gradient: 'from-emerald-500 to-teal-600', - bg: 'bg-emerald-50', - border: 'border-emerald-200', - text: 'text-emerald-600', - }, -] as const; +// ─── 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 TRANSPORT_OPTIONS = [ - { - id: 'rental', - label: 'Araç Kiralama', - description: 'Özgür & esnek rota', - icon: Car, - }, - { - id: 'transfer', - label: 'Özel Transfer', - description: 'Konforlu & zahmetsiz', - icon: Gem, - }, - { - id: 'shuttle', - label: 'Servis / Minibüs', - description: 'Ekonomik grup ulaşımı', - icon: Bus, - }, - { - id: 'mixed', - label: 'Karma', - description: 'Duruma göre en iyisi', - icon: Shuffle, - }, -] as const; +type FormValues = z.infer; -export const BUDGET_OPTIONS = [ - { - id: 'budget', - label: 'Ekonomik', - description: 'Akıllı harcama, tam deneyim', - range: '₺500 – ₺1.000 / gün', - icon: Wallet, - tier: 1, - color: 'text-sky-600', - activeBg: 'bg-sky-50', - activeBorder: 'border-sky-400', - dot: 'bg-sky-400', - }, - { - id: 'moderate', - label: 'Orta', - description: 'Kalite & tasarruf dengesi', - range: '₺1.000 – ₺2.500 / gün', - icon: CreditCard, - tier: 2, - color: 'text-emerald-600', - activeBg: 'bg-emerald-50', - activeBorder: 'border-emerald-400', - dot: 'bg-emerald-400', - }, - { - id: 'comfort', - label: 'Konforlu', - description: 'Ekstra konfor & özel deneyim', - range: '₺2.500 – ₺5.000 / gün', - icon: Gem, - tier: 3, - color: 'text-orange-600', - activeBg: 'bg-orange-50', - activeBorder: 'border-orange-400', - dot: 'bg-orange-400', - }, - { - id: 'luxury', - label: 'Lüks', - description: 'Sınırsız & ayrıcalıklı', - range: '₺5.000+ / gün', - icon: Crown, - tier: 4, - color: 'text-amber-600', - activeBg: 'bg-amber-50', - activeBorder: 'border-amber-400', - dot: 'bg-amber-400', - }, -] as const; - -export const INTEREST_OPTIONS = [ - { id: 'balloon', label: 'Sıcak Hava Balonu', icon: Plane }, - { id: 'nature', label: 'Doğa & Yürüyüş', icon: Trees }, - { id: 'history', label: 'Tarih & Kültür', icon: Building2 }, - { id: 'photography', label: 'Fotoğraf', icon: Camera }, - { id: 'adventure', label: 'Macera', icon: Bike }, - { id: 'gastronomy', label: 'Gastronomi', icon: UtensilsCrossed }, -] as const; - -export const LOADING_STEPS = [ - { label: 'Tercihleriniz analiz ediliyor...', progress: 0 }, - { label: 'Rotanız oluşturuluyor...', progress: 33 }, - { label: 'Mekanlar seçiliyor...', progress: 66 }, - { label: 'Son rötuşlar yapılıyor...', progress: 90 }, -] as const; - -export const STEPS = [ +// ─── 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?' }, @@ -171,3 +65,465 @@ export const STEPS = [ { 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; + +// ─── 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 diff --git a/app-9xzmfic2e4g1/src/contexts/AuthContext.tsx b/app-9xzmfic2e4g1/src/contexts/AuthContext.tsx index 86606d0..4d55b09 100644 --- a/app-9xzmfic2e4g1/src/contexts/AuthContext.tsx +++ b/app-9xzmfic2e4g1/src/contexts/AuthContext.tsx @@ -20,8 +20,8 @@ interface AuthContextType { user: User | null; profile: Profile | null; loading: boolean; - signIn: (email: string, password: string) => Promise<{ error: Error | null }>; - signUp: (email: string, password: string) => Promise<{ error: Error | null }>; + signInWithUsername: (username: string, password: string) => Promise<{ error: Error | null }>; + signUpWithUsername: (username: string, password: string) => Promise<{ error: Error | null }>; signOut: () => Promise; refreshProfile: () => Promise; } @@ -51,7 +51,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { } setLoading(false); }); - + // In this function, do NOT use any await calls. Use `.then()` instead to avoid deadlocks. const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { setUser(session?.user ?? null); if (session?.user) { @@ -64,8 +64,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { return () => subscription.unsubscribe(); }, []); - const signIn = async (email: string, password: string) => { + const signInWithUsername = async (username: string, password: string) => { try { + const email = `${username}@miaoda.com`; const { error } = await supabase.auth.signInWithPassword({ email, password, @@ -78,8 +79,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }; - const signUp = async (email: string, password: string) => { + const signUpWithUsername = async (username: string, password: string) => { try { + const email = `${username}@miaoda.com`; const { error } = await supabase.auth.signUp({ email, password, @@ -99,7 +101,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { }; return ( - + {children} ); @@ -111,4 +113,4 @@ export function useAuth() { throw new Error('useAuth must be used within an AuthProvider'); } return context; -} \ No newline at end of file +} diff --git a/app-9xzmfic2e4g1/src/domain/lead/leadService.ts b/app-9xzmfic2e4g1/src/domain/lead/leadService.ts deleted file mode 100644 index cb3b4db..0000000 --- a/app-9xzmfic2e4g1/src/domain/lead/leadService.ts +++ /dev/null @@ -1,155 +0,0 @@ - -import { supabase } from '../../db/supabase'; -import { Lead, LeadCategory, Operator, LeadStatus } from '../../types/lead'; - -export const leadService = { - /** - * Calculates a lead score based on the criteria: - * - 2+ kişi: +10 - * - 3+ gün kalış: +15 - * - Balon seçmiş: +25 - * - Budget belirtilmiş: +20 - * - Tarih net: +15 - */ - scoreLead(lead: Partial, tripDurationDays: number = 0): number { - let score = 0; - - // 2+ kişi: +10 - if (lead.participants && lead.participants >= 2) { - score += 10; - } - - // 3+ gün kalış: +15 (if trip context is provided) - if (tripDurationDays >= 3) { - score += 15; - } - - // Balon seçmiş: +25 - if (lead.category === 'balloon') { - score += 25; - } - - // Budget belirtilmiş: +20 - if (lead.budget_range && lead.budget_range !== 'not_sure') { - score += 20; - } - - // Tarih net: +15 - if (lead.preferred_date) { - score += 15; - } - - return score; - }, - - /** - * Creates a new lead in the database and triggers scoring and assignment. - */ - async createLead(leadData: Partial, tripDurationDays: number = 0): Promise { - const score = this.scoreLead(leadData, tripDurationDays); - - const { data: lead, error } = await supabase - .from('leads') - .insert({ - ...leadData, - score, - status: 'new' as LeadStatus - }) - .select() - .single(); - - if (error) throw error; - - // Log creation event - await this.logEvent(lead.id, 'created', { score }); - - // Auto-assign operator - const assignedOperator = await this.assignOperator(lead); - - if (assignedOperator) { - const { data: updatedLead, error: updateError } = await supabase - .from('leads') - .update({ - assigned_operator_id: assignedOperator.id, - status: 'assigned' as LeadStatus - }) - .eq('id', lead.id) - .select() - .single(); - - if (updateError) throw updateError; - - await this.logEvent(lead.id, 'assigned', { operator_id: assignedOperator.id }); - await this.notifyOperator(assignedOperator, updatedLead); - - return updatedLead; - } - - return lead; - }, - - /** - * Assigns the best available operator based on category, active status, daily limit, and priority. - */ - async assignOperator(lead: Lead): Promise { - // 1. Get active operators for the category - const { data: operators, error } = await supabase - .from('operators') - .select('*') - .eq('category', lead.category) - .eq('active', true) - .order('priority_score', { ascending: false }); - - if (error) throw error; - if (!operators || operators.length === 0) return null; - - // 2. Check daily limits (simplified: count leads assigned today) - const today = new Date().toISOString().split('T')[0]; - - for (const operator of operators) { - const { count, error: countError } = await supabase - .from('leads') - .select('*', { count: 'exact', head: true }) - .eq('assigned_operator_id', operator.id) - .gte('created_at', today); - - if (countError) continue; - - if ((count || 0) < operator.max_daily_leads) { - return operator; - } - } - - return null; - }, - - /** - * Notifies the operator via WhatsApp (simulated/template link) or email. - */ - async notifyOperator(operator: Operator, lead: Lead) { - const message = `Yeni ${lead.category.toUpperCase()} Lead:\nTarih: ${lead.preferred_date}\nKişi: ${lead.participants}\nBütçe: ${lead.budget_range}\nAd: ${lead.full_name}\nTelefon: ${lead.phone}`; - - // In a real app, this would trigger an Edge Function or Twilio/WhatsApp API - console.log(`Notifying Operator ${operator.name} (${operator.whatsapp}): ${message}`); - - await this.logEvent(lead.id, 'notified', { - operator_id: operator.id, - method: operator.whatsapp ? 'whatsapp' : 'email' - }); - }, - - /** - * Logs events for analytics. - */ - async logEvent(leadId: string, eventType: string, metadata: any = {}) { - const { error } = await supabase - .from('lead_events') - .insert({ - lead_id: leadId, - event_type: eventType, - metadata - }); - - if (error) console.error('Error logging lead event:', error); - } -}; diff --git a/app-9xzmfic2e4g1/src/pages/AccountPage.tsx b/app-9xzmfic2e4g1/src/pages/AccountPage.tsx index 576a132..c41a99b 100644 --- a/app-9xzmfic2e4g1/src/pages/AccountPage.tsx +++ b/app-9xzmfic2e4g1/src/pages/AccountPage.tsx @@ -5,7 +5,7 @@ import api, { Trip } from '@/db/api'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { Loader2, Calendar, MapPin, Trash2, ChevronRight, PlusCircle, Zap, Compass as ExploreIcon, History, User } from 'lucide-react'; +import { Loader2, Calendar, MapPin, Trash2, ChevronRight, PlusCircle, Zap, Compass as ExploreIcon } from 'lucide-react'; import { format } from 'date-fns'; import { tr } from 'date-fns/locale'; import { toast } from 'sonner'; @@ -57,16 +57,23 @@ export default function AccountPage() { if (!user) { return ( -
- -
- +
+
+ bg +
+ +
+
-
-

HESABINIZ

-

Gezilerinizi görüntülemek ve yönetmek için giriş yapın.

+
+

HESABIM

+

Gezilerinizi yönetmek için giriş yapmalısınız.

- @@ -75,91 +82,93 @@ export default function AccountPage() { } return ( -
-
-
-
+
+
+
+
- - {trips.length} KAYITLI ROTA + Kişisel Arşiv + + {trips.length} Rota
-

+

GEZİLERİM

-

Kişisel Kapadokya arşiviniz.

+

Planladığınız Kapadokya efsaneleri.

- -
+
{loading ? ( -
- -

Veriler Getiriliyor...

+
+ +

Yükleniyor...

) : trips.length === 0 ? ( - -
- + +
+
-
-

HENÜZ PLAN YOK

-

- Henüz bir seyahat rotası oluşturmadınız. +

+

Planınız yok

+

+ İlk Kapadokya rotanızı oluşturun.

- ) : ( -
+
{trips.map((trip, idx) => ( - +
-
+
{trip.title} -
- +
+ + {trip.itinerary.days.length} GÜN
-
-
-
+
+
+
- {trip.destination} + {trip.destination}
-

+

{trip.title}

-
-
- +
+
+ {format(new Date(trip.start_date), 'd MMM yyyy', { locale: tr })}
@@ -167,34 +176,34 @@ export default function AccountPage() { - - - - GEZİYİ SİL? - - Bu rotayı kalıcı olarak silmek istediğinize emin misiniz? Bu işlem geri alınamaz. + + + Planı Sil? + + Silmek istediğinize emin misiniz? - - İptal + + Hayır handleDelete(trip.id)} - className="h-14 px-10 rounded-2xl font-black uppercase tracking-widest text-[11px] bg-red-600 hover:bg-red-700 text-white border-none shadow-lg shadow-red-600/20" + className="h-12 px-8 rounded-xl font-black bg-red-600 hover:bg-red-700 text-white" > - Silmeyi Onayla + Evet, Sil
-
-
- {trip.itinerary.days[0].items.slice(0, 4).map((item, i) => ( -
+
+
+ {trip.itinerary.days[0].items.slice(0, 3).map((item, i) => ( +
))} - {trip.itinerary.days[0].items.length > 4 && ( -
- +{trip.itinerary.days[0].items.length - 4} -
- )}
- @@ -227,4 +231,4 @@ export default function AccountPage() {
); -} +} \ No newline at end of file diff --git a/app-9xzmfic2e4g1/src/pages/ExplorePage.tsx b/app-9xzmfic2e4g1/src/pages/ExplorePage.tsx index 2293585..8e719d7 100644 --- a/app-9xzmfic2e4g1/src/pages/ExplorePage.tsx +++ b/app-9xzmfic2e4g1/src/pages/ExplorePage.tsx @@ -3,9 +3,8 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; -import { Search, Star, Clock, Filter, Sparkles, Heart, Share2, ArrowUpRight, Camera, MapPin, ChevronRight } from 'lucide-react'; +import { Search, Star, Clock, Filter, Sparkles, Heart, Share2, ArrowUpRight, Camera } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Link } from 'react-router-dom'; const CATEGORIES = [ { id: 'all', label: 'Tümü' }, @@ -24,7 +23,7 @@ const PLACES = [ rating: 4.8, reviews: 12400, duration: '2-3 Saat', - image: 'https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=800', + image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_2736e413-5912-4327-880c-f8494d625176.jpg', description: 'UNESCO Dünya Mirası listesinde yer alan, 10. ve 12. yüzyıllar arasında oyulmuş eşsiz kiliseler topluluğu.', }, { @@ -34,7 +33,7 @@ const PLACES = [ rating: 4.7, reviews: 8500, duration: '1 Saat', - image: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&q=80&w=800', + image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_8ea8dda5-57a3-4533-bd11-28440db86c34.jpg', description: 'Kapadokya\'s en yüksek noktası. Tüm bölgeyi panoramik olarak görebileceğiniz devasa bir doğal kaya kalesi.', }, { @@ -44,7 +43,7 @@ const PLACES = [ rating: 4.9, reviews: 15200, duration: '1.5 Saat', - image: 'https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=800', + image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_9c62deaa-2f09-44b8-a9e4-1d03ae155cce.jpg', description: 'Binlerce yıl önce insanların saldırılardan korunmak için inşa ettiği, yerin 85 metre altına kadar uzanan muazzam bir yapı.', }, { @@ -54,7 +53,7 @@ const PLACES = [ rating: 4.6, reviews: 6300, duration: '4-5 Saat', - image: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&q=80&w=800', + image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_a0a63c27-9e25-4f1a-89ef-aea901ec645d.jpg', description: 'Melendiz Çayı\'nın aşındırmasıyla oluşmuş 14 kilometrelik kanyon. İçerisinde onlarca kaya oyma kilise bulunur.', }, { @@ -64,7 +63,7 @@ const PLACES = [ rating: 4.8, reviews: 9100, duration: '1 Saat', - image: 'https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=800', + image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_8cfa89df-0b41-4cb0-9e72-f7c2e4ebda81.jpg', description: 'Kapadokya\'nın en karakteristik peribacalarının bulunduğu yer. Bazı peribacaları üç şapkalı yapıya sahiptir.', }, { @@ -74,7 +73,7 @@ const PLACES = [ rating: 4.5, reviews: 4200, duration: '1-2 Saat', - image: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&q=80&w=800', + image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_dd8b8233-fb7c-4dc3-90fe-eb24b5e136ee.jpg', description: 'Kızılırmak\'tan çıkan kızıl toprakla binlerce yıldır devam eden çömlekçilik geleneğini deneyimleyin.', } ]; @@ -91,58 +90,69 @@ export default function ExplorePage() { }); return ( -
- {/* Minimalist Header */} -
-
-
-
- - - KEŞİF REHBERİ - - - Kapadokya'yı
Keşfedin -
-
- - - - setSearchQuery(e.target.value)} - /> - -
+
+ {/* Immersive Header */} +
+
+ Explore Hero +
+
+ +
+ + + Efsanevi Duraklar + + + + KAPADOKYA KEŞFİ + + + + + setSearchQuery(e.target.value)} + /> +
-
+
{/* Categories Carousel */} -
+
+ +
{CATEGORIES.map((cat) => ( +
-
- - +
+ + {place.rating}
-
-
-

+
+
+

{place.name}

-

- {place.description} +

+ "{place.description}"

-
-
-
- +
+
+
+ {place.duration}
+
+ + Görsel +
- +
@@ -225,21 +251,28 @@ export default function ExplorePage() { key="empty" initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} - className="py-24 text-center space-y-6" + className="py-20 text-center space-y-6" > -
- +
+
-

SONUÇ YOK

-

- Aramanıza uygun mekan bulunamadı. +

BULUNAMADI

+

+ Farklı bir keşif terimi deneyin.

+ )}
); -} \ No newline at end of file +} diff --git a/app-9xzmfic2e4g1/src/pages/LandingPage.tsx b/app-9xzmfic2e4g1/src/pages/LandingPage.tsx index d6aaab1..c2604d4 100644 --- a/app-9xzmfic2e4g1/src/pages/LandingPage.tsx +++ b/app-9xzmfic2e4g1/src/pages/LandingPage.tsx @@ -1,7 +1,8 @@ import { Link } from 'react-router-dom'; import { Button } from '@/components/ui/button'; -import { Sparkles, MapPin, Calendar, Compass, ShieldCheck, Zap, ArrowRight, Star, Globe, ChevronRight } from 'lucide-react'; -import { motion } from 'framer-motion'; +import { Sparkles, MapPin, Calendar, Compass, ShieldCheck, Zap, ArrowRight, Star, ArrowUpRight, Play, Globe } from 'lucide-react'; +import { motion, useScroll, useTransform } from 'framer-motion'; +import { useRef } from 'react'; const FeatureCard = ({ icon: Icon, title, description, index }: { icon: any, title: string, description: string, index: number }) => ( -
- +
+
-

{title}

-

{description}

+

{title}

+

{description}

); export default function LandingPage() { + const containerRef = useRef(null); + const { scrollYProgress } = useScroll({ + target: containerRef, + offset: ["start start", "end end"] + }); + + const heroY = useTransform(scrollYProgress, [0, 1], [0, 200]); + const heroOpacity = useTransform(scrollYProgress, [0, 0.2], [1, 0]); + return ( -
+
{/* Hero Section */} -
- {/* Subtle background patterns */} -
- - - - - - - - +
+ {/* Cinematic Background */} + +
+ Cappadocia + + + {/* Hero Content */} +
+ + + Yapay Zeka Destekli Premium Deneyim + + + + KAPADOKYA
+ + EFSANESİ + +
+ + + "Sıradan bir gezi değil, ruhunuza dokunacak bir keşif hikayesi. Saniyeler içinde size özel kurgulanmış premium rotalar." + + + + + +
-
-
- - - Yapay Zeka Destekli Seyahat Rehberiniz - - - - Kapadokya'yı
- Yeniden Keşfedin -
- - - Kişiselleştirilmiş rotalar, anlık öneriler ve kusursuz bir planlama deneyimi. Yapay zeka ile her detayı düşünülmüş bir Kapadokya efsanesi. - - - - - - + {/* Scroll Indicator */} + +
+
-
+
- {/* Trust Stats */} -
-
-
-
- - TOP DESTINATION 2026 -
-
- - GLOBAL REACH -
-
- - SECURE BOOKING -
+ {/* Stats Section */} +
+
+
+
+ {[ + { label: "Mutlu Gezgin", value: "25k+", icon: Globe }, + { label: "Kişiye Özel Rota", value: "100k+", icon: Compass }, + { label: "Doğrulanmış Mekan", value: "1.2k", icon: MapPin }, + { label: "Müşteri Puanı", value: "4.9", icon: Star }, + ].map((stat, i) => ( + +
+ +
+
{stat.value}
+
{stat.label}
+
+ ))}
{/* Features Grid */} -
+
-
+
- Neden Biz? + Kusursuz Mühendislik - Seyahat Planlamanın
Geleceği Burada + Seyahatinizi Sanata
Dönüştürüyoruz
-
+
- {/* Modern CTA */} -
+ {/* Luxury CTA */} +
- {/* Subtle noise/texture overlay */} -
+
+ CTA +
-
-

- BÜYÜLÜ BİR GEZİ
SİZİ BEKLİYOR +
+

+ BİR SONRAKİ
EFSANENİZİ YAZIN

-

- Kapadokya'nın masalsı dokusunu modern teknolojinin kolaylığıyla keşfedin. Planlamaya bugün başlayın. +

+ Kapadokya'nın zamansız ruhunu, modern teknolojinin gücüyle birleştirin. Bugün başlayın.

-
@@ -200,58 +233,36 @@ export default function LandingPage() {

- {/* Simple Footer */} -
+ {/* Modern Footer */} +
-
-
-
- - - Kapadokya Efsanesi - +
+
+
+
-

- Yapay zeka gücüyle Kapadokya seyahatlerinizi daha anlamlı ve unutulmaz kılıyoruz. -

+ + Kapadokya Efsanesi +
-
-
-

Ürün

-
    -
  • Keşfet
  • -
  • Planla
  • -
  • Fiyatlandırma
  • -
-
-
-

Destek

-
    -
  • İletişim
  • -
  • S.S.S
  • -
-
-
-

Yasal

-
    -
  • Gizlilik
  • -
  • Şartlar
  • -
-
+
+ Keşfet + Planla + Hesabım
-
-

© 2026 Kapadokya Efsanesi. Tripizy esintisiyle.

+
+

© 2026 Cappadocia Legend. Her anı bir hikaye.

- - Made with in Cappadocia - + TR +
+ USD
); -} +} \ No newline at end of file diff --git a/app-9xzmfic2e4g1/src/pages/LoginPage.tsx b/app-9xzmfic2e4g1/src/pages/LoginPage.tsx index 7afb055..7eb52fa 100644 --- a/app-9xzmfic2e4g1/src/pages/LoginPage.tsx +++ b/app-9xzmfic2e4g1/src/pages/LoginPage.tsx @@ -11,12 +11,12 @@ import { motion } from 'framer-motion'; export default function LoginPage() { const [isLogin, setIsLogin] = useState(true); - const [email, setEmail] = useState(''); + const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [rememberMe, setRememberMe] = useState(false); const [loading, setLoading] = useState(false); - const { signIn, signUp, user } = useAuth(); + const { signInWithUsername, signUpWithUsername, user } = useAuth(); const navigate = useNavigate(); const location = useLocation(); const from = location.state?.from || '/explore'; @@ -27,7 +27,12 @@ export default function LoginPage() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!email || !password) return; + if (!username || !password) return; + + if (!/^[a-z0-9_]+$/.test(username)) { + toast.error('Kullanıcı adı sadece harf, rakam ve alt çizgi içerebilir'); + return; + } if (password.length < 6) { toast.error('Şifre en az 6 karakter olmalıdır'); @@ -37,25 +42,25 @@ export default function LoginPage() { setLoading(true); try { if (isLogin) { - const { error } = await signIn(email, password); + const { error } = await signInWithUsername(username, password); if (error) { if (error.message.includes('Invalid login credentials')) { - throw new Error('E-posta veya şifre hatalı'); + throw new Error('Kullanıcı adı veya şifre hatalı'); } throw error; } - toast.success(`Hoşgeldin!`); + toast.success(`Hoşgeldin, ${username}!`); navigate(from, { replace: true }); } else { - const { error } = await signUp(email, password); + const { error } = await signUpWithUsername(username, password); if (error) { if (error.message.includes('already registered')) { - throw new Error('Bu e-posta zaten kullanılıyor'); + throw new Error('Bu kullanıcı adı zaten kullanılıyor'); } throw error; } toast.success('Hesap oluşturuldu! Giriş yapılıyor...'); - const { error: signInError } = await signIn(email, password); + const { error: signInError } = await signInWithUsername(username, password); if (!signInError) { navigate(from, { replace: true }); } @@ -136,14 +141,13 @@ export default function LoginPage() {
- + setEmail(e.target.value)} + value={username} + onChange={e => setUsername(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, ''))} className="h-14 rounded-xl border-2 border-gray-100 bg-gray-50/50 focus:border-primary px-5 text-base font-bold transition-luxury" />
@@ -230,4 +234,4 @@ export default function LoginPage() {
); -} +} \ No newline at end of file diff --git a/app-9xzmfic2e4g1/src/types/lead.ts b/app-9xzmfic2e4g1/src/types/lead.ts deleted file mode 100644 index 5585b2d..0000000 --- a/app-9xzmfic2e4g1/src/types/lead.ts +++ /dev/null @@ -1,45 +0,0 @@ - -export type LeadCategory = 'balloon' | 'atv' | 'horse' | 'hotel' | 'restaurant' | 'transfer' | 'other'; - -export type LeadStatus = 'new' | 'assigned' | 'contacted' | 'converted' | 'lost'; - -export interface Lead { - id?: string; - user_id?: string; - trip_id?: string; - category: LeadCategory; - budget_range?: string; - participants: number; - preferred_date?: string; - status: LeadStatus; - score: number; - assigned_operator_id?: string; - full_name: string; - phone: string; - whatsapp_available: boolean; - email?: string; - metadata?: any; - created_at?: string; - updated_at?: string; -} - -export interface Operator { - id: string; - name: string; - category: LeadCategory; - whatsapp?: string; - email?: string; - max_daily_leads: number; - priority_score: number; - active: boolean; - created_at: string; - updated_at: string; -} - -export interface LeadEvent { - id?: string; - lead_id: string; - event_type: string; - metadata?: any; - created_at?: string; -} diff --git a/app-9xzmfic2e4g1/supabase/migrations/00007_lead_marketplace.sql b/app-9xzmfic2e4g1/supabase/migrations/00007_lead_marketplace.sql deleted file mode 100644 index f3b46b1..0000000 --- a/app-9xzmfic2e4g1/supabase/migrations/00007_lead_marketplace.sql +++ /dev/null @@ -1,98 +0,0 @@ - --- Lead Categories -CREATE TYPE public.lead_category AS ENUM ('balloon', 'atv', 'horse', 'hotel', 'restaurant', 'transfer', 'other'); - --- Lead Status -CREATE TYPE public.lead_status AS ENUM ('new', 'assigned', 'contacted', 'converted', 'lost'); - --- Operators Table -CREATE TABLE public.operators ( - id uuid DEFAULT gen_random_uuid() PRIMARY KEY, - name text NOT NULL, - category public.lead_category NOT NULL, - whatsapp text, - email text, - max_daily_leads int DEFAULT 10, - priority_score int DEFAULT 0, - active boolean DEFAULT true, - created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - updated_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL -); - --- Leads Table -CREATE TABLE public.leads ( - id uuid DEFAULT gen_random_uuid() PRIMARY KEY, - user_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL, - trip_id uuid REFERENCES public.trips(id) ON DELETE SET NULL, - category public.lead_category NOT NULL, - budget_range text, - participants int DEFAULT 1, - preferred_date date, - status public.lead_status DEFAULT 'new'::public.lead_status, - score int DEFAULT 0, - assigned_operator_id uuid REFERENCES public.operators(id) ON DELETE SET NULL, - - -- User details captured at lead point - full_name text, - phone text, - whatsapp_available boolean DEFAULT false, - email text, - metadata jsonb DEFAULT '{}'::jsonb, - - created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - updated_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL -); - --- Lead Events Table (for Analytics) -CREATE TABLE public.lead_events ( - id uuid DEFAULT gen_random_uuid() PRIMARY KEY, - lead_id uuid REFERENCES public.leads(id) ON DELETE CASCADE, - event_type text NOT NULL, -- e.g., 'created', 'scored', 'assigned', 'notified', 'status_changed' - metadata jsonb DEFAULT '{}'::jsonb, - created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL -); - --- Enable RLS -ALTER TABLE public.operators ENABLE ROW LEVEL SECURITY; -ALTER TABLE public.leads ENABLE ROW LEVEL SECURITY; -ALTER TABLE public.lead_events ENABLE ROW LEVEL SECURITY; - --- Operators Policies (Admins only for management) -CREATE POLICY "Admins can manage operators" ON public.operators - FOR ALL TO authenticated USING (is_admin(auth.uid())); - -CREATE POLICY "Public can view active operators (for assignment logic check)" ON public.operators - FOR SELECT TO authenticated USING (active = true); - --- Leads Policies -CREATE POLICY "Users can view their own leads" ON public.leads - FOR SELECT TO authenticated USING (auth.uid() = user_id); - -CREATE POLICY "Users can insert their own leads" ON public.leads - FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id OR auth.uid() IS NULL); - -CREATE POLICY "Admins have full access to leads" ON public.leads - FOR ALL TO authenticated USING (is_admin(auth.uid())); - --- Lead Events Policies -CREATE POLICY "Admins have full access to lead events" ON public.lead_events - FOR ALL TO authenticated USING (is_admin(auth.uid())); - --- Functions -CREATE OR REPLACE FUNCTION update_updated_at_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER update_operators_updated_at - BEFORE UPDATE ON operators - FOR EACH ROW - EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_leads_updated_at - BEFORE UPDATE ON leads - FOR EACH ROW - EXECUTE FUNCTION update_updated_at_column(); diff --git a/app-9xzmfic2e4g1/supabase/migrations/00008_seed_operators.sql b/app-9xzmfic2e4g1/supabase/migrations/00008_seed_operators.sql deleted file mode 100644 index 2864b47..0000000 --- a/app-9xzmfic2e4g1/supabase/migrations/00008_seed_operators.sql +++ /dev/null @@ -1,10 +0,0 @@ - --- Seed Operators -INSERT INTO public.operators (name, category, whatsapp, email, max_daily_leads, priority_score) -VALUES -('Kapadokya Balon Efsanesi', 'balloon', '+905001112233', 'info@balonefsanesi.com', 20, 100), -('Sultan Balloon Tours', 'balloon', '+905002223344', 'booking@sultanballoons.com', 15, 80), -('Vadiler Safari (ATV)', 'atv', '+905003334455', 'safari@cappadocia.com', 50, 90), -('Kapadokya At Çiftliği', 'horse', '+905004445566', 'at@cappadocia.com', 30, 85), -('Cave Legend Hotel', 'hotel', '+905005556677', 'stay@cavelegend.com', 10, 95), -('Görsel Sanatlar Restoran', 'restaurant', '+905006667788', 'food@cappadocia.com', 100, 70); diff --git a/app-9xzmfic2e4g1/supabase/migrations/00009_create_place_details_cache.sql b/app-9xzmfic2e4g1/supabase/migrations/00009_create_place_details_cache.sql deleted file mode 100644 index 05a0067..0000000 --- a/app-9xzmfic2e4g1/supabase/migrations/00009_create_place_details_cache.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Create table to cache Google Places API details -create table place_details_cache ( - place_id text primary key, - details jsonb not null, - created_at timestamptz default now() -);