Revert to version 773d823

This commit is contained in:
Flatlogic Bot 2026-03-04 13:48:42 +00:00
parent 0495927787
commit b7bc6b853a
15 changed files with 983 additions and 1134 deletions

View File

@ -1,4 +1,5 @@
VITE_SUPABASE_URL=https://bhaumxerateojqvleoyw.supabase.co VITE_SUPABASE_URL=https://ofqojaxiopqxahfvxpmx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJoYXVteGVyYXRlb2pxdmxlb3l3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjczNTQ4OTksImV4cCI6MjA4MjkzMDg5OX0.uyF3i17AaNd6CN5yWrGMR4vFsJ-boPrfKZYByXKBqUE VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9mcW9qYXhpb3BxeGFoZnZ4cG14Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIyODExMjAsImV4cCI6MjA4Nzg1NzEyMH0.CVyjWPp9ldCd5qxA4TbViD5MJ0axbEWfGr-1n1pPjn0
VITE_GOOGLE_MAPS_API_KEY=AIzaSyBbXWk7VhZfzn9txrAr9N-faAPuKy_LnKw VITE_GOOGLE_MAPS_API_KEY=AIzaSyCLPiqNWwFSUS0X15YvTdHZxrb-2LXoYlw
OPENAI_API_KEY=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 VITE_APP_ID=app-9xzmfic2e4g1
VITE_FORM_ID=form-9xzmfic2e4g1

View File

@ -2,7 +2,8 @@ import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; 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 { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -23,6 +24,7 @@ export function Navbar() {
const [searchOpen, setSearchOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
// Scroll efekti için
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
setScrolled(window.scrollY > 10); setScrolled(window.scrollY > 10);
@ -38,113 +40,117 @@ export function Navbar() {
return ( return (
<> <>
<nav className={cn( <nav className={cn(
"fixed top-0 left-0 right-0 z-50 w-full transition-all duration-300 h-16 border-b", "fixed top-0 left-0 right-0 z-50 w-full border-b bg-background/95 backdrop-blur-navbar transition-smooth",
scrolled scrolled && "navbar-shadow"
? "bg-white/80 dark:bg-black/80 backdrop-blur-md border-zinc-100 dark:border-zinc-900"
: "bg-white/50 dark:bg-black/50 backdrop-blur-sm border-transparent"
)}> )}>
<div className="container mx-auto h-full px-6"> <div className="container mx-auto px-4 lg:px-6">
<div className="flex h-full items-center justify-between"> <div className="flex h-16 items-center justify-between">
{/* Left: Logo */} {/* Sol: Logo ve Marka */}
<Link to="/" className="flex items-center gap-2 group"> <Link to="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="w-9 h-9 bg-primary rounded-xl flex items-center justify-center text-white shadow-lg shadow-primary/20 group-hover:scale-105 transition-transform duration-300"> <MapPin className="h-8 w-8 text-primary" />
<MapPin className="h-5 w-5" /> <span className="text-lg font-semibold tracking-tight text-orange-600">Kapadokya</span>
</div>
<span className="text-lg font-black tracking-tighter uppercase dark:text-white">
Kapadokya <span className="text-primary">Efsanesi</span>
</span>
</Link> </Link>
{/* Center: Desktop Nav */} {/* Orta: Desktop Navigasyon */}
<div className="hidden lg:flex items-center gap-10"> <div className="hidden lg:flex items-center gap-8">
<Link <Link
to="/explore" to="/explore"
className="text-xs font-black uppercase tracking-widest text-zinc-500 hover:text-primary transition-colors" className="text-sm font-medium text-muted-foreground hover:text-primary transition-colors"
> >
Keşfet Keşfet
</Link> </Link>
<Link <Link
to="/planner" to="/planner"
className="text-xs font-black uppercase tracking-widest text-zinc-500 hover:text-primary transition-colors" className="text-sm font-medium text-muted-foreground hover:text-primary transition-colors"
> >
Planla Planla
</Link> </Link>
<Link <Link
to="/account" to="/account"
className="text-xs font-black uppercase tracking-widest text-zinc-500 hover:text-primary transition-colors" className="text-sm font-medium text-muted-foreground hover:text-primary transition-colors"
> >
Gezilerim Gezilerim
</Link> </Link>
</div> </div>
{/* Right: Actions */} {/* Sağ: Aksiyonlar */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
{/* Arama butonu */}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setSearchOpen(true)} onClick={() => setSearchOpen(true)}
className="rounded-xl text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-900 h-9 w-9" aria-label="Ara"
> >
<Search className="h-4 w-4" /> <Search className="h-5 w-5" />
</Button> </Button>
{/* Tema değiştirici */}
<div className="hidden sm:block"> <div className="hidden sm:block">
<ThemeToggle /> <ThemeToggle />
</div> </div>
{/* Bildirimler (sadece giriş yapılmışsa) */}
{user && ( {user && (
<div className="hidden sm:block"> <div className="hidden sm:block">
<NotificationsDropdown /> <NotificationsDropdown />
</div> </div>
)} )}
{/* Kullanıcı menüsü veya giriş butonu */}
{user ? ( {user ? (
<div className="hidden lg:block"> <div className="hidden lg:block">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-9 w-9 rounded-xl p-0 hover:bg-zinc-100 dark:hover:bg-zinc-900"> <Button variant="ghost" className="relative h-9 w-9 rounded-full">
<Avatar className="h-9 w-9 rounded-xl border-2 border-zinc-100 dark:border-zinc-800"> <Avatar className="h-9 w-9">
<AvatarFallback className="bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-white text-[10px] font-black"> <AvatarFallback className="bg-primary text-primary-foreground text-sm">
{getInitials((profile as any)?.email || 'U')} {getInitials((profile as any)?.email || 'U')}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64 rounded-2xl p-2 border-zinc-100 dark:border-zinc-800 shadow-2xl"> <DropdownMenuContent align="end" className="w-60">
<div className="flex items-center gap-3 p-4"> <div className="flex items-center gap-3 p-3">
<Avatar className="h-10 w-10 rounded-xl"> <Avatar className="h-10 w-10">
<AvatarFallback className="bg-primary text-white font-black text-xs"> <AvatarFallback className="bg-primary text-primary-foreground">
{getInitials((profile as any)?.email || 'U')} {getInitials((profile as any)?.email || 'U')}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-col min-w-0"> <div className="flex flex-col space-y-0.5 flex-1 min-w-0">
<p className="font-black text-sm text-zinc-900 dark:text-white truncate"> <p className="font-medium text-sm truncate">
{(profile as any)?.email?.split('@')[0]} {(profile as any)?.email?.split('@')[0]}
</p> </p>
<p className="text-[9px] font-bold text-zinc-400 truncate uppercase tracking-widest"> <p className="text-xs text-muted-foreground truncate">
{(profile as any)?.email} {(profile as any)?.email}
</p> </p>
</div> </div>
</div> </div>
<DropdownMenuSeparator className="bg-zinc-100 dark:bg-zinc-800" /> <DropdownMenuSeparator />
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/account" className="cursor-pointer p-3 rounded-xl font-black uppercase tracking-widest text-[9px] gap-3"> <Link to="/account/profile" className="cursor-pointer">
<History className="h-4 w-4 text-primary" /> <User className="mr-2 h-4 w-4" />
Gezilerim Profilim
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/account/preferences" className="cursor-pointer p-3 rounded-xl font-black uppercase tracking-widest text-[9px] gap-3"> <Link to="/account" className="cursor-pointer">
<Settings className="h-4 w-4 text-zinc-400" /> <History className="mr-2 h-4 w-4" />
Rotalarım
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/account/preferences" className="cursor-pointer">
<Settings className="mr-2 h-4 w-4" />
Tercihler Tercihler
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="bg-zinc-100 dark:bg-zinc-800" /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => signOut()} onClick={() => signOut()}
className="text-red-500 focus:text-red-500 cursor-pointer p-3 rounded-xl font-black uppercase tracking-widest text-[9px] gap-3" className="text-destructive focus:text-destructive cursor-pointer"
> >
<LogOut className="h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
Çıkış Yap Çıkış Yap
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -154,20 +160,23 @@ export function Navbar() {
<Button <Button
onClick={() => navigate('/login')} onClick={() => navigate('/login')}
variant="outline" variant="outline"
className="hidden lg:flex h-9 px-5 rounded-xl border-zinc-200 dark:border-zinc-800 font-black uppercase tracking-widest text-[9px]" className="hidden lg:flex h-11 px-6"
> >
Giriş Yap Giriş Yap
</Button> </Button>
)} )}
{/* Mobil menü */}
<MobileMenu /> <MobileMenu />
</div> </div>
</div> </div>
</div> </div>
</nav> </nav>
{/* Navbar yüksekliği kadar boşluk bırak */}
<div className="h-16" /> <div className="h-16" />
{/* Arama modalı */}
<SearchModal open={searchOpen} onOpenChange={setSearchOpen} /> <SearchModal open={searchOpen} onOpenChange={setSearchOpen} />
</> </>
); );

View File

@ -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<LeadFormValues>({
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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[450px] p-0 overflow-hidden rounded-[2rem] border-none shadow-2xl">
<div className="bg-primary p-8 text-white">
<DialogHeader>
<DialogTitle className="text-2xl font-black uppercase tracking-tight leading-tight">
{getTitle()}
</DialogTitle>
<DialogDescription className="text-white/80 font-medium italic mt-2">
{getDescription()}
</DialogDescription>
</DialogHeader>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="p-8 space-y-6 bg-white dark:bg-zinc-950">
<div className="grid grid-cols-2 gap-5">
<FormField
control={form.control}
name="full_name"
render={({ field }) => (
<FormItem className="col-span-2 space-y-2">
<FormLabel className="text-[10px] font-black uppercase tracking-widest text-primary">Ad Soyad</FormLabel>
<FormControl>
<Input placeholder="Mehmet Yılmaz" {...field} className="h-12 rounded-xl bg-zinc-50 border-zinc-100" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem className="col-span-2 space-y-2">
<FormLabel className="text-[10px] font-black uppercase tracking-widest text-primary">Telefon / WhatsApp</FormLabel>
<FormControl>
<Input placeholder="+90 5xx xxx xx xx" {...field} className="h-12 rounded-xl bg-zinc-50 border-zinc-100" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="participants"
render={({ field }) => (
<FormItem className="space-y-2">
<FormLabel className="text-[10px] font-black uppercase tracking-widest text-primary">Kişi Sayısı</FormLabel>
<FormControl>
<Input
type="number"
value={field.value}
onChange={e => 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"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="budget_range"
render={({ field }) => (
<FormItem className="space-y-2">
<FormLabel className="text-[10px] font-black uppercase tracking-widest text-primary">Bütçe Aralığı</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="h-12 rounded-xl bg-zinc-50 border-zinc-100">
<SelectValue placeholder="Bütçe Seçin" />
</SelectTrigger>
</FormControl>
<SelectContent className="rounded-xl border-zinc-100">
<SelectItem value="budget">Ekonomik</SelectItem>
<SelectItem value="moderate">Standart</SelectItem>
<SelectItem value="luxury">Lüks</SelectItem>
<SelectItem value="not_sure">Belirsiz</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="whatsapp_available"
render={({ field }) => (
<FormItem className="col-span-2 flex flex-row items-center space-x-3 space-y-0 p-4 bg-zinc-50 dark:bg-zinc-900 rounded-2xl border border-zinc-100 dark:border-zinc-800">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="text-xs font-bold flex items-center gap-2 cursor-pointer">
<MessageCircle className="h-4 w-4 text-green-500" />
WhatsApp üzerinden dönüş almak istiyorum
</FormLabel>
</div>
</FormItem>
)}
/>
</div>
<DialogFooter className="pt-4">
<Button
type="submit"
className="w-full h-14 text-base font-black uppercase tracking-widest rounded-2xl shadow-xl shadow-primary/20"
disabled={isSubmitting}
>
{isSubmitting ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<Send className="mr-2 h-5 w-5" />
)}
Fiyat Teklifi Al
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -77,8 +77,19 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }:
const fetchPlaceDetail = useCallback(async (poi: SelectedPOI) => { const fetchPlaceDetail = useCallback(async (poi: SelectedPOI) => {
setDetailLoading(true); setDetailLoading(true);
setPlaceDetail(null); setPlaceDetail(null);
setActiveTab("about"); 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); setDetailLoading(false);
}
}, []); }, []);
// ── Handle add ──────────────────────────────────────────────────────────── // ── Handle add ────────────────────────────────────────────────────────────
@ -493,14 +504,14 @@ export function TripMap({ itinerary, activePlaceId, onMarkerClick, onAddPlace }:
)} )}
{/* Çalışma saatleri */} {/* Çalışma saatleri */}
{placeDetail.opening_hours && placeDetail.opening_hours.length > 0 && ( {placeDetail.opening_hours?.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-[11px] font-black text-gray-900 uppercase tracking-widest flex items-center gap-1.5"> <h4 className="text-[11px] font-black text-gray-900 uppercase tracking-widest flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 text-blue-500" /> <Clock className="h-3.5 w-3.5 text-blue-500" />
Çalışma Saatleri Çalışma Saatleri
</h4> </h4>
<div className="space-y-1"> <div className="space-y-1">
{placeDetail.opening_hours?.map((h, i) => ( {placeDetail.opening_hours.map((h, i) => (
<p key={i} className="text-[11px] text-gray-500">{h}</p> <p key={i} className="text-[11px] text-gray-500">{h}</p>
))} ))}
</div> </div>

View File

@ -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 { import {
Hotel, Home, Navigation, Plane, Trees, Building2, Camera, Loader2, ArrowRight, ArrowLeft, Sparkles,
Bike, UtensilsCrossed, Users, Heart, Baby, MapPin, Calendar, Users, Coffee, Heart,
Car, Bus, Shuffle, Gem, Wallet, CreditCard, Crown, Car, Wallet, CheckCircle2, ChevronRight,
PersonStanding, Calendar PersonStanding,
} from 'lucide-react'; } 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 = [ import { LOADING_STEPS, TRAVEL_TYPE_OPTIONS, BUDGET_OPTIONS, TRANSPORT_OPTIONS, ACCOMMODATION_OPTIONS, INTEREST_OPTIONS } from '@/constants/planner';
{ import { DateSelector } from '@/components/planner/DateSelector';
id: 'hotel', import { TravelerInput } from '@/components/planner/TravelerInput';
label: 'Otel', import { AccommodationSelector } from '@/components/planner/AccommodationSelector';
description: 'Konforlu & servis odaklı', import { InterestsGrid } from '@/components/planner/InterestsGrid';
icon: Hotel, import { TravelTypeSelector } from '@/components/planner/TravelTypeSelector';
}, import { TransportSelector } from '@/components/planner/TransportSelector';
{ import { BudgetSelector } from '@/components/planner/BudgetSelector';
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;
export const TRAVEL_TYPE_OPTIONS = [ // ─── Schema ───────────────────────────────────────────────────────────────────
{ const formSchema = z.object({
id: 'solo', dateRange: z.object({
label: 'Solo', from: z.date({ required_error: 'Başlangıç tarihi gereklidir' }),
description: 'Kendi tempomda özgür', to: z.date({ required_error: 'Bitiş tarihi gereklidir' }),
icon: PersonStanding, })
gradient: 'from-violet-500 to-purple-600', .refine(d => d.from >= new Date(new Date().setHours(0, 0, 0, 0)), {
bg: 'bg-violet-50', message: 'Başlangıç tarihi bugünden önce olamaz',
border: 'border-violet-200', })
text: 'text-violet-600', .refine(d => d.to > d.from, {
}, message: 'Bitiş tarihi başlangıç tarihinden sonra olmalıdır',
{ })
id: 'couple', .refine(d => {
label: 'Çift', const days = differenceInDays(d.to, d.from) + 1;
description: 'Romantik & özel anlar', return days >= 1 && days <= 14;
icon: Heart, }, { message: 'Seyahat süresi 114 gün arasında olmalıdır' }),
gradient: 'from-rose-500 to-pink-600', travelType: z.string().min(1, 'Seyahat tipi seçiniz'),
bg: 'bg-rose-50', travelers: z.number().min(1).max(15),
border: 'border-rose-200', accommodation: z.string(),
text: 'text-rose-600', 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),
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;
export const TRANSPORT_OPTIONS = [ type FormValues = z.infer<typeof formSchema>;
{
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;
export const BUDGET_OPTIONS = [ // ─── Steps ────────────────────────────────────────────────────────────────────
{ const STEPS = [
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 = [
{ id: 'dates', title: 'Tarihler', icon: Calendar, description: 'Ne zaman gidiyorsunuz?' }, { id: 'dates', title: 'Tarihler', icon: Calendar, description: 'Ne zaman gidiyorsunuz?' },
{ id: 'travelType', title: 'Seyahat Tipi', icon: PersonStanding, description: 'Nasıl bir seyahat?' }, { 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: '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: '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?' }, { id: 'interests', title: 'İlgi Alanları', icon: Heart, description: 'Neleri keşfetmek istersiniz?' },
] as const; ] as const;
// ─── Summary label helpers ────────────────────────────────────────────────────
function getSummaryLabel(stepId: string, values: Partial<FormValues>): 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<FormValues>({
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<string, keyof FormValues | (keyof FormValues)[]> = {
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 (
<div className="h-screen w-full flex flex-col items-center justify-center bg-secondary relative overflow-hidden">
<div className="absolute inset-0 opacity-15">
<img
src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400"
alt=""
className="w-full h-full object-cover grayscale"
/>
</div>
<div className="absolute inset-0 bg-secondary/60" />
<div className="relative z-10 max-w-md w-full px-8 text-center space-y-10">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="relative mx-auto w-24 h-24"
>
<div className="absolute inset-0 bg-primary rounded-2xl animate-pulse opacity-30 scale-110" />
<div className="w-24 h-24 bg-primary rounded-2xl flex items-center justify-center shadow-2xl shadow-primary/30">
<Loader2 className="h-11 w-11 text-white animate-spin" />
</div>
</motion.div>
<div className="space-y-5">
<AnimatePresence mode="wait">
<motion.h2
key={loadingStep}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="text-3xl font-black text-white tracking-tighter uppercase"
>
{LOADING_STEPS[loadingStep].label}
</motion.h2>
</AnimatePresence>
<div className="w-full h-2 bg-white/10 rounded-full overflow-hidden">
<motion.div
className="h-full bg-primary rounded-full"
initial={{ width: 0 }}
animate={{ width: `${LOADING_STEPS[loadingStep].progress}%` }}
transition={{ duration: 0.6, ease: 'easeInOut' }}
/>
</div>
<div className="flex justify-between text-[10px] font-bold text-white/30 uppercase tracking-widest px-1">
<span>Hazırlanıyor</span>
<span>{LOADING_STEPS[loadingStep].progress}%</span>
</div>
</div>
<p className="text-white/30 text-xs font-medium italic">
Size özel Kapadokya efsanesi kurguluyoruz...
</p>
</div>
</div>
);
}
// ── Main layout ─────────────────────────────────────────────────────────────
return (
<div className="min-h-screen bg-background flex flex-col lg:flex-row overflow-hidden">
{/* ── Sidebar ─────────────────────────────────────────────────────────── */}
<div className="w-full lg:w-[300px] xl:w-[360px] bg-secondary flex flex-col relative overflow-hidden shrink-0">
{/* Decorative orbs */}
<div className="absolute top-0 left-0 w-72 h-72 bg-primary/10 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2 pointer-events-none" />
<div className="absolute bottom-0 right-0 w-64 h-64 bg-accent/5 rounded-full blur-3xl translate-x-1/3 translate-y-1/3 pointer-events-none" />
<div className="relative z-10 flex flex-col h-full p-8 lg:p-10 gap-8">
{/* Logo */}
<div className="flex items-center gap-3">
<div className="w-9 h-9 bg-primary rounded-xl flex items-center justify-center text-white shadow-lg shadow-primary/20">
<MapPin className="h-4 w-4" />
</div>
<span className="text-base font-black text-white tracking-tight uppercase">
Kapadokya <span className="text-primary">Efsanesi</span>
</span>
</div>
{/* Headline */}
<div className="space-y-2">
<h1 className="text-3xl xl:text-4xl font-black text-white leading-none tracking-tighter uppercase">
ROTANIZI<br /><span className="text-primary">TASARLAYIN</span>
</h1>
<p className="text-white/35 text-sm font-medium italic">
"Size özel kurgulanmış seyahat mimarisi."
</p>
</div>
{/* Step list */}
<div className="flex-1 space-y-1.5">
{STEPS.map((step, i) => {
const Icon = step.icon;
const isActive = i === currentStep;
const isCompleted = i < currentStep;
const summaryLabel = getSummaryLabel(step.id, watchedValues);
return (
<motion.div
key={step.id}
animate={{ opacity: isActive || isCompleted ? 1 : 0.35 }}
className={cn(
'flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group',
isActive && 'bg-white/8'
)}
>
{/* Step indicator */}
<div className={cn(
'w-8 h-8 rounded-lg flex items-center justify-center shrink-0 transition-all duration-300',
isActive ? 'bg-primary text-white shadow-md shadow-primary/30 scale-110'
: isCompleted ? 'bg-white/10 text-primary'
: 'bg-white/5 text-white/20'
)}>
{isCompleted
? <CheckCircle2 className="h-4 w-4" />
: <Icon className="h-4 w-4" />
}
</div>
{/* Labels */}
<div className="flex-1 min-w-0">
<div className={cn(
'text-[9px] font-black uppercase tracking-[0.15em]',
isActive ? 'text-primary' : 'text-white/25'
)}>
{String(i + 1).padStart(2, '0')}
</div>
<div className={cn(
'text-sm font-bold leading-tight truncate',
isActive ? 'text-white' : isCompleted ? 'text-white/60' : 'text-white/30'
)}>
{step.title}
</div>
</div>
{/* Summary pill */}
{isCompleted && summaryLabel && (
<span className="text-[9px] font-black text-primary/70 bg-primary/10 px-2 py-0.5 rounded-full shrink-0 max-w-[80px] truncate">
{summaryLabel}
</span>
)}
{isActive && (
<ChevronRight className="h-3.5 w-3.5 text-primary/60 shrink-0" />
)}
</motion.div>
);
})}
</div>
{/* Progress bar */}
<div className="space-y-2 pt-2 border-t border-white/8">
<div className="flex justify-between text-[10px] font-bold text-white/25 uppercase tracking-widest">
<span>İlerleme</span>
<span>{Math.round(progress)}%</span>
</div>
<div className="h-1 bg-white/8 rounded-full overflow-hidden">
<motion.div
className="h-full bg-primary rounded-full"
animate={{ width: `${progress}%` }}
transition={{ duration: 0.5, ease: 'easeOut' }}
/>
</div>
</div>
</div>
</div>
{/* ── Main Form Area ──────────────────────────────────────────────────── */}
<div className="flex-1 bg-white dark:bg-card overflow-y-auto">
<div className="max-w-2xl mx-auto min-h-full flex flex-col px-8 py-10 lg:py-14 lg:px-16">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex-1 flex flex-col">
<AnimatePresence mode="wait">
<motion.div
key={currentStep}
initial={{ opacity: 0, x: 24 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -24 }}
transition={{ duration: 0.35, ease: 'easeOut' }}
className="flex-1 space-y-10"
>
{/* Step header */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-[10px] font-black text-primary uppercase tracking-[0.2em]">
<span>Adım {currentStep + 1}/{STEPS.length}</span>
<span className="w-12 h-0.5 bg-primary/20 rounded-full" />
<span>{STEPS[currentStep].title}</span>
</div>
<h2 className="text-3xl md:text-4xl xl:text-5xl font-black text-gray-900 dark:text-white tracking-tighter leading-[0.95] uppercase">
{STEPS[currentStep].description}
</h2>
</div>
{/* Step content */}
<div className="pt-2">
{/* Step 0 — Dates */}
{currentStep === 0 && (
<FormField control={form.control} name="dateRange" render={({ field }) => (
<FormItem className="space-y-3">
<Label className="text-[10px] font-black uppercase tracking-[0.2em] text-primary">Seyahat Takvimi</Label>
<DateSelector
date={field.value}
onDateChange={field.onChange}
isOpen={datePickerOpen}
onOpenChange={setDatePickerOpen}
/>
<FormMessage />
</FormItem>
)} />
)}
{/* Step 1 — Travel Type */}
{currentStep === 1 && (
<FormField control={form.control} name="travelType" render={({ field }) => (
<FormItem className="space-y-3">
<Label className="text-[10px] font-black uppercase tracking-[0.2em] text-primary">Seyahat Tarzı</Label>
<TravelTypeSelector selectedId={field.value} onSelect={field.onChange} />
<FormMessage />
</FormItem>
)} />
)}
{/* Step 2 — Travelers & Accommodation */}
{currentStep === 2 && (
<div className="space-y-8">
<FormField control={form.control} name="travelers" render={({ field }) => (
<FormItem className="space-y-3">
<Label className="text-[10px] font-black uppercase tracking-[0.2em] text-primary">Grup Büyüklüğü</Label>
<TravelerInput value={field.value} onChange={field.onChange} />
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="accommodation" render={({ field }) => (
<FormItem className="space-y-3">
<Label className="text-[10px] font-black uppercase tracking-[0.2em] text-primary">Konaklama Tarzı</Label>
<AccommodationSelector selectedId={field.value} onSelect={field.onChange} />
<FormMessage />
</FormItem>
)} />
</div>
)}
{/* Step 3 — Transport */}
{currentStep === 3 && (
<FormField control={form.control} name="transport" render={({ field }) => (
<FormItem className="space-y-3">
<Label className="text-[10px] font-black uppercase tracking-[0.2em] text-primary">Ulaşım Tercihi</Label>
<TransportSelector selectedId={field.value} onSelect={field.onChange} />
<FormMessage />
</FormItem>
)} />
)}
{/* Step 4 — Budget */}
{currentStep === 4 && (
<FormField control={form.control} name="budget" render={({ field }) => (
<FormItem className="space-y-3">
<Label className="text-[10px] font-black uppercase tracking-[0.2em] text-primary">Günlük Bütçe</Label>
<BudgetSelector selectedId={field.value} onSelect={field.onChange} />
<FormMessage />
</FormItem>
)} />
)}
{/* Step 5 — Interests */}
{currentStep === 5 && (
<FormField control={form.control} name="interests" render={({ field }) => (
<FormItem className="space-y-3">
<div className="flex justify-between items-center">
<Label className="text-[10px] font-black uppercase tracking-[0.2em] text-primary">İlgi Alanları</Label>
<span className="text-[10px] font-bold text-gray-400">{field.value.length}/6 Seçildi</span>
</div>
<InterestsGrid selectedInterests={field.value} onToggle={handleInterestToggle} />
<FormMessage />
</FormItem>
)} />
)}
</div>
</motion.div>
</AnimatePresence>
{/* Navigation */}
<div className="pt-10 mt-auto flex items-center justify-between gap-4 border-t border-gray-100 dark:border-white/8">
<Button
type="button"
variant="ghost"
size="lg"
onClick={prevStep}
disabled={currentStep === 0}
className="h-13 px-7 text-sm font-bold rounded-xl hover:bg-gray-50 group disabled:opacity-30"
>
<ArrowLeft className="mr-2 h-4 w-4 group-hover:-translate-x-1 transition-transform" />
Geri
</Button>
{currentStep < STEPS.length - 1 ? (
<Button
type="button"
size="lg"
onClick={nextStep}
className="h-13 px-10 text-sm font-black bg-primary hover:bg-primary/90 rounded-xl shadow-lg shadow-primary/20 group uppercase tracking-widest"
>
Devam Et
<ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform" />
</Button>
) : (
<Button
type="submit"
size="lg"
className="h-13 px-12 text-sm font-black bg-primary hover:bg-primary/90 rounded-xl shadow-lg shadow-primary/20 uppercase tracking-widest"
>
Rotayı Oluştur
<Sparkles className="ml-2 h-4 w-4 animate-pulse" />
</Button>
)}
</div>
</form>
</Form>
</div>
</div>
</div>
);
};
export default memo(PlannerPage);

View File

@ -20,8 +20,8 @@ interface AuthContextType {
user: User | null; user: User | null;
profile: Profile | null; profile: Profile | null;
loading: boolean; loading: boolean;
signIn: (email: string, password: string) => Promise<{ error: Error | null }>; signInWithUsername: (username: string, password: string) => Promise<{ error: Error | null }>;
signUp: (email: string, password: string) => Promise<{ error: Error | null }>; signUpWithUsername: (username: string, password: string) => Promise<{ error: Error | null }>;
signOut: () => Promise<void>; signOut: () => Promise<void>;
refreshProfile: () => Promise<void>; refreshProfile: () => Promise<void>;
} }
@ -51,7 +51,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
} }
setLoading(false); 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) => { const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null); setUser(session?.user ?? null);
if (session?.user) { if (session?.user) {
@ -64,8 +64,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();
}, []); }, []);
const signIn = async (email: string, password: string) => { const signInWithUsername = async (username: string, password: string) => {
try { try {
const email = `${username}@miaoda.com`;
const { error } = await supabase.auth.signInWithPassword({ const { error } = await supabase.auth.signInWithPassword({
email, email,
password, 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 { try {
const email = `${username}@miaoda.com`;
const { error } = await supabase.auth.signUp({ const { error } = await supabase.auth.signUp({
email, email,
password, password,
@ -99,7 +101,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}; };
return ( return (
<AuthContext.Provider value={{ user, profile, loading, signIn, signUp, signOut, refreshProfile }}> <AuthContext.Provider value={{ user, profile, loading, signInWithUsername, signUpWithUsername, signOut, refreshProfile }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

View File

@ -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<Lead>, 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<Lead>, tripDurationDays: number = 0): Promise<Lead> {
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<Operator | null> {
// 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);
}
};

View File

@ -5,7 +5,7 @@ import api, { Trip } from '@/db/api';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; 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 { format } from 'date-fns';
import { tr } from 'date-fns/locale'; import { tr } from 'date-fns/locale';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -57,16 +57,23 @@ export default function AccountPage() {
if (!user) { if (!user) {
return ( return (
<div className="min-h-screen flex items-center justify-center p-6 bg-zinc-50 dark:bg-black relative overflow-hidden"> <div className="min-h-screen flex items-center justify-center p-6 bg-secondary relative overflow-hidden">
<Card className="max-w-md w-full p-10 text-center space-y-8 bg-white dark:bg-zinc-900 border-zinc-100 dark:border-zinc-800 rounded-[2.5rem] shadow-sm relative z-10"> <div className="absolute inset-0 z-0 opacity-10">
<div className="w-20 h-20 bg-zinc-50 dark:bg-zinc-800 rounded-3xl flex items-center justify-center mx-auto border border-zinc-100 dark:border-zinc-700"> <img
<User className="h-10 w-10 text-zinc-300" /> src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400"
alt="bg"
className="w-full h-full object-cover grayscale"
/>
</div> </div>
<div className="space-y-3"> <Card className="max-w-sm w-full p-8 text-center space-y-6 bg-white/5 backdrop-blur-xl border-white/10 rounded-3xl shadow-2xl relative z-10">
<h2 className="text-3xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase leading-none">HESABINIZ</h2> <div className="w-16 h-16 bg-primary/20 rounded-2xl flex items-center justify-center mx-auto border border-primary/40">
<p className="text-zinc-500 font-medium text-base">Gezilerinizi görüntülemek ve yönetmek için giriş yapın.</p> <ExploreIcon className="h-8 w-8 text-primary" />
</div> </div>
<Button className="w-full h-16 bg-primary hover:bg-primary-dark text-white font-black uppercase tracking-widest rounded-2xl shadow-xl shadow-primary/20" asChild> <div className="space-y-2">
<h2 className="text-3xl font-black text-white tracking-tighter uppercase leading-none">HESABIM</h2>
<p className="text-white/60 font-medium italic text-sm">Gezilerinizi yönetmek için giriş yapmalısınız.</p>
</div>
<Button className="w-full h-14 bg-primary hover:bg-primary-dark text-white font-black uppercase tracking-widest rounded-xl" asChild>
<Link to="/login">Giriş Yap</Link> <Link to="/login">Giriş Yap</Link>
</Button> </Button>
</Card> </Card>
@ -75,91 +82,93 @@ export default function AccountPage() {
} }
return ( return (
<div className="min-h-screen bg-[#FDFDFD] dark:bg-black pt-24 pb-24"> <div className="min-h-screen bg-background pt-20 pb-12">
<div className="max-w-4xl mx-auto px-6 space-y-12"> <div className="max-w-4xl mx-auto px-6 space-y-10">
<header className="flex flex-col md:flex-row md:items-end justify-between gap-8 pb-12 border-b border-zinc-100 dark:border-zinc-900"> <div className="flex flex-col md:flex-row md:items-end justify-between gap-6 pb-10 border-b border-border">
<div className="space-y-4"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge className="bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 border-none font-black px-3 py-1 rounded-full uppercase text-[9px] tracking-widest"> <span className="text-[10px] font-black text-primary uppercase tracking-widest">Kişisel Arşiv</span>
{trips.length} KAYITLI ROTA <Badge className="bg-primary/10 text-primary border-none font-black px-2 py-0.5 rounded-full uppercase text-[9px]">
{trips.length} Rota
</Badge> </Badge>
</div> </div>
<h1 className="text-5xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase leading-none"> <h1 className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white tracking-tighter uppercase leading-none">
GEZİLERİM GEZİLERİM
</h1> </h1>
<p className="text-lg text-zinc-500 font-medium italic">Kişisel Kapadokya arşiviniz.</p> <p className="text-lg text-gray-500 font-medium italic">Planladığınız Kapadokya efsaneleri.</p>
</div> </div>
<Button className="h-14 px-10 bg-zinc-900 hover:bg-black text-white font-black uppercase tracking-widest rounded-2xl shadow-lg gap-2 group transition-all duration-300" asChild> <Button className="h-14 px-8 bg-primary hover:bg-primary-dark text-white font-black uppercase tracking-widest rounded-xl shadow-lg shadow-primary/20 gap-2 group transition-luxury" asChild>
<Link to="/planner"> <Link to="/planner">
<PlusCircle className="h-5 w-5 group-hover:rotate-90 transition-transform" /> <PlusCircle className="h-5 w-5 group-hover:rotate-90 transition-transform" />
Yeni Plan Yeni Plan
</Link> </Link>
</Button> </Button>
</header> </div>
{loading ? ( {loading ? (
<div className="flex flex-col items-center justify-center py-24 gap-6"> <div className="flex flex-col items-center justify-center py-20 gap-4">
<Loader2 className="h-10 w-10 animate-spin text-primary opacity-40" /> <Loader2 className="h-12 w-12 animate-spin text-primary opacity-40" />
<p className="text-zinc-400 font-black uppercase tracking-widest text-[10px]">Veriler Getiriliyor...</p> <p className="text-gray-400 font-black uppercase tracking-widest text-[9px]">Yükleniyor...</p>
</div> </div>
) : trips.length === 0 ? ( ) : trips.length === 0 ? (
<motion.div <motion.div
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
> >
<Card className="bg-zinc-50 dark:bg-zinc-950 border-2 border-dashed border-zinc-100 dark:border-zinc-900 py-24 text-center space-y-8 rounded-[2.5rem]"> <Card className="bg-gray-50 dark:bg-white/5 border-dashed border-2 border-gray-100 dark:border-white/10 py-20 text-center space-y-6 rounded-3xl">
<div className="w-24 h-24 bg-white dark:bg-zinc-900 rounded-3xl flex items-center justify-center mx-auto border border-zinc-100 dark:border-zinc-800"> <div className="w-20 h-20 bg-white dark:bg-white/5 rounded-2xl flex items-center justify-center mx-auto border border-gray-100 dark:border-white/10">
<History className="h-10 w-10 text-zinc-200 dark:text-zinc-700" /> <ExploreIcon className="h-8 w-8 text-gray-200" />
</div> </div>
<div className="space-y-3"> <div className="space-y-2">
<h3 className="text-3xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase">HENÜZ PLAN YOK</h3> <h3 className="text-2xl font-black text-gray-900 dark:text-white tracking-tighter uppercase">Planınız yok</h3>
<p className="text-zinc-500 font-medium text-base max-w-xs mx-auto"> <p className="text-gray-500 font-medium italic text-sm max-w-xs mx-auto">
Henüz bir seyahat rotası oluşturmadınız. İlk Kapadokya rotanızı oluşturun.
</p> </p>
</div> </div>
<Button variant="outline" className="h-14 px-10 rounded-2xl border-2 border-primary text-primary hover:bg-primary hover:text-white font-black uppercase tracking-widest transition-all duration-300" asChild> <Button variant="outline" className="h-12 px-8 rounded-xl border-2 border-primary text-primary hover:bg-primary hover:text-white font-black uppercase tracking-widest transition-luxury" asChild>
<Link to="/planner">Hemen Başlayın</Link> <Link to="/planner">Hemen Planla</Link>
</Button> </Button>
</Card> </Card>
</motion.div> </motion.div>
) : ( ) : (
<div className="grid gap-8"> <div className="grid gap-6">
<AnimatePresence> <AnimatePresence>
{trips.map((trip, idx) => ( {trips.map((trip, idx) => (
<motion.div <motion.div
key={trip.id} key={trip.id}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }} transition={{ delay: idx * 0.1 }}
> >
<Card className="group overflow-hidden bg-white dark:bg-zinc-950 border border-zinc-100 dark:border-zinc-900 rounded-[2rem] shadow-sm hover:shadow-xl transition-all duration-500"> <Card className="group overflow-hidden bg-white dark:bg-white/5 border-2 border-gray-50 dark:border-white/5 rounded-3xl shadow-md hover:shadow-xl transition-luxury">
<CardContent className="p-0"> <CardContent className="p-0">
<div className="flex flex-col sm:flex-row"> <div className="flex flex-col sm:flex-row">
<div className="sm:w-64 h-56 sm:h-auto bg-zinc-100 dark:bg-zinc-900 relative overflow-hidden shrink-0"> <div className="sm:w-56 h-48 sm:h-auto bg-gray-200 relative overflow-hidden shrink-0">
<img <img
src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=800" src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400"
alt={trip.title} alt={trip.title}
className="w-full h-full object-cover group-hover:scale-105 transition-all duration-700 grayscale-[0.3] group-hover:grayscale-0" className="w-full h-full object-cover group-hover:scale-105 transition-luxury duration-700"
/> />
<div className="absolute top-5 left-5"> <div className="absolute top-4 left-4">
<Badge className="bg-white/90 backdrop-blur-md text-zinc-900 border-none font-black px-3 py-1 rounded-full uppercase text-[9px] tracking-widest shadow-sm"> <Badge className="bg-primary text-white border-none font-black px-2 py-0.5 rounded-full uppercase text-[8px]">
<Zap className="h-2 w-2 mr-1" />
{trip.itinerary.days.length} GÜN {trip.itinerary.days.length} GÜN
</Badge> </Badge>
</div> </div>
</div> </div>
<div className="flex-1 p-8 flex flex-col justify-between"> <div className="flex-1 p-6 flex flex-col justify-between">
<div className="flex items-start justify-between gap-6"> <div className="flex items-start justify-between gap-4">
<div className="space-y-2"> <div className="space-y-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[10px] font-black text-primary uppercase tracking-widest">{trip.destination}</span> <span className="text-[9px] font-black text-primary uppercase tracking-widest">{trip.destination}</span>
</div> </div>
<h3 className="text-3xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase leading-none group-hover:text-primary transition-colors"> <h3 className="text-2xl font-black text-gray-900 dark:text-white tracking-tighter uppercase leading-none group-hover:text-primary transition-colors">
{trip.title} {trip.title}
</h3> </h3>
<div className="flex items-center gap-4 pt-2"> <div className="flex items-center gap-4 pt-1">
<div className="flex items-center gap-1.5 text-[11px] font-bold text-zinc-400 italic"> <div className="flex items-center gap-1.5 text-[10px] font-bold text-gray-500 italic">
<Calendar className="h-4 w-4 text-primary" /> <Calendar className="h-3.5 w-3.5 text-primary" />
<span>{format(new Date(trip.start_date), 'd MMM yyyy', { locale: tr })}</span> <span>{format(new Date(trip.start_date), 'd MMM yyyy', { locale: tr })}</span>
</div> </div>
</div> </div>
@ -167,34 +176,34 @@ export default function AccountPage() {
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant="ghost" size="icon" className="h-10 w-10 rounded-2xl text-zinc-300 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950/20"> <Button variant="ghost" size="icon" className="h-10 w-10 rounded-xl text-gray-300 hover:text-red-600">
<Trash2 className="h-5 w-5" /> <Trash2 className="h-5 w-5" />
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="rounded-[2.5rem] p-10 bg-white dark:bg-zinc-900 border-zinc-100 dark:border-zinc-800 shadow-2xl"> <AlertDialogContent className="rounded-3xl p-8 bg-secondary border-white/10">
<AlertDialogHeader className="space-y-4 text-center"> <AlertDialogHeader className="space-y-3">
<AlertDialogTitle className="text-3xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase">GEZİYİ SİL?</AlertDialogTitle> <AlertDialogTitle className="text-2xl font-black text-white tracking-tighter uppercase">Planı Sil?</AlertDialogTitle>
<AlertDialogDescription className="text-zinc-500 font-medium text-base"> <AlertDialogDescription className="text-white/60 font-medium italic text-base">
Bu rotayı kalıcı olarak silmek istediğinize emin misiniz? Bu işlem geri alınamaz. Silmek istediğinize emin misiniz?
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter className="pt-8 flex sm:justify-center gap-4"> <AlertDialogFooter className="pt-6">
<AlertDialogCancel className="h-14 px-10 rounded-2xl font-black uppercase tracking-widest text-[11px] bg-zinc-50 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 border-zinc-100 dark:border-zinc-700">İptal</AlertDialogCancel> <AlertDialogCancel className="h-12 px-8 rounded-xl font-bold bg-white/5 text-white border-white/10">Hayır</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={() => handleDelete(trip.id)} onClick={() => 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
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
<div className="mt-10 flex items-center justify-between border-t border-zinc-100 dark:border-zinc-900 pt-8"> <div className="mt-8 flex items-center justify-between border-t border-gray-50 dark:border-white/5 pt-6">
<div className="flex -space-x-3"> <div className="flex -space-x-2">
{trip.itinerary.days[0].items.slice(0, 4).map((item, i) => ( {trip.itinerary.days[0].items.slice(0, 3).map((item, i) => (
<div key={i} className="w-12 h-12 rounded-2xl border-4 border-white dark:border-zinc-950 bg-zinc-100 overflow-hidden shadow-sm"> <div key={i} className="w-10 h-10 rounded-xl border-2 border-white dark:border-secondary bg-gray-100 overflow-hidden shadow-md">
<img <img
src={item.photo_reference ? (item.photo_reference.startsWith('http') ? item.photo_reference : api.getPhotoUrl(item.photo_reference)) : 'https://via.placeholder.com/100'} src={item.photo_reference ? (item.photo_reference.startsWith('http') ? item.photo_reference : api.getPhotoUrl(item.photo_reference)) : 'https://via.placeholder.com/100'}
alt="" alt=""
@ -202,15 +211,10 @@ export default function AccountPage() {
/> />
</div> </div>
))} ))}
{trip.itinerary.days[0].items.length > 4 && (
<div className="w-12 h-12 rounded-2xl border-4 border-white dark:border-zinc-950 bg-zinc-100 flex items-center justify-center text-[10px] font-black text-zinc-400">
+{trip.itinerary.days[0].items.length - 4}
</div> </div>
)} <Button className="h-11 px-6 rounded-xl bg-secondary dark:bg-white/10 hover:bg-primary hover:text-white transition-luxury font-black uppercase tracking-widest text-[9px] gap-2" asChild>
</div>
<Button className="h-12 px-8 rounded-2xl bg-zinc-900 hover:bg-black text-white transition-all duration-300 font-black uppercase tracking-widest text-[10px] gap-2 shadow-md" asChild>
<Link to={`/trip/${trip.id}`}> <Link to={`/trip/${trip.id}`}>
İNCELE
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
</Link> </Link>
</Button> </Button>

View File

@ -3,9 +3,8 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input'; 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 { motion, AnimatePresence } from 'framer-motion';
import { Link } from 'react-router-dom';
const CATEGORIES = [ const CATEGORIES = [
{ id: 'all', label: 'Tümü' }, { id: 'all', label: 'Tümü' },
@ -24,7 +23,7 @@ const PLACES = [
rating: 4.8, rating: 4.8,
reviews: 12400, reviews: 12400,
duration: '2-3 Saat', 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.', 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, rating: 4.7,
reviews: 8500, reviews: 8500,
duration: '1 Saat', 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.', 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, rating: 4.9,
reviews: 15200, reviews: 15200,
duration: '1.5 Saat', 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ı.', 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, rating: 4.6,
reviews: 6300, reviews: 6300,
duration: '4-5 Saat', 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.', 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, rating: 4.8,
reviews: 9100, reviews: 9100,
duration: '1 Saat', 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.', 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, rating: 4.5,
reviews: 4200, reviews: 4200,
duration: '1-2 Saat', 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.', 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 ( return (
<div className="min-h-screen bg-[#FDFDFD] dark:bg-black selection:bg-primary/20 pb-24"> <div className="min-h-screen bg-background selection:bg-primary/20 pb-20">
{/* Minimalist Header */} {/* Immersive Header */}
<section className="pt-24 pb-16 border-b border-zinc-100 dark:border-zinc-900 bg-white dark:bg-zinc-950"> <section className="relative h-[40vh] flex items-center justify-center overflow-hidden mb-12">
<div className="container px-6"> <div className="absolute inset-0 z-0 scale-105">
<div className="max-w-4xl mx-auto space-y-10"> <img
<div className="space-y-4 text-center md:text-left"> src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400"
<motion.div alt="Explore Hero"
initial={{ opacity: 0, y: -10 }} className="w-full h-full object-cover grayscale-[0.2]"
animate={{ opacity: 1, y: 0 }} />
className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 text-[10px] font-black uppercase tracking-widest" <div className="absolute inset-0 bg-gradient-to-t from-background via-background/20 to-transparent z-10" />
>
<MapPin className="h-3.5 w-3.5 text-primary" />
KEŞİF REHBERİ
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-4xl md:text-6xl font-black text-zinc-900 dark:text-white tracking-tighter leading-tight uppercase"
>
Kapadokya'yı <br /> <span className="text-primary italic">Keşfedin</span>
</motion.h1>
</div> </div>
<div className="container relative z-20 px-6 text-center space-y-6">
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/10 backdrop-blur-md border border-white/20 text-white text-[9px] font-black uppercase tracking-widest"
>
<Sparkles className="h-3.5 w-3.5 text-accent" />
Efsanevi Duraklar
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-4xl md:text-6xl font-black text-white tracking-tighter leading-none uppercase"
>
KAPADOKYA <span className="text-primary uppercase">KEŞFİ</span>
</motion.h1>
<motion.div
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }} transition={{ delay: 0.2 }}
className="relative group max-w-2xl" className="max-w-xl mx-auto relative group"
> >
<Search className="absolute left-5 top-1/2 -translate-y-1/2 h-5 w-5 text-zinc-400 group-focus-within:text-primary transition-colors" /> <Search className="absolute left-5 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 group-focus-within:text-primary transition-colors" />
<Input <Input
placeholder="Mekan veya aktivite arayın..." placeholder="Bir efsane veya aktivite arayın..."
className="w-full h-16 pl-12 pr-6 bg-zinc-50 dark:bg-zinc-900 border-zinc-100 dark:border-zinc-800 text-zinc-900 dark:text-white placeholder:text-zinc-400 rounded-[1.5rem] text-base font-bold focus:ring-primary/20 transition-all shadow-sm" className="w-full h-14 pl-12 pr-6 bg-white/10 backdrop-blur-xl border-white/20 text-white placeholder:text-white/40 rounded-2xl text-base font-bold focus:bg-white focus:text-gray-900 transition-luxury shadow-2xl"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</motion.div> </motion.div>
</div> </div>
</div>
</section> </section>
<div className="container px-6 pt-12"> <div className="container px-6">
{/* Categories Carousel */} {/* Categories Carousel */}
<div className="flex items-center gap-3 overflow-x-auto pb-8 mb-4 no-scrollbar"> <div className="flex items-center gap-3 overflow-x-auto pb-6 mb-12 no-scrollbar">
<Button variant="outline" className="h-11 px-6 rounded-xl border-2 border-gray-100 dark:border-white/10 font-black uppercase tracking-widest text-[10px] shrink-0 bg-white/5 backdrop-blur-md">
<Filter className="h-3.5 w-3.5 mr-2 text-primary" />
Filtrele
</Button>
<div className="w-px h-6 bg-gray-100 dark:bg-white/10 mx-1 shrink-0" />
{CATEGORIES.map((cat) => ( {CATEGORIES.map((cat) => (
<Button <Button
key={cat.id} key={cat.id}
onClick={() => setSelectedCategory(cat.id)} onClick={() => setSelectedCategory(cat.id)}
className={`h-10 px-6 rounded-2xl font-black uppercase tracking-widest text-[10px] shrink-0 transition-all duration-300 ${selectedCategory === cat.id className={`h-11 px-8 rounded-xl font-black uppercase tracking-widest text-[10px] shrink-0 transition-luxury ${selectedCategory === cat.id
? 'bg-zinc-900 text-white shadow-md' ? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 text-zinc-400 hover:border-zinc-300' : 'bg-white/5 border-2 border-gray-100 dark:border-white/10 text-gray-400 hover:border-primary/40 hover:text-primary'
}`} }`}
> >
{cat.label} {cat.label}
@ -158,61 +168,77 @@ export default function ExplorePage() {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="grid md:grid-cols-2 lg:grid-cols-3 gap-10" className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"
> >
{filteredPlaces.map((place, index) => ( {filteredPlaces.map((place, index) => (
<motion.div <motion.div
key={place.id} key={place.id}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }} transition={{ delay: index * 0.1 }}
> >
<Card className="group overflow-hidden border-zinc-100 dark:border-zinc-900 bg-white dark:bg-zinc-950 rounded-[2rem] shadow-sm hover:shadow-xl transition-all duration-500 border-none"> <Card className="group overflow-hidden border-2 border-gray-50 dark:border-white/5 bg-white dark:bg-white/5 rounded-3xl shadow-lg hover:shadow-2xl hover:-translate-y-1 transition-luxury">
<CardContent className="p-0"> <CardContent className="p-0">
<div className="relative h-72 overflow-hidden"> <div className="relative h-64 overflow-hidden">
<img <img
src={place.image} src={place.image}
alt={place.name} alt={place.name}
className="w-full h-full object-cover transition-all duration-700 group-hover:scale-105" className="w-full h-full object-cover transition-luxury duration-700 group-hover:scale-105"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-all" /> <div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-luxury" />
<div className="absolute top-5 left-5"> <div className="absolute top-4 left-4">
<Badge className="bg-white/90 backdrop-blur-md text-zinc-900 border-none font-black uppercase tracking-widest text-[9px] px-3 py-1 rounded-full"> <Badge className="bg-white/10 backdrop-blur-md text-white border-white/20 font-black uppercase tracking-widest text-[9px] px-3 py-0.5 rounded-full">
{CATEGORIES.find(c => c.id === place.category)?.label} {CATEGORIES.find(c => c.id === place.category)?.label}
</Badge> </Badge>
</div> </div>
<div className="absolute top-5 right-5"> <div className="absolute bottom-4 left-4 right-4 flex items-center justify-between opacity-0 group-hover:opacity-100 translate-y-2 group-hover:translate-y-0 transition-luxury">
<Badge className="bg-primary text-white border-none font-black text-xs px-2 py-1 rounded-xl flex items-center gap-1 shadow-lg"> <div className="flex items-center gap-2">
<Star className="h-3 w-3 fill-white" /> <div className="w-8 h-8 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white hover:bg-primary transition-colors cursor-pointer">
<Heart className="h-4 w-4" />
</div>
<div className="w-8 h-8 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white hover:bg-primary transition-colors cursor-pointer">
<Share2 className="h-4 w-4" />
</div>
</div>
<Button className="bg-primary text-white font-black uppercase tracking-widest text-[9px] rounded-full h-8 px-4">
Planla
</Button>
</div>
<div className="absolute top-4 right-4">
<Badge className="bg-accent text-gray-900 border-none font-black text-xs px-2 py-0.5 rounded-lg flex items-center gap-1 shadow-lg">
<Star className="h-3 w-3 fill-gray-900" />
{place.rating} {place.rating}
</Badge> </Badge>
</div> </div>
</div> </div>
<div className="p-8 space-y-4"> <div className="p-6 md:p-8 space-y-4">
<div className="space-y-2"> <div className="space-y-1">
<h3 className="text-2xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase leading-tight group-hover:text-primary transition-colors"> <h3 className="text-2xl font-black text-gray-900 dark:text-white tracking-tighter uppercase leading-tight group-hover:text-primary transition-colors">
{place.name} {place.name}
</h3> </h3>
<p className="text-zinc-500 font-medium leading-relaxed text-sm line-clamp-2"> <p className="text-gray-500 font-medium italic leading-relaxed text-sm line-clamp-2">
{place.description} "{place.description}"
</p> </p>
</div> </div>
<div className="pt-6 flex items-center justify-between border-t border-zinc-100 dark:border-zinc-900"> <div className="pt-6 flex items-center justify-between border-t border-gray-50 dark:border-white/5">
<div className="flex items-center gap-4"> <div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-1.5 text-[10px] font-black text-zinc-400 uppercase tracking-widest"> <div className="flex items-center gap-1.5 text-[9px] font-black text-gray-400 uppercase tracking-widest">
<Clock className="h-4 w-4 text-primary" /> <Clock className="h-3.5 w-3.5 text-primary" />
<span>{place.duration}</span> <span>{place.duration}</span>
</div> </div>
<div className="flex items-center gap-1.5 text-[9px] font-black text-gray-400 uppercase tracking-widest">
<Camera className="h-3.5 w-3.5 text-primary" />
<span>Görsel</span>
</div> </div>
<Button variant="ghost" size="icon" className="w-10 h-10 rounded-2xl bg-zinc-50 dark:bg-zinc-900 text-zinc-400 hover:bg-primary hover:text-white transition-all group/btn" asChild> </div>
<Link to="/planner"> <button className="w-10 h-10 rounded-xl bg-gray-50 dark:bg-white/10 flex items-center justify-center text-gray-400 hover:bg-primary hover:text-white transition-luxury group/btn">
<ChevronRight className="h-5 w-5 group-hover/btn:translate-x-0.5 transition-transform" /> <ArrowUpRight className="h-5 w-5 group-hover/btn:rotate-45 transition-transform" />
</Link> </button>
</Button>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -225,17 +251,24 @@ export default function ExplorePage() {
key="empty" key="empty"
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
className="py-24 text-center space-y-6" className="py-20 text-center space-y-6"
> >
<div className="w-20 h-20 bg-zinc-50 dark:bg-zinc-900 rounded-3xl flex items-center justify-center mx-auto mb-6 border border-zinc-100 dark:border-zinc-800"> <div className="w-24 h-24 bg-gray-50 dark:bg-white/5 rounded-2xl flex items-center justify-center mx-auto mb-6 border-2 border-dashed border-gray-200 dark:border-white/10">
<Search className="h-8 w-8 text-zinc-200" /> <Search className="h-10 w-10 text-gray-200" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-2xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase">SONUÇ YOK</h3> <h3 className="text-2xl font-black text-gray-900 dark:text-white tracking-tighter uppercase">BULUNAMADI</h3>
<p className="text-sm text-zinc-500 font-medium max-w-xs mx-auto"> <p className="text-base text-gray-500 font-medium italic max-w-xs mx-auto">
Aramanıza uygun mekan bulunamadı. Farklı bir keşif terimi deneyin.
</p> </p>
</div> </div>
<Button
variant="outline"
onClick={() => { setSearchQuery(''); setSelectedCategory('all'); }}
className="h-12 px-8 rounded-xl font-black uppercase tracking-widest text-[10px] border-2 border-primary text-primary hover:bg-primary hover:text-white transition-luxury"
>
Tümü
</Button>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>

View File

@ -1,7 +1,8 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Sparkles, MapPin, Calendar, Compass, ShieldCheck, Zap, ArrowRight, Star, Globe, ChevronRight } from 'lucide-react'; import { Sparkles, MapPin, Calendar, Compass, ShieldCheck, Zap, ArrowRight, Star, ArrowUpRight, Play, Globe } from 'lucide-react';
import { motion } from 'framer-motion'; 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 }) => ( const FeatureCard = ({ icon: Icon, title, description, index }: { icon: any, title: string, description: string, index: number }) => (
<motion.div <motion.div
@ -9,190 +10,222 @@ const FeatureCard = ({ icon: Icon, title, description, index }: { icon: any, tit
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }} transition={{ duration: 0.5, delay: index * 0.1 }}
className="group p-8 bg-white dark:bg-zinc-900 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-sm hover:shadow-md transition-all duration-300" className="group p-6 bg-white/40 dark:bg-white/5 backdrop-blur-xl rounded-3xl border border-white/20 dark:border-white/10 shadow-luxury hover:shadow-xl transition-luxury"
> >
<div className="w-12 h-12 bg-zinc-100 dark:bg-zinc-800 rounded-2xl flex items-center justify-center mb-6 group-hover:bg-primary/10 transition-colors"> <div className="w-12 h-12 bg-gradient-to-br from-primary/20 to-accent/20 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-luxury">
<Icon className="h-6 w-6 text-zinc-600 dark:text-zinc-400 group-hover:text-primary transition-colors" /> <Icon className="h-6 w-6 text-primary" />
</div> </div>
<h3 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-3 tracking-tight">{title}</h3> <h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3 tracking-tight">{title}</h3>
<p className="text-zinc-500 dark:text-zinc-400 leading-relaxed text-sm">{description}</p> <p className="text-gray-600 dark:text-gray-400 leading-relaxed text-sm font-medium">{description}</p>
</motion.div> </motion.div>
); );
export default function LandingPage() { export default function LandingPage() {
return ( const containerRef = useRef<HTMLDivElement>(null);
<div className="min-h-screen bg-[#FDFDFD] dark:bg-black selection:bg-primary/20"> const { scrollYProgress } = useScroll({
{/* Hero Section */} target: containerRef,
<section className="relative pt-20 pb-20 md:pt-32 md:pb-32 overflow-hidden"> offset: ["start start", "end end"]
{/* Subtle background patterns */} });
<div className="absolute inset-0 z-0 opacity-[0.03] dark:opacity-[0.05] pointer-events-none">
<svg className="h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<defs>
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="currentColor" strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div>
<div className="container relative z-10 px-6"> const heroY = useTransform(scrollYProgress, [0, 1], [0, 200]);
<div className="max-w-4xl mx-auto text-center space-y-8"> const heroOpacity = useTransform(scrollYProgress, [0, 0.2], [1, 0]);
return (
<div className="min-h-screen bg-background selection:bg-primary/20" ref={containerRef}>
{/* Hero Section */}
<section className="relative h-[90vh] flex items-center justify-center overflow-hidden">
{/* Cinematic Background */}
<motion.div <motion.div
initial={{ opacity: 0, y: -20 }} style={{ y: heroY, opacity: heroOpacity }}
animate={{ opacity: 1, y: 0 }} className="absolute inset-0 z-0"
transition={{ duration: 0.5 }}
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 text-zinc-600 dark:text-zinc-300 text-[11px] font-bold tracking-widest uppercase"
> >
<Sparkles className="h-3.5 w-3.5 text-primary" /> <div className="absolute inset-0 bg-black/40 z-10" />
Yapay Zeka Destekli Seyahat Rehberiniz <img
src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400"
alt="Cappadocia"
className="w-full h-full object-cover scale-105"
/>
</motion.div>
{/* Hero Content */}
<div className="container relative z-20 px-6 text-center">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/10 backdrop-blur-md border border-white/20 text-white text-[10px] font-bold mb-8 tracking-widest uppercase"
>
<Sparkles className="h-3.5 w-3.5 text-accent" />
Yapay Zeka Destekli Premium Deneyim
</motion.div> </motion.div>
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
className="text-5xl md:text-7xl lg:text-8xl font-black text-zinc-900 dark:text-white leading-[1.1] tracking-tighter" className="text-5xl md:text-7xl lg:text-8xl font-black text-white mb-8 leading-[0.95] tracking-tighter"
> >
Kapadokya'yı <br /> KAPADOKYA <br />
<span className="text-primary italic">Yeniden</span> Keşfedin <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary via-accent to-primary animate-gradient">
EFSANESİ
</span>
</motion.h1> </motion.h1>
<motion.p <motion.p
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }} transition={{ duration: 0.8, delay: 0.4 }}
className="text-lg md:text-2xl text-zinc-500 dark:text-zinc-400 max-w-2xl mx-auto leading-relaxed font-medium" className="text-lg md:text-xl text-white/80 max-w-2xl mx-auto mb-12 leading-relaxed font-medium italic"
> >
Kişiselleştirilmiş rotalar, anlık öneriler ve kusursuz bir planlama deneyimi. Yapay zeka ile her detayı düşünülmüş bir Kapadokya efsanesi. "Sıradan bir gezi değil, ruhunuza dokunacak bir keşif hikayesi. Saniyeler içinde size özel kurgulanmış premium rotalar."
</motion.p> </motion.p>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }} transition={{ duration: 0.8, delay: 0.6 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4" className="flex flex-col sm:flex-row items-center justify-center gap-4"
> >
<Button size="lg" className="h-14 px-10 text-lg font-bold bg-primary hover:bg-primary-dark shadow-lg shadow-primary/20 rounded-2xl transition-all duration-300" asChild> <Button size="lg" className="h-14 px-8 text-lg font-bold bg-primary hover:bg-primary-dark shadow-xl shadow-primary/20 rounded-2xl transition-luxury group" asChild>
<Link to="/planner"> <Link to="/planner">
Hemen Başlayın Hemen Keşfet
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</Link>
</Button>
<Button size="lg" variant="outline" className="h-14 px-10 text-lg font-bold border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-900 rounded-2xl transition-all duration-300" asChild>
<Link to="/explore">
Rotayı Keşfet
</Link> </Link>
</Button> </Button>
<button className="h-14 px-8 text-lg font-bold bg-white/10 backdrop-blur-xl border border-white/20 text-white rounded-2xl hover:bg-white/20 transition-luxury flex items-center gap-2">
<Play className="h-5 w-5 fill-white" />
Tanıtımı İzle
</button>
</motion.div> </motion.div>
</div> </div>
{/* Scroll Indicator */}
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ repeat: Infinity, duration: 2 }}
className="absolute bottom-8 left-1/2 -translate-x-1/2 text-white/40"
>
<div className="w-5 h-8 border-2 border-white/20 rounded-full flex justify-center p-1">
<div className="w-1 h-1 bg-white/40 rounded-full" />
</div> </div>
</motion.div>
</section> </section>
{/* Trust Stats */} {/* Stats Section */}
<section className="py-12 border-y border-zinc-100 dark:border-zinc-900 bg-white dark:bg-zinc-950"> <section className="py-16 bg-secondary text-white relative overflow-hidden">
<div className="container px-6"> <div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/10 rounded-full blur-[100px] -translate-y-1/2 translate-x-1/2" />
<div className="flex flex-wrap justify-center items-center gap-8 md:gap-16 opacity-50 grayscale contrast-125"> <div className="container relative z-10 px-6">
<div className="flex items-center gap-2"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-8 text-center">
<Star className="h-5 w-5 fill-zinc-900 dark:fill-white" /> {[
<span className="font-bold tracking-tighter text-lg">TOP DESTINATION 2026</span> { label: "Mutlu Gezgin", value: "25k+", icon: Globe },
</div> { label: "Kişiye Özel Rota", value: "100k+", icon: Compass },
<div className="flex items-center gap-2"> { label: "Doğrulanmış Mekan", value: "1.2k", icon: MapPin },
<Globe className="h-5 w-5" /> { label: "Müşteri Puanı", value: "4.9", icon: Star },
<span className="font-bold tracking-tighter text-lg">GLOBAL REACH</span> ].map((stat, i) => (
</div> <motion.div
<div className="flex items-center gap-2"> key={i}
<ShieldCheck className="h-5 w-5" /> initial={{ opacity: 0, scale: 0.5 }}
<span className="font-bold tracking-tighter text-lg">SECURE BOOKING</span> whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.1 }}
className="space-y-3"
>
<div className="w-10 h-10 mx-auto bg-white/5 rounded-xl flex items-center justify-center">
<stat.icon className="h-5 w-5 text-accent" />
</div> </div>
<div className="text-3xl md:text-4xl font-black text-primary">{stat.value}</div>
<div className="text-white/60 font-bold tracking-widest uppercase text-[10px]">{stat.label}</div>
</motion.div>
))}
</div> </div>
</div> </div>
</section> </section>
{/* Features Grid */} {/* Features Grid */}
<section className="py-24 bg-[#FDFDFD] dark:bg-black"> <section className="py-20 bg-background relative overflow-hidden">
<div className="container px-6"> <div className="container px-6">
<div className="max-w-2xl mb-20 space-y-4"> <div className="max-w-2xl mx-auto text-center mb-16 space-y-4">
<motion.span <motion.span
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }} whileInView={{ opacity: 1 }}
className="text-primary font-bold tracking-widest uppercase text-xs" className="text-primary font-black tracking-widest uppercase text-xs"
> >
Neden Biz? Kusursuz Mühendislik
</motion.span> </motion.span>
<motion.h2 <motion.h2
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
className="text-4xl md:text-5xl font-black text-zinc-900 dark:text-white leading-tight tracking-tighter" className="text-3xl md:text-5xl font-black text-gray-900 dark:text-white leading-tight"
> >
Seyahat Planlamanın <br /> Geleceği Burada Seyahatinizi Sanata <br /> <span className="text-gradient">Dönüştürüyoruz</span>
</motion.h2> </motion.h2>
</div> </div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<FeatureCard <FeatureCard
index={0} index={0}
icon={Zap} icon={Zap}
title="Yapay Zeka Motoru" title="Yapay Zeka Mimari"
description="Tercihlerinize göre saniyeler içinde binlerce olasılığı tarayan akıllı seyahat motoru." description="OpenAI 4o-mini altyapısıyla her detayı düşünülmüş, tutarlı ve akıcı bir seyahat programı."
/> />
<FeatureCard <FeatureCard
index={1} index={1}
icon={MapPin} icon={ShieldCheck}
title="Gerçek Zamanlı Veri" title="Premium Veri"
description="Google Places entegrasyonu ile her zaman güncel çalışma saatleri ve güvenilir fotoğraflar." description="Google Places verileriyle gerçek fotoğraflar, anlık çalışma saatleri ve güvenilir yorumlar."
/> />
<FeatureCard <FeatureCard
index={2} index={2}
icon={Compass} icon={MapPin}
title="Yerel Kürasyon" title="Akıllı Navigasyon"
description="Sadece turistik yerler değil, yerel uzmanlarımızın onayladığı gizli cevherler." description="Sadece rota değil, Google Directions ile en mantıklı sıralama ve ulaşım süreleri."
/> />
<FeatureCard <FeatureCard
index={3} index={3}
icon={Calendar} icon={Compass}
title="Zahmetsiz Yönetim" title="Kürasyon"
description="Sürükle bırak özelliği ile planınızı anında revize edin, rota otomatik güncellensin." description="Yerel rehberlerin gizli favorileri ve turistik kalabalıkların ötesindeki özel duraklar."
/> />
<FeatureCard <FeatureCard
index={4} index={4}
icon={ShieldCheck} icon={Calendar}
title="Güvenilir Operatörler" title="Dinamik Kontrol"
description="Bölgenin en iyi balon ve aktivite operatörleri ile doğrudan iletişim ve güvenli rezervasyon." description="Planınızı dilediğiniz zaman sürükle-bırak yöntemiyle revize edin, her an güncel kalın."
/> />
<FeatureCard <FeatureCard
index={5} index={5}
icon={Sparkles} icon={Sparkles}
title="Kusursuz Arayüz" title="Modern Estetik"
description="Karmaşadan uzak, sadece size ve seyahatinize odaklanan minimalist tasarım." description="En yüksek standartlarda kullanıcı deneyimi için tasarlanmış akıcı ve şık arayüz."
/> />
</div> </div>
</div> </div>
</section> </section>
{/* Modern CTA */} {/* Luxury CTA */}
<section className="py-24 px-6"> <section className="py-20 px-6">
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.98 }} initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
className="max-w-6xl mx-auto rounded-[3rem] bg-zinc-900 dark:bg-zinc-900 p-12 lg:p-24 text-center relative overflow-hidden group shadow-2xl" className="max-w-6xl mx-auto rounded-[2.5rem] bg-secondary p-12 lg:p-20 text-center relative overflow-hidden group"
> >
{/* Subtle noise/texture overlay */} <div className="absolute inset-0 opacity-20 grayscale hover:grayscale-0 transition-luxury duration-1000 group-hover:scale-105">
<div className="absolute inset-0 opacity-[0.05] pointer-events-none bg-[url('https://www.transparenttextures.com/patterns/asfalt-dark.png')]" /> <img src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400" alt="CTA" className="w-full h-full object-cover" />
</div>
<div className="relative z-10 space-y-10"> <div className="relative z-10 space-y-8">
<h2 className="text-4xl lg:text-7xl font-black text-white leading-[1.1] tracking-tighter"> <h2 className="text-4xl lg:text-7xl font-black text-white leading-[0.95] tracking-tighter uppercase">
BÜYÜLÜ BİR GEZİ <br /> SİZİ BEKLİYOR BİR SONRAKİ <br /> <span className="text-primary">EFSANENİZİ</span> YAZIN
</h2> </h2>
<p className="text-zinc-400 text-lg md:text-xl max-w-xl mx-auto font-medium leading-relaxed"> <p className="text-white/60 text-lg md:text-xl max-w-xl mx-auto font-medium leading-relaxed">
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.
</p> </p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4"> <div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
<Button size="lg" className="h-16 px-12 text-xl font-bold bg-white text-black hover:bg-zinc-200 rounded-2xl transition-all duration-300 group shadow-xl" asChild> <Button size="lg" className="h-16 px-10 text-xl font-bold bg-primary hover:bg-primary-dark rounded-2xl shadow-2xl shadow-primary/20 transition-luxury group" asChild>
<Link to="/planner" className="flex items-center gap-3"> <Link to="/planner" className="flex items-center gap-3">
Ücretsiz Başlayın Rotanı Oluştur
<ChevronRight className="h-6 w-6 group-hover:translate-x-1 transition-transform" /> <ArrowUpRight className="h-6 w-6 group-hover:rotate-45 transition-transform" />
</Link> </Link>
</Button> </Button>
</div> </div>
@ -200,54 +233,32 @@ export default function LandingPage() {
</motion.div> </motion.div>
</section> </section>
{/* Simple Footer */} {/* Modern Footer */}
<footer className="py-20 border-t border-zinc-100 dark:border-zinc-900 bg-white dark:bg-black"> <footer className="py-16 border-t border-border bg-background">
<div className="container px-6"> <div className="container px-6">
<div className="flex flex-col md:flex-row justify-between items-start gap-12"> <div className="flex flex-col md:flex-row justify-between items-center gap-8">
<div className="space-y-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<MapPin className="h-6 w-6 text-primary" /> <div className="w-10 h-10 bg-primary rounded-xl flex items-center justify-center text-white">
<MapPin className="h-5 w-5" />
</div>
<span className="text-xl font-black tracking-tighter uppercase dark:text-white"> <span className="text-xl font-black tracking-tighter uppercase dark:text-white">
Kapadokya <span className="text-primary">Efsanesi</span> Kapadokya <span className="text-primary">Efsanesi</span>
</span> </span>
</div> </div>
<p className="text-zinc-500 text-sm max-w-xs leading-relaxed">
Yapay zeka gücüyle Kapadokya seyahatlerinizi daha anlamlı ve unutulmaz kılıyoruz.
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-12"> <div className="flex items-center gap-8 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
<div className="space-y-4"> <Link to="/explore" className="hover:text-primary transition-colors">Keşfet</Link>
<h4 className="text-xs font-black uppercase tracking-widest text-zinc-900 dark:text-white">Ürün</h4> <Link to="/planner" className="hover:text-primary transition-colors">Planla</Link>
<ul className="space-y-2 text-sm text-zinc-500"> <Link to="/account" className="hover:text-primary transition-colors">Hesabım</Link>
<li><Link to="/explore" className="hover:text-primary transition-colors">Keşfet</Link></li>
<li><Link to="/planner" className="hover:text-primary transition-colors">Planla</Link></li>
<li><Link to="/pricing" className="hover:text-primary transition-colors">Fiyatlandırma</Link></li>
</ul>
</div>
<div className="space-y-4">
<h4 className="text-xs font-black uppercase tracking-widest text-zinc-900 dark:text-white">Destek</h4>
<ul className="space-y-2 text-sm text-zinc-500">
<li><Link to="/contact" className="hover:text-primary transition-colors">İletişim</Link></li>
<li><Link to="/faq" className="hover:text-primary transition-colors">S.S.S</Link></li>
</ul>
</div>
<div className="space-y-4">
<h4 className="text-xs font-black uppercase tracking-widest text-zinc-900 dark:text-white">Yasal</h4>
<ul className="space-y-2 text-sm text-zinc-500">
<li><Link to="/privacy" className="hover:text-primary transition-colors">Gizlilik</Link></li>
<li><Link to="/terms" className="hover:text-primary transition-colors">Şartlar</Link></li>
</ul>
</div>
</div> </div>
</div> </div>
<div className="mt-20 pt-10 border-t border-zinc-100 dark:border-zinc-900 flex flex-col md:flex-row justify-between items-center gap-4 text-zinc-400 text-xs font-medium"> <div className="mt-16 pt-10 border-t border-border flex flex-col md:flex-row justify-between items-center gap-4 text-gray-500 text-xs font-medium">
<p>© 2026 Kapadokya Efsanesi. Tripizy esintisiyle.</p> <p>© 2026 Cappadocia Legend. Her anı bir hikaye.</p>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<span className="flex items-center gap-1"> <span>TR</span>
Made with <span className="text-red-500"></span> in Cappadocia <div className="h-3 w-px bg-border" />
</span> <span>USD</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,12 +11,12 @@ import { motion } from 'framer-motion';
export default function LoginPage() { export default function LoginPage() {
const [isLogin, setIsLogin] = useState(true); const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false); const [rememberMe, setRememberMe] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { signIn, signUp, user } = useAuth(); const { signInWithUsername, signUpWithUsername, user } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const from = location.state?.from || '/explore'; const from = location.state?.from || '/explore';
@ -27,7 +27,12 @@ export default function LoginPage() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); 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) { if (password.length < 6) {
toast.error('Şifre en az 6 karakter olmalıdır'); toast.error('Şifre en az 6 karakter olmalıdır');
@ -37,25 +42,25 @@ export default function LoginPage() {
setLoading(true); setLoading(true);
try { try {
if (isLogin) { if (isLogin) {
const { error } = await signIn(email, password); const { error } = await signInWithUsername(username, password);
if (error) { if (error) {
if (error.message.includes('Invalid login credentials')) { 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; throw error;
} }
toast.success(`Hoşgeldin!`); toast.success(`Hoşgeldin, ${username}!`);
navigate(from, { replace: true }); navigate(from, { replace: true });
} else { } else {
const { error } = await signUp(email, password); const { error } = await signUpWithUsername(username, password);
if (error) { if (error) {
if (error.message.includes('already registered')) { 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; throw error;
} }
toast.success('Hesap oluşturuldu! Giriş yapılıyor...'); 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) { if (!signInError) {
navigate(from, { replace: true }); navigate(from, { replace: true });
} }
@ -136,14 +141,13 @@ export default function LoginPage() {
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-5"> <div className="space-y-5">
<div className="space-y-2.5"> <div className="space-y-2.5">
<Label htmlFor="email" className="text-[10px] font-black uppercase tracking-widest text-primary">E-posta Adresi</Label> <Label htmlFor="username" className="text-[10px] font-black uppercase tracking-widest text-primary">Kullanıcı Kimliği</Label>
<Input <Input
id="email" id="username"
type="email" placeholder="kullanici_adi"
placeholder="eposta@adresiniz.com"
required required
value={email} value={username}
onChange={e => setEmail(e.target.value)} 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" 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"
/> />
</div> </div>

View File

@ -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;
}

View File

@ -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();

View File

@ -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);

View File

@ -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()
);