first commit

This commit is contained in:
2026-06-16 13:04:11 +03:00
commit 7ecdb30afc
54 changed files with 15479 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
'use client'
import { motion } from 'framer-motion'
import Link from 'next/link'
import Image from 'next/image'
import { ArrowRight, Star } from 'lucide-react'
import { ShimmerButton } from '@/components/magicui/shimmer-button'
import { cn } from '@/components/common/Navbar'
const EASE = [0.22, 1, 0.36, 1] as const
const fadeInUp = {
hidden: { opacity: 0, y: 40, filter: 'blur(8px)' },
show: { opacity: 1, y: 0, filter: 'blur(0px)', transition: { duration: 0.8, ease: EASE } }
}
export function HomePageClient({
dict,
tours,
categories,
reviews
}: {
dict: any,
tours: any[],
categories: any[],
reviews: any[]
}) {
return (
<div className="bg-background min-h-screen">
{/* Hero Section */}
<section className="relative h-screen min-h-[700px] flex items-center justify-center overflow-hidden">
<motion.div
initial={{ scale: 1.1, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 1.5, ease: EASE }}
className="absolute inset-0"
>
<Image
src="https://images.unsplash.com/photo-1471922694854-ff1b63b20054?auto=format&fit=crop&q=80"
alt="Fethiye Hero"
fill
className="object-cover"
priority
sizes="100vw"
/>
<div className="absolute inset-0 bg-gradient-to-b from-background/40 via-background/20 to-background/90" />
</motion.div>
<div className="relative z-10 text-center px-4 max-w-5xl mx-auto flex flex-col items-center">
<motion.div
initial="hidden"
animate="show"
variants={{
hidden: { opacity: 0 },
show: { opacity: 1, transition: { staggerChildren: 0.2 } }
}}
>
<motion.h1
variants={fadeInUp}
className="font-heading text-hero text-foreground mb-6 leading-none drop-shadow-2xl"
>
{dict.hTitle}
</motion.h1>
<motion.p
variants={fadeInUp}
className="text-h3 text-foreground/80 mb-12 max-w-2xl mx-auto font-light tracking-wide"
>
{dict.hSubtitle}
</motion.p>
<motion.div variants={fadeInUp}>
<Link href="/tours">
<ShimmerButton
className="shadow-2xl"
shimmerColor="var(--primary)"
background="var(--background)"
borderRadius="10px"
>
<span className="font-bold tracking-widest uppercase text-sm text-foreground flex items-center gap-2">
{dict.hCta} <ArrowRight className="w-4 h-4 text-primary" />
</span>
</ShimmerButton>
</Link>
</motion.div>
</motion.div>
</div>
</section>
{/* Popular Tours Section */}
<section className="py-32 relative z-20">
<div className="container mx-auto px-6">
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.8, ease: EASE }}
className="flex flex-col md:flex-row justify-between items-end mb-16 gap-6"
>
<h2 className="font-heading text-h2 text-foreground max-w-md leading-tight">
{dict.sPopular}
</h2>
<Link href="/tours" className="group flex items-center gap-2 text-sm font-bold tracking-widest uppercase text-foreground hover:text-primary transition-colors">
{dict.sViewAll}
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</Link>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{tours.slice(0, 4).map((tour, i) => (
<motion.div
key={tour.id}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.8, ease: EASE, delay: i * 0.1 }}
className="group relative flex flex-col gap-4"
>
<Link href={`/tours/${tour.slug}`} className="relative h-[400px] overflow-hidden rounded-2xl">
<Image
src={tour.imageUrl || ''}
alt={tour.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw"
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
/>
<div className="absolute inset-0 bg-black/20 group-hover:bg-transparent transition-colors duration-500" />
</Link>
<div className="flex flex-col gap-2">
<h3 className="font-heading text-2xl font-bold text-foreground group-hover:text-primary transition-colors">{tour.title}</h3>
<div className="flex items-center justify-between">
<span className="text-xs tracking-widest uppercase text-muted-foreground">{dict.tFrom}</span>
<span className="font-sans font-bold text-xl text-primary">${tour.price}</span>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
{/* Editorial Categories */}
<section className="py-32 bg-card/30 border-y border-border/30">
<div className="container mx-auto px-6">
<motion.h2
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="font-heading text-h2 text-center text-foreground mb-20"
>
{dict.sCategories}
</motion.h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{categories.slice(0, 3).map((cat, i) => (
<motion.div
key={cat.id}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: EASE, delay: i * 0.1 }}
>
<Link href={`/tours?category=${cat.slug}`} className="group block relative h-[500px] overflow-hidden rounded-2xl">
<Image
src={cat.imageUrl || ''}
alt={cat.name}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background/90 via-background/20 to-transparent" />
<div className="absolute bottom-0 left-0 p-8 w-full flex items-end justify-between">
<h3 className="font-heading text-3xl font-bold text-foreground">{cat.name}</h3>
<div className="w-10 h-10 rounded-full bg-primary/20 backdrop-blur-md flex items-center justify-center group-hover:bg-primary transition-colors">
<ArrowRight className="w-5 h-5 text-foreground" />
</div>
</div>
</Link>
</motion.div>
))}
</div>
</div>
</section>
{/* Reviews Section */}
<section className="py-32">
<div className="container mx-auto px-6">
<h2 className="font-heading text-h2 text-center text-foreground mb-20">{dict.sReviews}</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{reviews.map((review, i) => (
<motion.div
key={review.id}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: EASE, delay: i * 0.1 }}
className="bg-card/40 p-8 rounded-3xl border border-border/50 hover:border-primary/30 transition-colors"
>
<div className="flex text-primary mb-6">
{[...Array(review.rating)].map((_, j) => <Star key={j} className="w-4 h-4 fill-current mr-1" />)}
</div>
<p className="text-foreground/80 font-serif italic text-lg leading-relaxed mb-8">"{review.comment}"</p>
<div>
<div className="font-bold text-foreground text-sm tracking-widest uppercase">{review.authorName}</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
</div>
)
}
+97
View File
@@ -0,0 +1,97 @@
'use client'
import { signOut } from 'next-auth/react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { LayoutDashboard, Users, Settings, LogOut, Menu, X } from 'lucide-react'
import { useState } from 'react'
export default function AdminLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const [sidebarOpen, setSidebarOpen] = useState(false)
const navigation = [
{ name: 'Dashboard', href: '/admin', icon: LayoutDashboard },
{ name: 'Kullanıcılar', href: '/admin/users', icon: Users },
{ name: 'Ayarlar', href: '/admin/settings', icon: Settings },
]
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex">
{/* Mobile sidebar backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-gray-900/80 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<div className={`
fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-gray-950 border-r border-gray-200 dark:border-gray-800
transform transition-transform duration-200 ease-in-out lg:translate-x-0 lg:static lg:inset-0
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
`}>
<div className="h-full flex flex-col">
<div className="h-16 flex items-center px-6 border-b border-gray-200 dark:border-gray-800">
<h1 className="text-lg font-bold text-gray-900 dark:text-white">Admin Paneli</h1>
<button
className="ml-auto lg:hidden text-gray-500"
onClick={() => setSidebarOpen(false)}
>
<X className="h-5 w-5" />
</button>
</div>
<nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
{navigation.map((item) => {
const isActive = pathname.endsWith(item.href)
return (
<Link
key={item.name}
href={item.href}
className={`
flex items-center px-3 py-2.5 text-sm font-medium rounded-md transition-colors
${isActive
? 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white'}
`}
>
<item.icon className={`mr-3 flex-shrink-0 h-5 w-5 ${isActive ? 'text-gray-900 dark:text-white' : 'text-gray-400'}`} />
{item.name}
</Link>
)
})}
</nav>
<div className="p-4 border-t border-gray-200 dark:border-gray-800">
<button
onClick={() => signOut({ callbackUrl: '/' })}
className="flex w-full items-center px-3 py-2.5 text-sm font-medium text-red-600 dark:text-red-400 rounded-md hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors"
>
<LogOut className="mr-3 h-5 w-5" />
Çıkış Yap
</button>
</div>
</div>
</div>
{/* Main content */}
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<header className="h-16 flex items-center lg:hidden bg-white dark:bg-gray-950 border-b border-gray-200 dark:border-gray-800 px-4">
<button
onClick={() => setSidebarOpen(true)}
className="text-gray-500 hover:text-gray-900 dark:hover:text-white focus:outline-none"
>
<Menu className="h-6 w-6" />
</button>
<span className="ml-4 text-lg font-bold text-gray-900 dark:text-white">Admin Paneli</span>
</header>
<main className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
{children}
</main>
</div>
</div>
)
}
+47
View File
@@ -0,0 +1,47 @@
import { auth } from '@/lib/auth'
export default async function AdminDashboardPage() {
const session = await auth()
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Dashboard</h2>
<p className="text-gray-500 dark:text-gray-400 mt-2">
Hoş geldiniz, {session?.user?.name || session?.user?.email}. İşte projenizin genel görünümü.
</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Placeholder Stat Cards */}
{[
{ name: 'Toplam Kullanıcı', stat: '1,245' },
{ name: 'Aktif Oturumlar', stat: '42' },
{ name: 'Yeni Kayıtlar', stat: '8' },
{ name: 'Sistem Durumu', stat: 'Online' },
].map((item) => (
<div
key={item.name}
className="overflow-hidden rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 px-4 py-5 shadow-sm sm:p-6"
>
<dt className="truncate text-sm font-medium text-gray-500 dark:text-gray-400">{item.name}</dt>
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{item.stat}
</dd>
</div>
))}
</div>
<div className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm">
<div className="p-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Son Aktiviteler</h3>
<div className="mt-4 border-t border-gray-100 dark:border-gray-800">
<div className="py-4 text-sm text-gray-500">
Henüz aktivite kaydı bulunmuyor.
</div>
</div>
</div>
</div>
</div>
)
}
+77
View File
@@ -0,0 +1,77 @@
import { setRequestLocale } from 'next-intl/server'
import { Mail, MapPin, Phone } from 'lucide-react'
export default async function ContactPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
return (
<div className="bg-zinc-950 min-h-screen py-16">
<div className="container mx-auto px-4 max-w-5xl">
<h1 className="text-4xl font-black mb-12 text-center text-white uppercase tracking-tighter">Contact Us</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
{/* Contact Info */}
<div className="space-y-8">
<h2 className="text-2xl font-bold text-white">Get In Touch</h2>
<p className="text-zinc-300">
Pioneer travel is here to help you with all your trips and excursions. Fill out the form or contact us directly using the information below.
</p>
<div className="space-y-6">
<div className="flex items-start">
<div className="w-12 h-12 bg-orange-500/10 rounded-full flex items-center justify-center text-orange-500 shrink-0 mr-4">
<MapPin className="w-6 h-6" />
</div>
<div>
<h4 className="font-bold text-white mb-1">Office Address</h4>
<p className="text-zinc-400">Çarşı Caddesi Tonoz İş Merkezi No:3/1<br/>Ölüdeniz - Fethiye / Türkiye</p>
</div>
</div>
<div className="flex items-start">
<div className="w-12 h-12 bg-orange-500/10 rounded-full flex items-center justify-center text-orange-500 shrink-0 mr-4">
<Phone className="w-6 h-6" />
</div>
<div>
<h4 className="font-bold text-white mb-1">Phone Number</h4>
<p className="text-zinc-400">+90 530 378 48 82</p>
</div>
</div>
<div className="flex items-start">
<div className="w-12 h-12 bg-orange-500/10 rounded-full flex items-center justify-center text-orange-500 shrink-0 mr-4">
<Mail className="w-6 h-6" />
</div>
<div>
<h4 className="font-bold text-white mb-1">Email Address</h4>
<p className="text-zinc-400">info@fethiyeholiday.com</p>
</div>
</div>
</div>
</div>
{/* Contact Form */}
<div className="bg-zinc-900 p-8 rounded-2xl shadow-xl border border-zinc-800">
<h2 className="text-2xl font-bold text-white mb-6">Send a Message</h2>
<form className="space-y-4">
<div>
<label className="block text-sm font-medium text-zinc-300 mb-1">Your Name</label>
<input type="text" className="w-full px-4 py-2 border border-zinc-700 rounded-md bg-zinc-950/50 text-white focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none transition-colors" placeholder="John Doe" />
</div>
<div>
<label className="block text-sm font-medium text-zinc-300 mb-1">Your Email</label>
<input type="email" className="w-full px-4 py-2 border border-zinc-700 rounded-md bg-zinc-950/50 text-white focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none transition-colors" placeholder="john@example.com" />
</div>
<div>
<label className="block text-sm font-medium text-zinc-300 mb-1">Message</label>
<textarea rows={4} className="w-full px-4 py-2 border border-zinc-700 rounded-md bg-zinc-950/50 text-white focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none transition-colors" placeholder="How can we help you?"></textarea>
</div>
<button type="button" className="w-full py-3 bg-orange-500 text-white font-bold rounded-md hover:bg-orange-600 transition-colors shadow-[0_0_15px_rgba(249,115,22,0.3)]">
Send Message
</button>
</form>
</div>
</div>
</div>
</div>
)
}
+65
View File
@@ -0,0 +1,65 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono, Playfair_Display } from "next/font/google";
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import { Navbar } from '@/components/common/Navbar';
import { Footer } from '@/components/common/Footer';
import "../globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const playfair = Playfair_Display({
variable: "--font-playfair",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Fethiye Holiday | Pioneer Travel",
description: "Discover the beauty of the Mediterranean with Pioneer Travel.",
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({locale}));
}
export default async function RootLayout({
children,
params
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
if (!routing.locales.includes(locale as any)) {
notFound();
}
setRequestLocale(locale);
const messages = await getMessages();
return (
<html
lang={locale}
className={`${geistSans.variable} ${geistMono.variable} ${playfair.variable} h-full antialiased dark`}
>
<body className="min-h-full flex flex-col bg-background text-foreground" suppressHydrationWarning>
<NextIntlClientProvider messages={messages}>
<Navbar locale={locale} />
{children}
<Footer />
</NextIntlClientProvider>
</body>
</html>
);
}
+94
View File
@@ -0,0 +1,94 @@
'use client'
import { useState } from 'react'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
export default function LoginPage() {
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
const result = await signIn('credentials', {
redirect: false,
email,
password,
})
if (result?.error) {
setError('Geçersiz e-posta veya şifre')
setLoading(false)
} else {
router.push('/admin')
router.refresh()
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
<div className="w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-100 dark:border-gray-800 overflow-hidden">
<div className="p-8">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Admin Girişi</h1>
<p className="text-sm text-gray-500 mt-2">Yönetim paneline erişmek için giriş yapın</p>
</div>
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-md text-sm mb-6 border border-red-100">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" htmlFor="email">
E-posta
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-white transition-colors"
placeholder="admin@ayris.tech"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" htmlFor="password">
Şifre
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-white transition-colors"
placeholder="••••••••"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-4 rounded-md transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
>
{loading ? 'Giriş yapılıyor...' : 'Giriş Yap'}
</button>
</form>
<div className="mt-6 text-center text-xs text-gray-400">
Demo credentials: admin@ayris.tech / admin
</div>
</div>
</div>
</div>
)
}
+32
View File
@@ -0,0 +1,32 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { MOCK_CATEGORIES, MOCK_TOURS, MOCK_REVIEWS } from '@/lib/mock'
import { HomePageClient } from './HomePageClient'
export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const h = await getTranslations('hero')
const s = await getTranslations('sections')
const t = await getTranslations('tour')
const dict = {
hTitle: h('title'),
hSubtitle: h('subtitle'),
hCta: h('cta'),
sPopular: s('popular'),
sViewAll: s('view_all'),
sCategories: s('categories'),
sReviews: s('reviews'),
tFrom: t('from')
}
return (
<HomePageClient
dict={dict}
tours={MOCK_TOURS}
categories={MOCK_CATEGORIES}
reviews={MOCK_REVIEWS}
/>
)
}
+48
View File
@@ -0,0 +1,48 @@
import { setRequestLocale } from 'next-intl/server'
import { MOCK_TOURS } from '@/lib/mock'
import { notFound } from 'next/navigation'
import Image from 'next/image'
import { Clock, Star } from 'lucide-react'
export default async function TourDetailPage({ params }: { params: Promise<{ locale: string, slug: string }> }) {
const { locale, slug } = await params
setRequestLocale(locale)
const tour = MOCK_TOURS.find(t => t.slug === slug)
if (!tour) notFound()
return (
<div className="bg-zinc-950 min-h-screen py-12">
<div className="container mx-auto px-4 max-w-6xl">
<div className="bg-zinc-900 rounded-2xl overflow-hidden shadow-2xl border border-zinc-800 flex flex-col md:flex-row">
<div className="w-full md:w-1/2 relative h-64 md:h-auto">
<Image src={tour.imageUrl || ''} alt={tour.title} fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover" />
</div>
<div className="w-full md:w-1/2 p-8 md:p-12 flex flex-col">
<h1 className="text-3xl md:text-4xl font-black mb-4 text-white">
{tour.title}
</h1>
<div className="flex items-center space-x-6 text-sm text-zinc-400 mb-8">
<span className="flex items-center"><Clock className="w-4 h-4 mr-1 text-orange-500" /> {tour.duration}</span>
<span className="flex items-center"><Star className="w-4 h-4 mr-1 text-orange-500 fill-current" /> 5.0 (Review)</span>
</div>
<p className="text-zinc-300 mb-8 leading-relaxed text-lg">
{tour.description}
</p>
<div className="mt-auto bg-zinc-950 p-6 rounded-xl flex items-center justify-between border border-zinc-800">
<div>
<span className="text-sm text-zinc-500 uppercase tracking-wider block mb-1">Price per person</span>
<span className="font-black text-4xl text-orange-500">${tour.price}</span>
</div>
<button className="px-8 py-4 bg-orange-500 text-white font-bold rounded-lg hover:bg-orange-600 transition-all duration-300 shadow-[0_0_15px_rgba(249,115,22,0.4)] hover:shadow-[0_0_25px_rgba(249,115,22,0.6)] transform hover:-translate-y-0.5">
Book Now
</button>
</div>
</div>
</div>
</div>
</div>
)
}
+53
View File
@@ -0,0 +1,53 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { MOCK_TOURS, MOCK_CATEGORIES } from '@/lib/mock'
import Link from 'next/link'
import Image from 'next/image'
export default async function ToursPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const s = await getTranslations('sections')
const t = await getTranslations('tour')
return (
<div className="bg-zinc-950 min-h-screen py-12">
<div className="container mx-auto px-4">
<h1 className="text-4xl font-black mb-8 text-white uppercase tracking-tighter">
{s('categories')}
</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{MOCK_TOURS.map(tour => {
const cat = MOCK_CATEGORIES.find(c => c.id === tour.categoryId)
return (
<div key={tour.id} className="bg-zinc-900 rounded-xl overflow-hidden shadow-lg border border-zinc-800 hover:border-orange-500/50 transition-all duration-300 flex flex-col group">
<div className="relative h-56 overflow-hidden">
<Image src={tour.imageUrl || ''} alt={tour.title} fill sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw" className="object-cover group-hover:scale-110 transition-transform duration-500" />
{cat && (
<div className="absolute top-4 left-4 bg-orange-500 text-white text-xs font-bold px-3 py-1 rounded-full shadow">
{cat.name}
</div>
)}
</div>
<div className="p-6 flex-1 flex flex-col">
<h3 className="font-bold text-xl mb-3 text-zinc-100 line-clamp-1">{tour.title}</h3>
<p className="text-zinc-400 text-sm mb-6 flex-1 line-clamp-3">{tour.description}</p>
<div className="flex items-center justify-between border-t border-zinc-800 pt-4 mt-auto">
<div>
<span className="text-xs text-zinc-500 uppercase tracking-wider block">{t('from')}</span>
<span className="font-bold text-2xl text-orange-500">${tour.price}</span>
</div>
<Link href={`/tours/${tour.slug}`} className="px-5 py-2.5 bg-zinc-800 text-white text-sm font-medium rounded-md hover:bg-orange-500 transition-colors duration-300">
{t('book_now')}
</Link>
</div>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
+3
View File
@@ -0,0 +1,3 @@
import { handlers } from "@/lib/auth"
export const { GET, POST } = handlers
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+124
View File
@@ -0,0 +1,124 @@
@import "tailwindcss";
@theme {
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
--font-heading: var(--font-playfair), var(--font-geist-sans), serif;
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
@layer base {
:root {
/* Modern Editorial Dark Theme defaults */
--background: oklch(0.12 0.01 260);
--foreground: oklch(0.95 0.01 80);
/* Vibrant Orange Accent */
--primary: oklch(0.65 0.20 35);
--primary-foreground: oklch(0.98 0.008 80);
--secondary: oklch(0.20 0.01 260);
--secondary-foreground: oklch(0.95 0.01 80);
--muted: oklch(0.20 0.01 260);
--muted-foreground: oklch(0.60 0.01 80);
--accent: oklch(0.65 0.20 35);
--accent-foreground: oklch(0.98 0.008 80);
--destructive: oklch(0.40 0.20 25);
--destructive-foreground: oklch(0.98 0.008 80);
--border: oklch(0.25 0.01 260);
--input: oklch(0.25 0.01 260);
--ring: oklch(0.65 0.20 35);
--card: oklch(0.15 0.01 260);
--card-foreground: oklch(0.95 0.01 80);
--popover: oklch(0.15 0.01 260);
--popover-foreground: oklch(0.95 0.01 80);
--radius: 0.75rem;
/* Fluid Typography Scale */
--text-hero: clamp(3rem, 7vw, 7rem);
--text-h1: clamp(2.5rem, 5vw, 5rem);
--text-h2: clamp(1.75rem, 3vw, 3rem);
--text-h3: clamp(1.25rem, 2vw, 1.75rem);
--text-body: 1rem;
--text-small: 0.875rem;
}
body {
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, .font-hero {
font-family: var(--font-heading);
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.02em;
}
h2 {
font-family: var(--font-heading);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.01em;
}
.text-hero { font-size: var(--text-hero); }
.text-h1 { font-size: var(--text-h1); }
.text-h2 { font-size: var(--text-h2); }
.text-h3 { font-size: var(--text-h3); }
}
@layer utilities {
.glass-panel {
background: color-mix(in oklch, var(--card) 60%, transparent);
backdrop-filter: blur(12px);
border: 1px solid color-mix(in oklch, var(--border) 40%, transparent);
}
.text-gradient {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(to right, var(--primary), oklch(0.80 0.15 50));
}
}