Autosave: 20260304-124313

This commit is contained in:
Flatlogic Bot 2026-03-04 12:43:13 +00:00
parent 886283c152
commit a23dcbb154
12 changed files with 1104 additions and 935 deletions

View File

@ -1,5 +1,5 @@
VITE_SUPABASE_URL=https://ofqojaxiopqxahfvxpmx.supabase.co VITE_SUPABASE_URL=https://ofqojaxiopqxahfvxpmx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9mcW9qYXhpb3BxeGFoZnZ4cG14Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIyODExMjAsImV4cCI6MjA4Nzg1NzEyMH0.CVyjWPp9ldCd5qxA4TbViD5MJ0axbEWfGr-1n1pPjn0 VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9mcW9qYXhpb3BxeGFoZnZ4cG14Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIyODExMjAsImV4cCI6MjA4Nzg1NzEyMH0.CVyjWPp9ldCd5qxA4TbViD5MJ0axbEWfGr-1n1pPjn0
VITE_GOOGLE_MAPS_API_KEY=AIzaSyCLPiqNWwFSUS0X15YvTdHZxrb-2LXoYlw VITE_GOOGLE_MAPS_API_KEY=AIzaSyBbXWk7VhZfzn9txrAr9N-faAPuKy_LnKw
VITE_APP_ID=app-9xzmfic2e4g1 VITE_APP_ID=app-9xzmfic2e4g1
VITE_FORM_ID=form-9xzmfic2e4g1 VITE_FORM_ID=form-9xzmfic2e4g1

View File

@ -2,8 +2,7 @@ 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 { Separator } from '@/components/ui/separator'; import { MapPin, History, User, LogOut, Search, Settings } from 'lucide-react';
import { MapPin, History, Compass, User, LogOut, Search, Settings, FileText } from 'lucide-react';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -24,7 +23,6 @@ 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);
@ -40,117 +38,113 @@ export function Navbar() {
return ( return (
<> <>
<nav className={cn( <nav className={cn(
"fixed top-0 left-0 right-0 z-50 w-full border-b bg-background/95 backdrop-blur-navbar transition-smooth", "fixed top-0 left-0 right-0 z-50 w-full transition-all duration-300 h-16 border-b",
scrolled && "navbar-shadow" scrolled
? "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 px-4 lg:px-6"> <div className="container mx-auto h-full px-6">
<div className="flex h-16 items-center justify-between"> <div className="flex h-full items-center justify-between">
{/* Sol: Logo ve Marka */} {/* Left: Logo */}
<Link to="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity"> <Link to="/" className="flex items-center gap-2 group">
<MapPin className="h-8 w-8 text-primary" /> <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">
<span className="text-lg font-semibold tracking-tight text-orange-600">Kapadokya</span> <MapPin className="h-5 w-5" />
</div>
<span className="text-lg font-black tracking-tighter uppercase dark:text-white">
Kapadokya <span className="text-primary">Efsanesi</span>
</span>
</Link> </Link>
{/* Orta: Desktop Navigasyon */} {/* Center: Desktop Nav */}
<div className="hidden lg:flex items-center gap-8"> <div className="hidden lg:flex items-center gap-10">
<Link <Link
to="/explore" to="/explore"
className="text-sm font-medium text-muted-foreground hover:text-primary transition-colors" className="text-xs font-black uppercase tracking-widest text-zinc-500 hover:text-primary transition-colors"
> >
Keşfet Keşfet
</Link> </Link>
<Link <Link
to="/planner" to="/planner"
className="text-sm font-medium text-muted-foreground hover:text-primary transition-colors" className="text-xs font-black uppercase tracking-widest text-zinc-500 hover:text-primary transition-colors"
> >
Planla Planla
</Link> </Link>
<Link <Link
to="/account" to="/account"
className="text-sm font-medium text-muted-foreground hover:text-primary transition-colors" className="text-xs font-black uppercase tracking-widest text-zinc-500 hover:text-primary transition-colors"
> >
Gezilerim Gezilerim
</Link> </Link>
</div> </div>
{/* Sağ: Aksiyonlar */} {/* Right: Actions */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
{/* Arama butonu */}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setSearchOpen(true)} onClick={() => setSearchOpen(true)}
aria-label="Ara" className="rounded-xl text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-900 h-9 w-9"
> >
<Search className="h-5 w-5" /> <Search className="h-4 w-4" />
</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-full"> <Button variant="ghost" className="relative h-9 w-9 rounded-xl p-0 hover:bg-zinc-100 dark:hover:bg-zinc-900">
<Avatar className="h-9 w-9"> <Avatar className="h-9 w-9 rounded-xl border-2 border-zinc-100 dark:border-zinc-800">
<AvatarFallback className="bg-primary text-primary-foreground text-sm"> <AvatarFallback className="bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-white text-[10px] font-black">
{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-60"> <DropdownMenuContent align="end" className="w-64 rounded-2xl p-2 border-zinc-100 dark:border-zinc-800 shadow-2xl">
<div className="flex items-center gap-3 p-3"> <div className="flex items-center gap-3 p-4">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10 rounded-xl">
<AvatarFallback className="bg-primary text-primary-foreground"> <AvatarFallback className="bg-primary text-white font-black text-xs">
{getInitials((profile as any)?.email || 'U')} {getInitials((profile as any)?.email || 'U')}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-col space-y-0.5 flex-1 min-w-0"> <div className="flex flex-col min-w-0">
<p className="font-medium text-sm truncate"> <p className="font-black text-sm text-zinc-900 dark:text-white truncate">
{(profile as any)?.email?.split('@')[0]} {(profile as any)?.email?.split('@')[0]}
</p> </p>
<p className="text-xs text-muted-foreground truncate"> <p className="text-[9px] font-bold text-zinc-400 truncate uppercase tracking-widest">
{(profile as any)?.email} {(profile as any)?.email}
</p> </p>
</div> </div>
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator className="bg-zinc-100 dark:bg-zinc-800" />
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/account/profile" className="cursor-pointer"> <Link to="/account" className="cursor-pointer p-3 rounded-xl font-black uppercase tracking-widest text-[9px] gap-3">
<User className="mr-2 h-4 w-4" /> <History className="h-4 w-4 text-primary" />
Profilim Gezilerim
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/account" className="cursor-pointer"> <Link to="/account/preferences" className="cursor-pointer p-3 rounded-xl font-black uppercase tracking-widest text-[9px] gap-3">
<History className="mr-2 h-4 w-4" /> <Settings className="h-4 w-4 text-zinc-400" />
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 /> <DropdownMenuSeparator className="bg-zinc-100 dark:bg-zinc-800" />
<DropdownMenuItem <DropdownMenuItem
onClick={() => signOut()} onClick={() => signOut()}
className="text-destructive focus:text-destructive cursor-pointer" className="text-red-500 focus:text-red-500 cursor-pointer p-3 rounded-xl font-black uppercase tracking-widest text-[9px] gap-3"
> >
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="h-4 w-4" />
Çıkış Yap Çıkış Yap
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -160,23 +154,20 @@ export function Navbar() {
<Button <Button
onClick={() => navigate('/login')} onClick={() => navigate('/login')}
variant="outline" variant="outline"
className="hidden lg:flex h-11 px-6" className="hidden lg:flex h-9 px-5 rounded-xl border-zinc-200 dark:border-zinc-800 font-black uppercase tracking-widest text-[9px]"
> >
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

@ -0,0 +1,268 @@
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

@ -1,63 +1,169 @@
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 {
Loader2, ArrowRight, ArrowLeft, Sparkles, Hotel, Home, Navigation, Plane, Trees, Building2, Camera,
MapPin, Calendar, Users, Coffee, Heart, Bike, UtensilsCrossed, Users, Heart, Baby,
Car, Wallet, CheckCircle2, ChevronRight, Car, Bus, Shuffle, Gem, Wallet, CreditCard, Crown,
PersonStanding, PersonStanding, Calendar
} 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';
import { LOADING_STEPS, TRAVEL_TYPE_OPTIONS, BUDGET_OPTIONS, TRANSPORT_OPTIONS, ACCOMMODATION_OPTIONS, INTEREST_OPTIONS } from '@/constants/planner'; export const ACCOMMODATION_OPTIONS = [
import { DateSelector } from '@/components/planner/DateSelector'; {
import { TravelerInput } from '@/components/planner/TravelerInput'; id: 'hotel',
import { AccommodationSelector } from '@/components/planner/AccommodationSelector'; label: 'Otel',
import { InterestsGrid } from '@/components/planner/InterestsGrid'; description: 'Konforlu & servis odaklı',
import { TravelTypeSelector } from '@/components/planner/TravelTypeSelector'; icon: Hotel,
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;
// ─── Schema ─────────────────────────────────────────────────────────────────── export const TRAVEL_TYPE_OPTIONS = [
const formSchema = z.object({ {
dateRange: z.object({ id: 'solo',
from: z.date({ required_error: 'Başlangıç tarihi gereklidir' }), label: 'Solo',
to: z.date({ required_error: 'Bitiş tarihi gereklidir' }), description: 'Kendi tempomda özgür',
}) icon: PersonStanding,
.refine(d => d.from >= new Date(new Date().setHours(0, 0, 0, 0)), { gradient: 'from-violet-500 to-purple-600',
message: 'Başlangıç tarihi bugünden önce olamaz', bg: 'bg-violet-50',
}) border: 'border-violet-200',
.refine(d => d.to > d.from, { text: 'text-violet-600',
message: 'Bitiş tarihi başlangıç tarihinden sonra olmalıdır', },
}) {
.refine(d => { id: 'couple',
const days = differenceInDays(d.to, d.from) + 1; label: 'Çift',
return days >= 1 && days <= 14; description: 'Romantik & özel anlar',
}, { message: 'Seyahat süresi 114 gün arasında olmalıdır' }), icon: Heart,
travelType: z.string().min(1, 'Seyahat tipi seçiniz'), gradient: 'from-rose-500 to-pink-600',
travelers: z.number().min(1).max(15), bg: 'bg-rose-50',
accommodation: z.string(), border: 'border-rose-200',
transport: z.string().min(1, 'Ulaşım tercihi seçiniz'), text: 'text-rose-600',
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;
type FormValues = z.infer<typeof formSchema>; export const TRANSPORT_OPTIONS = [
{
id: 'rental',
label: 'Araç Kiralama',
description: 'Özgür & esnek rota',
icon: Car,
},
{
id: 'transfer',
label: 'Özel Transfer',
description: 'Konforlu & zahmetsiz',
icon: Gem,
},
{
id: 'shuttle',
label: 'Servis / Minibüs',
description: 'Ekonomik grup ulaşımı',
icon: Bus,
},
{
id: 'mixed',
label: 'Karma',
description: 'Duruma göre en iyisi',
icon: Shuffle,
},
] as const;
// ─── Steps ──────────────────────────────────────────────────────────────────── export const BUDGET_OPTIONS = [
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?' },
@ -65,465 +171,3 @@ 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

@ -0,0 +1,155 @@
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 } from 'lucide-react'; import { Loader2, Calendar, MapPin, Trash2, ChevronRight, PlusCircle, Zap, Compass as ExploreIcon, History, User } 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,23 +57,16 @@ export default function AccountPage() {
if (!user) { if (!user) {
return ( return (
<div className="min-h-screen flex items-center justify-center p-6 bg-secondary relative overflow-hidden"> <div className="min-h-screen flex items-center justify-center p-6 bg-zinc-50 dark:bg-black relative overflow-hidden">
<div className="absolute inset-0 z-0 opacity-10"> <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">
<img <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">
src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400" <User className="h-10 w-10 text-zinc-300" />
alt="bg"
className="w-full h-full object-cover grayscale"
/>
</div> </div>
<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"> <div className="space-y-3">
<div className="w-16 h-16 bg-primary/20 rounded-2xl flex items-center justify-center mx-auto border border-primary/40"> <h2 className="text-3xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase leading-none">HESABINIZ</h2>
<ExploreIcon className="h-8 w-8 text-primary" /> <p className="text-zinc-500 font-medium text-base">Gezilerinizi görüntülemek ve yönetmek için giriş yapın.</p>
</div> </div>
<div className="space-y-2"> <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>
<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>
@ -82,93 +75,91 @@ export default function AccountPage() {
} }
return ( return (
<div className="min-h-screen bg-background pt-20 pb-12"> <div className="min-h-screen bg-[#FDFDFD] dark:bg-black pt-24 pb-24">
<div className="max-w-4xl mx-auto px-6 space-y-10"> <div className="max-w-4xl mx-auto px-6 space-y-12">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 pb-10 border-b border-border"> <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="space-y-3"> <div className="space-y-4">
<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">Kişisel Arşiv</span> <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">
<Badge className="bg-primary/10 text-primary border-none font-black px-2 py-0.5 rounded-full uppercase text-[9px]"> {trips.length} KAYITLI ROTA
{trips.length} Rota
</Badge> </Badge>
</div> </div>
<h1 className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white tracking-tighter uppercase leading-none"> <h1 className="text-5xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase leading-none">
GEZİLERİM GEZİLERİM
</h1> </h1>
<p className="text-lg text-gray-500 font-medium italic">Planladığınız Kapadokya efsaneleri.</p> <p className="text-lg text-zinc-500 font-medium italic">Kişisel Kapadokya arşiviniz.</p>
</div> </div>
<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> <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>
<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>
</div> </header>
{loading ? ( {loading ? (
<div className="flex flex-col items-center justify-center py-20 gap-4"> <div className="flex flex-col items-center justify-center py-24 gap-6">
<Loader2 className="h-12 w-12 animate-spin text-primary opacity-40" /> <Loader2 className="h-10 w-10 animate-spin text-primary opacity-40" />
<p className="text-gray-400 font-black uppercase tracking-widest text-[9px]">Yükleniyor...</p> <p className="text-zinc-400 font-black uppercase tracking-widest text-[10px]">Veriler Getiriliyor...</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-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"> <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]">
<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"> <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">
<ExploreIcon className="h-8 w-8 text-gray-200" /> <History className="h-10 w-10 text-zinc-200 dark:text-zinc-700" />
</div> </div>
<div className="space-y-2"> <div className="space-y-3">
<h3 className="text-2xl font-black text-gray-900 dark:text-white tracking-tighter uppercase">Planınız yok</h3> <h3 className="text-3xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase">HENÜZ PLAN YOK</h3>
<p className="text-gray-500 font-medium italic text-sm max-w-xs mx-auto"> <p className="text-zinc-500 font-medium text-base max-w-xs mx-auto">
İlk Kapadokya rotanızı oluşturun. Henüz bir seyahat rotası oluşturmadınız.
</p> </p>
</div> </div>
<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> <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>
<Link to="/planner">Hemen Planla</Link> <Link to="/planner">Hemen Başlayın</Link>
</Button> </Button>
</Card> </Card>
</motion.div> </motion.div>
) : ( ) : (
<div className="grid gap-6"> <div className="grid gap-8">
<AnimatePresence> <AnimatePresence>
{trips.map((trip, idx) => ( {trips.map((trip, idx) => (
<motion.div <motion.div
key={trip.id} key={trip.id}
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.1 }} transition={{ delay: idx * 0.05 }}
> >
<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"> <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">
<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-56 h-48 sm:h-auto bg-gray-200 relative overflow-hidden shrink-0"> <div className="sm:w-64 h-56 sm:h-auto bg-zinc-100 dark:bg-zinc-900 relative overflow-hidden shrink-0">
<img <img
src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400" src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=800"
alt={trip.title} alt={trip.title}
className="w-full h-full object-cover group-hover:scale-105 transition-luxury duration-700" className="w-full h-full object-cover group-hover:scale-105 transition-all duration-700 grayscale-[0.3] group-hover:grayscale-0"
/> />
<div className="absolute top-4 left-4"> <div className="absolute top-5 left-5">
<Badge className="bg-primary text-white border-none font-black px-2 py-0.5 rounded-full uppercase text-[8px]"> <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">
<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-6 flex flex-col justify-between"> <div className="flex-1 p-8 flex flex-col justify-between">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-6">
<div className="space-y-1"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[9px] font-black text-primary uppercase tracking-widest">{trip.destination}</span> <span className="text-[10px] font-black text-primary uppercase tracking-widest">{trip.destination}</span>
</div> </div>
<h3 className="text-2xl font-black text-gray-900 dark:text-white tracking-tighter uppercase leading-none group-hover:text-primary transition-colors"> <h3 className="text-3xl font-black text-zinc-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-1"> <div className="flex items-center gap-4 pt-2">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-gray-500 italic"> <div className="flex items-center gap-1.5 text-[11px] font-bold text-zinc-400 italic">
<Calendar className="h-3.5 w-3.5 text-primary" /> <Calendar className="h-4 w-4 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>
@ -176,34 +167,34 @@ export default function AccountPage() {
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant="ghost" size="icon" className="h-10 w-10 rounded-xl text-gray-300 hover:text-red-600"> <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">
<Trash2 className="h-5 w-5" /> <Trash2 className="h-5 w-5" />
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="rounded-3xl p-8 bg-secondary border-white/10"> <AlertDialogContent className="rounded-[2.5rem] p-10 bg-white dark:bg-zinc-900 border-zinc-100 dark:border-zinc-800 shadow-2xl">
<AlertDialogHeader className="space-y-3"> <AlertDialogHeader className="space-y-4 text-center">
<AlertDialogTitle className="text-2xl font-black text-white tracking-tighter uppercase">Planı Sil?</AlertDialogTitle> <AlertDialogTitle className="text-3xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase">GEZİYİ SİL?</AlertDialogTitle>
<AlertDialogDescription className="text-white/60 font-medium italic text-base"> <AlertDialogDescription className="text-zinc-500 font-medium text-base">
Silmek istediğinize emin misiniz? Bu rotayı kalıcı olarak silmek istediğinize emin misiniz? Bu işlem geri alınamaz.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter className="pt-6"> <AlertDialogFooter className="pt-8 flex sm:justify-center gap-4">
<AlertDialogCancel className="h-12 px-8 rounded-xl font-bold bg-white/5 text-white border-white/10">Hayır</AlertDialogCancel> <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>
<AlertDialogAction <AlertDialogAction
onClick={() => handleDelete(trip.id)} onClick={() => handleDelete(trip.id)}
className="h-12 px-8 rounded-xl font-black bg-red-600 hover:bg-red-700 text-white" 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"
> >
Evet, Sil Silmeyi Onayla
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
<div className="mt-8 flex items-center justify-between border-t border-gray-50 dark:border-white/5 pt-6"> <div className="mt-10 flex items-center justify-between border-t border-zinc-100 dark:border-zinc-900 pt-8">
<div className="flex -space-x-2"> <div className="flex -space-x-3">
{trip.itinerary.days[0].items.slice(0, 3).map((item, i) => ( {trip.itinerary.days[0].items.slice(0, 4).map((item, i) => (
<div key={i} className="w-10 h-10 rounded-xl border-2 border-white dark:border-secondary bg-gray-100 overflow-hidden shadow-md"> <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">
<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=""
@ -211,10 +202,15 @@ 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,8 +3,9 @@ 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 } from 'lucide-react'; import { Search, Star, Clock, Filter, Sparkles, Heart, Share2, ArrowUpRight, Camera, MapPin, ChevronRight } 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ü' },
@ -23,7 +24,7 @@ const PLACES = [
rating: 4.8, rating: 4.8,
reviews: 12400, reviews: 12400,
duration: '2-3 Saat', duration: '2-3 Saat',
image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_2736e413-5912-4327-880c-f8494d625176.jpg', image: 'https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=800',
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.',
}, },
{ {
@ -33,7 +34,7 @@ const PLACES = [
rating: 4.7, rating: 4.7,
reviews: 8500, reviews: 8500,
duration: '1 Saat', duration: '1 Saat',
image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_8ea8dda5-57a3-4533-bd11-28440db86c34.jpg', image: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&q=80&w=800',
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.',
}, },
{ {
@ -43,7 +44,7 @@ const PLACES = [
rating: 4.9, rating: 4.9,
reviews: 15200, reviews: 15200,
duration: '1.5 Saat', duration: '1.5 Saat',
image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_9c62deaa-2f09-44b8-a9e4-1d03ae155cce.jpg', image: 'https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=800',
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ı.',
}, },
{ {
@ -53,7 +54,7 @@ const PLACES = [
rating: 4.6, rating: 4.6,
reviews: 6300, reviews: 6300,
duration: '4-5 Saat', duration: '4-5 Saat',
image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_a0a63c27-9e25-4f1a-89ef-aea901ec645d.jpg', image: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&q=80&w=800',
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.',
}, },
{ {
@ -63,7 +64,7 @@ const PLACES = [
rating: 4.8, rating: 4.8,
reviews: 9100, reviews: 9100,
duration: '1 Saat', duration: '1 Saat',
image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_8cfa89df-0b41-4cb0-9e72-f7c2e4ebda81.jpg', image: 'https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=800',
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.',
}, },
{ {
@ -73,7 +74,7 @@ const PLACES = [
rating: 4.5, rating: 4.5,
reviews: 4200, reviews: 4200,
duration: '1-2 Saat', duration: '1-2 Saat',
image: 'https://miaoda-site-img.s3cdn.medo.dev/images/KLing_dd8b8233-fb7c-4dc3-90fe-eb24b5e136ee.jpg', image: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&q=80&w=800',
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.',
} }
]; ];
@ -90,69 +91,58 @@ export default function ExplorePage() {
}); });
return ( return (
<div className="min-h-screen bg-background selection:bg-primary/20 pb-20"> <div className="min-h-screen bg-[#FDFDFD] dark:bg-black selection:bg-primary/20 pb-24">
{/* Immersive Header */} {/* Minimalist Header */}
<section className="relative h-[40vh] flex items-center justify-center overflow-hidden mb-12"> <section className="pt-24 pb-16 border-b border-zinc-100 dark:border-zinc-900 bg-white dark:bg-zinc-950">
<div className="absolute inset-0 z-0 scale-105"> <div className="container px-6">
<img <div className="max-w-4xl mx-auto space-y-10">
src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400" <div className="space-y-4 text-center md:text-left">
alt="Explore Hero"
className="w-full h-full object-cover grayscale-[0.2]"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/20 to-transparent z-10" />
</div>
<div className="container relative z-20 px-6 text-center space-y-6">
<motion.div <motion.div
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }} 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" 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"
> >
<Sparkles className="h-3.5 w-3.5 text-accent" /> <MapPin className="h-3.5 w-3.5 text-primary" />
Efsanevi Duraklar KEŞİF REHBERİ
</motion.div> </motion.div>
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }} transition={{ delay: 0.1 }}
className="text-4xl md:text-6xl font-black text-white tracking-tighter leading-none uppercase" className="text-4xl md:text-6xl font-black text-zinc-900 dark:text-white tracking-tighter leading-tight uppercase"
> >
KAPADOKYA <span className="text-primary uppercase">KEŞFİ</span> Kapadokya'yı <br /> <span className="text-primary italic">Keşfedin</span>
</motion.h1> </motion.h1>
</div>
<motion.div <motion.div
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }} transition={{ delay: 0.2 }}
className="max-w-xl mx-auto relative group" className="relative group max-w-2xl"
> >
<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" /> <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" />
<Input <Input
placeholder="Bir efsane veya aktivite arayın..." placeholder="Mekan veya aktivite arayın..."
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" 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"
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"> <div className="container px-6 pt-12">
{/* Categories Carousel */} {/* Categories Carousel */}
<div className="flex items-center gap-3 overflow-x-auto pb-6 mb-12 no-scrollbar"> <div className="flex items-center gap-3 overflow-x-auto pb-8 mb-4 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-11 px-8 rounded-xl font-black uppercase tracking-widest text-[10px] shrink-0 transition-luxury ${selectedCategory === 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
? 'bg-primary text-white shadow-lg shadow-primary/20' ? 'bg-zinc-900 text-white shadow-md'
: 'bg-white/5 border-2 border-gray-100 dark:border-white/10 text-gray-400 hover:border-primary/40 hover:text-primary' : 'bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 text-zinc-400 hover:border-zinc-300'
}`} }`}
> >
{cat.label} {cat.label}
@ -168,77 +158,61 @@ 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-8" className="grid md:grid-cols-2 lg:grid-cols-3 gap-10"
> >
{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.1 }} transition={{ delay: index * 0.05 }}
> >
<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"> <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">
<CardContent className="p-0"> <CardContent className="p-0">
<div className="relative h-64 overflow-hidden"> <div className="relative h-72 overflow-hidden">
<img <img
src={place.image} src={place.image}
alt={place.name} alt={place.name}
className="w-full h-full object-cover transition-luxury duration-700 group-hover:scale-105" className="w-full h-full object-cover transition-all duration-700 group-hover:scale-105"
/> />
<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 inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-all" />
<div className="absolute top-4 left-4"> <div className="absolute top-5 left-5">
<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"> <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">
{CATEGORIES.find(c => c.id === place.category)?.label} {CATEGORIES.find(c => c.id === place.category)?.label}
</Badge> </Badge>
</div> </div>
<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"> <div className="absolute top-5 right-5">
<div className="flex items-center gap-2"> <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="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"> <Star className="h-3 w-3 fill-white" />
<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-6 md:p-8 space-y-4"> <div className="p-8 space-y-4">
<div className="space-y-1"> <div className="space-y-2">
<h3 className="text-2xl font-black text-gray-900 dark:text-white tracking-tighter uppercase leading-tight group-hover:text-primary transition-colors"> <h3 className="text-2xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase leading-tight group-hover:text-primary transition-colors">
{place.name} {place.name}
</h3> </h3>
<p className="text-gray-500 font-medium italic leading-relaxed text-sm line-clamp-2"> <p className="text-zinc-500 font-medium 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-gray-50 dark:border-white/5"> <div className="pt-6 flex items-center justify-between border-t border-zinc-100 dark:border-zinc-900">
<div className="flex flex-wrap items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-1.5 text-[9px] font-black text-gray-400 uppercase tracking-widest"> <div className="flex items-center gap-1.5 text-[10px] font-black text-zinc-400 uppercase tracking-widest">
<Clock className="h-3.5 w-3.5 text-primary" /> <Clock className="h-4 w-4 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>
</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>
<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"> <Link to="/planner">
<ArrowUpRight className="h-5 w-5 group-hover/btn:rotate-45 transition-transform" /> <ChevronRight className="h-5 w-5 group-hover/btn:translate-x-0.5 transition-transform" />
</button> </Link>
</Button>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -251,24 +225,17 @@ 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-20 text-center space-y-6" className="py-24 text-center space-y-6"
> >
<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"> <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">
<Search className="h-10 w-10 text-gray-200" /> <Search className="h-8 w-8 text-zinc-200" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-2xl font-black text-gray-900 dark:text-white tracking-tighter uppercase">BULUNAMADI</h3> <h3 className="text-2xl font-black text-zinc-900 dark:text-white tracking-tighter uppercase">SONUÇ YOK</h3>
<p className="text-base text-gray-500 font-medium italic max-w-xs mx-auto"> <p className="text-sm text-zinc-500 font-medium max-w-xs mx-auto">
Farklı bir keşif terimi deneyin. Aramanıza uygun mekan bulunamadı.
</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,8 +1,7 @@
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, ArrowUpRight, Play, Globe } from 'lucide-react'; import { Sparkles, MapPin, Calendar, Compass, ShieldCheck, Zap, ArrowRight, Star, Globe, ChevronRight } from 'lucide-react';
import { motion, useScroll, useTransform } from 'framer-motion'; import { motion } 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
@ -10,222 +9,190 @@ 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-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" 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"
> >
<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"> <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">
<Icon className="h-6 w-6 text-primary" /> <Icon className="h-6 w-6 text-zinc-600 dark:text-zinc-400 group-hover:text-primary transition-colors" />
</div> </div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3 tracking-tight">{title}</h3> <h3 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-3 tracking-tight">{title}</h3>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed text-sm font-medium">{description}</p> <p className="text-zinc-500 dark:text-zinc-400 leading-relaxed text-sm">{description}</p>
</motion.div> </motion.div>
); );
export default function LandingPage() { export default function LandingPage() {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start start", "end end"]
});
const heroY = useTransform(scrollYProgress, [0, 1], [0, 200]);
const heroOpacity = useTransform(scrollYProgress, [0, 0.2], [1, 0]);
return ( return (
<div className="min-h-screen bg-background selection:bg-primary/20" ref={containerRef}> <div className="min-h-screen bg-[#FDFDFD] dark:bg-black selection:bg-primary/20">
{/* Hero Section */} {/* Hero Section */}
<section className="relative h-[90vh] flex items-center justify-center overflow-hidden"> <section className="relative pt-20 pb-20 md:pt-32 md:pb-32 overflow-hidden">
{/* Cinematic Background */} {/* Subtle background patterns */}
<motion.div <div className="absolute inset-0 z-0 opacity-[0.03] dark:opacity-[0.05] pointer-events-none">
style={{ y: heroY, opacity: heroOpacity }} <svg className="h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none">
className="absolute inset-0 z-0" <defs>
> <pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<div className="absolute inset-0 bg-black/40 z-10" /> <path d="M 10 0 L 0 0 0 10" fill="none" stroke="currentColor" strokeWidth="0.5" />
<img </pattern>
src="https://images.unsplash.com/photo-1541167760496-1628856ab772?auto=format&fit=crop&q=80&w=2400" </defs>
alt="Cappadocia" <rect width="100%" height="100%" fill="url(#grid)" />
className="w-full h-full object-cover scale-105" </svg>
/> </div>
</motion.div>
{/* Hero Content */} <div className="container relative z-10 px-6">
<div className="container relative z-20 px-6 text-center"> <div className="max-w-4xl mx-auto text-center space-y-8">
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }} transition={{ duration: 0.5 }}
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" 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-accent" /> <Sparkles className="h-3.5 w-3.5 text-primary" />
Yapay Zeka Destekli Premium Deneyim Yapay Zeka Destekli Seyahat Rehberiniz
</motion.div> </motion.div>
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 20 }}
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-white mb-8 leading-[0.95] tracking-tighter" className="text-5xl md:text-7xl lg:text-8xl font-black text-zinc-900 dark:text-white leading-[1.1] tracking-tighter"
> >
KAPADOKYA <br /> Kapadokya'yı <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary via-accent to-primary animate-gradient"> <span className="text-primary italic">Yeniden</span> Keşfedin
EFSANESİ
</span>
</motion.h1> </motion.h1>
<motion.p <motion.p
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 20 }}
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-xl text-white/80 max-w-2xl mx-auto mb-12 leading-relaxed font-medium italic" className="text-lg md:text-2xl text-zinc-500 dark:text-zinc-400 max-w-2xl mx-auto leading-relaxed font-medium"
> >
"Sıradan bir gezi değil, ruhunuza dokunacak bir keşif hikayesi. Saniyeler içinde size özel kurgulanmış premium rotalar." Kişiselleştirilmiş rotalar, anlık öneriler ve kusursuz bir planlama deneyimi. Yapay zeka ile her detayı düşünülmüş bir Kapadokya efsanesi.
</motion.p> </motion.p>
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 20 }}
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" className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4"
> >
<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> <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>
<Link to="/planner"> <Link to="/planner">
Hemen Keşfet Hemen Başlayın
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" /> <ArrowRight className="ml-2 h-5 w-5" />
</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>
{/* Stats Section */} {/* Trust Stats */}
<section className="py-16 bg-secondary text-white relative overflow-hidden"> <section className="py-12 border-y border-zinc-100 dark:border-zinc-900 bg-white dark:bg-zinc-950">
<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="container px-6">
<div className="container relative z-10 px-6"> <div className="flex flex-wrap justify-center items-center gap-8 md:gap-16 opacity-50 grayscale contrast-125">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 text-center"> <div className="flex items-center gap-2">
{[ <Star className="h-5 w-5 fill-zinc-900 dark:fill-white" />
{ label: "Mutlu Gezgin", value: "25k+", icon: Globe }, <span className="font-bold tracking-tighter text-lg">TOP DESTINATION 2026</span>
{ label: "Kişiye Özel Rota", value: "100k+", icon: Compass }, </div>
{ label: "Doğrulanmış Mekan", value: "1.2k", icon: MapPin }, <div className="flex items-center gap-2">
{ label: "Müşteri Puanı", value: "4.9", icon: Star }, <Globe className="h-5 w-5" />
].map((stat, i) => ( <span className="font-bold tracking-tighter text-lg">GLOBAL REACH</span>
<motion.div </div>
key={i} <div className="flex items-center gap-2">
initial={{ opacity: 0, scale: 0.5 }} <ShieldCheck className="h-5 w-5" />
whileInView={{ opacity: 1, scale: 1 }} <span className="font-bold tracking-tighter text-lg">SECURE BOOKING</span>
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-20 bg-background relative overflow-hidden"> <section className="py-24 bg-[#FDFDFD] dark:bg-black">
<div className="container px-6"> <div className="container px-6">
<div className="max-w-2xl mx-auto text-center mb-16 space-y-4"> <div className="max-w-2xl mb-20 space-y-4">
<motion.span <motion.span
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }} whileInView={{ opacity: 1 }}
className="text-primary font-black tracking-widest uppercase text-xs" className="text-primary font-bold tracking-widest uppercase text-xs"
> >
Kusursuz Mühendislik Neden Biz?
</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-3xl md:text-5xl font-black text-gray-900 dark:text-white leading-tight" className="text-4xl md:text-5xl font-black text-zinc-900 dark:text-white leading-tight tracking-tighter"
> >
Seyahatinizi Sanata <br /> <span className="text-gradient">Dönüştürüyoruz</span> Seyahat Planlamanın <br /> Geleceği Burada
</motion.h2> </motion.h2>
</div> </div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<FeatureCard <FeatureCard
index={0} index={0}
icon={Zap} icon={Zap}
title="Yapay Zeka Mimari" title="Yapay Zeka Motoru"
description="OpenAI 4o-mini altyapısıyla her detayı düşünülmüş, tutarlı ve akıcı bir seyahat programı." description="Tercihlerinize göre saniyeler içinde binlerce olasılığı tarayan akıllı seyahat motoru."
/> />
<FeatureCard <FeatureCard
index={1} index={1}
icon={ShieldCheck} icon={MapPin}
title="Premium Veri" title="Gerçek Zamanlı Veri"
description="Google Places verileriyle gerçek fotoğraflar, anlık çalışma saatleri ve güvenilir yorumlar." description="Google Places entegrasyonu ile her zaman güncel çalışma saatleri ve güvenilir fotoğraflar."
/> />
<FeatureCard <FeatureCard
index={2} index={2}
icon={MapPin} icon={Compass}
title="Akıllı Navigasyon" title="Yerel Kürasyon"
description="Sadece rota değil, Google Directions ile en mantıklı sıralama ve ulaşım süreleri." description="Sadece turistik yerler değil, yerel uzmanlarımızın onayladığı gizli cevherler."
/> />
<FeatureCard <FeatureCard
index={3} index={3}
icon={Compass} icon={Calendar}
title="Kürasyon" title="Zahmetsiz Yönetim"
description="Yerel rehberlerin gizli favorileri ve turistik kalabalıkların ötesindeki özel duraklar." description="Sürükle bırak özelliği ile planınızı anında revize edin, rota otomatik güncellensin."
/> />
<FeatureCard <FeatureCard
index={4} index={4}
icon={Calendar} icon={ShieldCheck}
title="Dinamik Kontrol" title="Güvenilir Operatörler"
description="Planınızı dilediğiniz zaman sürükle-bırak yöntemiyle revize edin, her an güncel kalın." description="Bölgenin en iyi balon ve aktivite operatörleri ile doğrudan iletişim ve güvenli rezervasyon."
/> />
<FeatureCard <FeatureCard
index={5} index={5}
icon={Sparkles} icon={Sparkles}
title="Modern Estetik" title="Kusursuz Arayüz"
description="En yüksek standartlarda kullanıcı deneyimi için tasarlanmış akıcı ve şık arayüz." description="Karmaşadan uzak, sadece size ve seyahatinize odaklanan minimalist tasarım."
/> />
</div> </div>
</div> </div>
</section> </section>
{/* Luxury CTA */} {/* Modern CTA */}
<section className="py-20 px-6"> <section className="py-24 px-6">
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, scale: 0.98 }}
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
className="max-w-6xl mx-auto rounded-[2.5rem] bg-secondary p-12 lg:p-20 text-center relative overflow-hidden group" 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"
> >
<div className="absolute inset-0 opacity-20 grayscale hover:grayscale-0 transition-luxury duration-1000 group-hover:scale-105"> {/* Subtle noise/texture overlay */}
<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 className="absolute inset-0 opacity-[0.05] pointer-events-none bg-[url('https://www.transparenttextures.com/patterns/asfalt-dark.png')]" />
</div>
<div className="relative z-10 space-y-8"> <div className="relative z-10 space-y-10">
<h2 className="text-4xl lg:text-7xl font-black text-white leading-[0.95] tracking-tighter uppercase"> <h2 className="text-4xl lg:text-7xl font-black text-white leading-[1.1] tracking-tighter">
BİR SONRAKİ <br /> <span className="text-primary">EFSANENİZİ</span> YAZIN BÜYÜLÜ BİR GEZİ <br /> SİZİ BEKLİYOR
</h2> </h2>
<p className="text-white/60 text-lg md:text-xl max-w-xl mx-auto font-medium leading-relaxed"> <p className="text-zinc-400 text-lg md:text-xl max-w-xl mx-auto font-medium leading-relaxed">
Kapadokya'nın zamansız ruhunu, modern teknolojinin gücüyle birleştirin. Bugün başlayın. Kapadokya'nın masalsı dokusunu modern teknolojinin kolaylığıyla keşfedin. Planlamaya 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-10 text-xl font-bold bg-primary hover:bg-primary-dark rounded-2xl shadow-2xl shadow-primary/20 transition-luxury group" asChild> <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>
<Link to="/planner" className="flex items-center gap-3"> <Link to="/planner" className="flex items-center gap-3">
Rotanı Oluştur Ücretsiz Başlayın
<ArrowUpRight className="h-6 w-6 group-hover:rotate-45 transition-transform" /> <ChevronRight className="h-6 w-6 group-hover:translate-x-1 transition-transform" />
</Link> </Link>
</Button> </Button>
</div> </div>
@ -233,32 +200,54 @@ export default function LandingPage() {
</motion.div> </motion.div>
</section> </section>
{/* Modern Footer */} {/* Simple Footer */}
<footer className="py-16 border-t border-border bg-background"> <footer className="py-20 border-t border-zinc-100 dark:border-zinc-900 bg-white dark:bg-black">
<div className="container px-6"> <div className="container px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-8"> <div className="flex flex-col md:flex-row justify-between items-start gap-12">
<div className="space-y-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary rounded-xl flex items-center justify-center text-white"> <MapPin className="h-6 w-6 text-primary" />
<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="flex items-center gap-8 text-[10px] font-bold text-gray-400 uppercase tracking-widest"> <div className="grid grid-cols-2 md:grid-cols-3 gap-12">
<Link to="/explore" className="hover:text-primary transition-colors">Keşfet</Link> <div className="space-y-4">
<Link to="/planner" className="hover:text-primary transition-colors">Planla</Link> <h4 className="text-xs font-black uppercase tracking-widest text-zinc-900 dark:text-white">Ürün</h4>
<Link to="/account" className="hover:text-primary transition-colors">Hesabım</Link> <ul className="space-y-2 text-sm text-zinc-500">
<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-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"> <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">
<p>© 2026 Cappadocia Legend. Her anı bir hikaye.</p> <p>© 2026 Kapadokya Efsanesi. Tripizy esintisiyle.</p>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<span>TR</span> <span className="flex items-center gap-1">
<div className="h-3 w-px bg-border" /> Made with <span className="text-red-500"></span> in Cappadocia
<span>USD</span> </span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,45 @@
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

@ -0,0 +1,98 @@
-- 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

@ -0,0 +1,10 @@
-- 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

@ -0,0 +1,6 @@
-- 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()
);