Update application features and add scripts (core changes)

This commit is contained in:
AyrisAI
2026-07-19 11:38:56 +03:00
parent a36b833e00
commit da84cdc231
44 changed files with 2953 additions and 449 deletions
+27
View File
@@ -0,0 +1,27 @@
import { auth } from '@/lib/auth'
import { AdminSidebar } from '@/components/admin/AdminSidebar'
import { AdminHeader } from '@/components/admin/AdminHeader'
import { SessionProvider } from 'next-auth/react'
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const session = await auth()
return (
<SessionProvider session={session}>
<div className="flex h-screen bg-[#F8FAFC] dark:bg-gray-950 overflow-hidden">
<AdminSidebar />
<div className="flex-1 flex flex-col min-w-0 overflow-hidden relative">
<AdminHeader
userEmail={session?.user?.email}
userName={session?.user?.name}
/>
<main className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8 bg-gray-50/50 dark:bg-gray-900/50">
<div className="mx-auto max-w-7xl">
{children}
</div>
</main>
</div>
</div>
</SessionProvider>
)
}
+87
View File
@@ -0,0 +1,87 @@
'use client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Users, Activity, UserPlus, Zap } from 'lucide-react'
import { motion } from 'framer-motion'
import { useSession } from 'next-auth/react'
export default function AdminDashboardPage() {
const { data: session } = useSession()
const stats = [
{ name: 'Toplam Kullanıcı', stat: '1,245', icon: Users, color: 'text-blue-600 dark:text-blue-400' },
{ name: 'Aktif Oturumlar', stat: '42', icon: Activity, color: 'text-green-600 dark:text-green-400' },
{ name: 'Yeni Kayıtlar', stat: '8', icon: UserPlus, color: 'text-orange-600 dark:text-orange-400' },
{ name: 'Sistem Durumu', stat: 'Online', icon: Zap, color: 'text-indigo-600 dark:text-indigo-400' },
]
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}
return (
<div className="space-y-8">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<h2 className="text-3xl 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 || 'Admin'}. İşte projenizin genel görünümü.
</p>
</motion.div>
<motion.div
variants={container}
initial="hidden"
animate="show"
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
>
{stats.map((stat, i) => (
<motion.div key={stat.name} variants={item}>
<Card className="border-gray-200 dark:border-gray-800 shadow-sm transition-all hover:shadow-md dark:bg-gray-950/50 backdrop-blur-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-500 dark:text-gray-400">
{stat.name}
</CardTitle>
<stat.icon className={`h-4 w-4 ${stat.color}`} />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900 dark:text-white">{stat.stat}</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.4 }}
>
<Card className="border-gray-200 dark:border-gray-800 shadow-sm dark:bg-gray-950/50 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-lg text-gray-900 dark:text-white">Son Aktiviteler</CardTitle>
</CardHeader>
<CardContent>
<div className="py-4 text-sm text-gray-500 dark:text-gray-400 text-center border-2 border-dashed rounded-lg border-gray-200 dark:border-gray-800">
Henüz aktivite kaydı bulunmuyor.
</div>
</CardContent>
</Card>
</motion.div>
</div>
)
}
@@ -0,0 +1,34 @@
import { RoomForm } from '@/components/admin/RoomForm'
import { ArrowLeft } from 'lucide-react'
import Link from 'next/link'
import { db } from '@/lib/db'
import { notFound } from 'next/navigation'
export default async function EditRoomPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const room = await db.room.findUnique({
where: { id }
})
if (!room) {
notFound()
}
return (
<div className="space-y-6">
<div className="flex items-center space-x-4">
<Link href="/admin/rooms" className="text-gray-500 hover:text-gray-900 dark:hover:text-white transition-colors">
<ArrowLeft className="h-6 w-6" />
</Link>
<div>
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Odayı Düzenle</h2>
<p className="text-gray-500 dark:text-gray-400 mt-2">
Oda bilgilerini güncellemek için aşağıdaki formu kullanın.
</p>
</div>
</div>
<RoomForm initialData={room} />
</div>
)
}
@@ -0,0 +1,23 @@
import { RoomForm } from '@/components/admin/RoomForm'
import { ArrowLeft } from 'lucide-react'
import Link from 'next/link'
export default function CreateRoomPage() {
return (
<div className="space-y-6">
<div className="flex items-center space-x-4">
<Link href="/admin/rooms" className="text-gray-500 hover:text-gray-900 dark:hover:text-white transition-colors">
<ArrowLeft className="h-6 w-6" />
</Link>
<div>
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Yeni Oda Ekle</h2>
<p className="text-gray-500 dark:text-gray-400 mt-2">
Sisteme yeni bir oda eklemek için aşağıdaki formu doldurun.
</p>
</div>
</div>
<RoomForm />
</div>
)
}
+155
View File
@@ -0,0 +1,155 @@
import { db } from '@/lib/db'
import Link from 'next/link'
import { Plus, Edit, Trash2, MoreHorizontal } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuGroup,
} from '@/components/ui/dropdown-menu'
export default async function AdminRoomsPage() {
const rooms = await db.room.findMany({
orderBy: { createdAt: 'desc' }
})
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h2 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Odalar</h2>
<p className="text-gray-500 dark:text-gray-400 mt-2">
Sistemdeki tüm odaları buradan yönetebilirsiniz.
</p>
</div>
<Link href="/admin/rooms/create">
<Button className="w-full sm:w-auto">
<Plus className="mr-2 h-4 w-4" /> Yeni Oda Ekle
</Button>
</Link>
</div>
<div className="bg-white dark:bg-gray-950/50 border border-gray-200 dark:border-gray-800 rounded-lg shadow-sm backdrop-blur-sm">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-24">Resimler</TableHead>
<TableHead>Ad (TR)</TableHead>
<TableHead>Tip</TableHead>
<TableHead>Kapasite</TableHead>
<TableHead>Fiyat</TableHead>
<TableHead>Durum</TableHead>
<TableHead className="text-right">İşlemler</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rooms.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center text-gray-500">
Henüz oda bulunmuyor.
</TableCell>
</TableRow>
) : (
rooms.map((room) => (
<TableRow key={room.id} className="group">
<TableCell>
<div className="flex flex-col gap-2">
{room.imageUrl ? (
<img
src={room.imageUrl}
alt={room.nameTr}
className="h-10 w-16 rounded-md object-cover border border-gray-200 dark:border-gray-800"
/>
) : (
<div className="h-10 w-16 rounded-md bg-gray-100 dark:bg-gray-800 flex items-center justify-center border border-gray-200 dark:border-gray-800">
<span className="text-[10px] text-gray-400 dark:text-gray-500">Yok</span>
</div>
)}
{room.images && room.images.length > 0 && (
<div className="flex gap-1">
{room.images.slice(0, 3).map((img, i) => (
<img key={i} src={img} alt="galeri" className="h-6 w-8 rounded object-cover border border-gray-200 dark:border-gray-800" />
))}
{room.images.length > 3 && (
<div className="h-6 w-8 rounded bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-[10px] font-medium border border-gray-200 dark:border-gray-800 text-gray-600 dark:text-gray-300">
+{room.images.length - 3}
</div>
)}
</div>
)}
</div>
</TableCell>
<TableCell className="font-medium text-gray-900 dark:text-white">
{room.nameTr}
</TableCell>
<TableCell className="text-gray-500 dark:text-gray-400">
{room.type === 'STUDIO_1_0' ? 'Stüdyo (1+0)' : 'Süit (1+1)'}
</TableCell>
<TableCell className="text-gray-500 dark:text-gray-400">
{room.capacity} Kişi
</TableCell>
<TableCell className="text-gray-500 dark:text-gray-400">
{room.price ? `${room.price}` : '-'}
</TableCell>
<TableCell>
{room.available ? (
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-900">
Müsait
</Badge>
) : (
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200 dark:bg-red-950/30 dark:text-red-400 dark:border-red-900">
Kapalı
</Badge>
)}
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity focus:opacity-100" />}>
<span className="sr-only">Menüyü </span>
<MoreHorizontal className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[160px]">
<DropdownMenuGroup>
<DropdownMenuLabel>İşlemler</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem render={<Link href={`/admin/rooms/${room.id}/edit`} />}>
<Edit className="mr-2 h-4 w-4 text-blue-600 dark:text-blue-400" />
Düzenle
</DropdownMenuItem>
<DropdownMenuItem render={<form action={async () => {
'use server'
const { deleteRoom } = await import('@/lib/actions/room')
await deleteRoom(room.id)
}} />}>
<button type="submit" className="w-full flex items-center cursor-pointer text-red-600 dark:text-red-400 focus:text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Sil
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
)
}
+33
View File
@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Literata, Nunito_Sans } from "next/font/google";
import "@/app/globals.css";
const nunito_sans = Nunito_Sans({
variable: "--font-nunito-sans",
subsets: ["latin"],
weight: ["400", "600", "700"],
});
const literata = Literata({
variable: "--font-literata",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "Admin Paneli - Sitar Apart",
};
export default function AdminRootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="tr" className={`${nunito_sans.variable} ${literata.variable} h-full antialiased font-sans`}>
<body className="h-full bg-gray-50 text-gray-900" suppressHydrationWarning>
{children}
</body>
</html>
);
}
-97
View File
@@ -1,97 +0,0 @@
'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
@@ -1,47 +0,0 @@
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>
)
}
+94 -92
View File
@@ -3,6 +3,8 @@ import { Link } from '@/i18n/routing'
import Image from 'next/image'
import { notFound } from 'next/navigation'
import { CheckCircle2, Maximize, Users, BedDouble, ArrowRight } from 'lucide-react'
import { db } from '@/lib/db'
import openinaryLoader from '@/lib/openinary-loader'
// Bu sayfa parametre olarak dinamik slug alacak
export default async function RoomDetailPage({ params }: { params: Promise<{ locale: string, slug: string }> }) {
@@ -11,77 +13,61 @@ export default async function RoomDetailPage({ params }: { params: Promise<{ loc
const tRooms = await getTranslations('rooms')
const MOCK_ROOMS = [
{
slug: '1-0-daire',
name: tRooms('room_list.1_0_daire.name'),
description: tRooms('room_list.1_0_daire.desc'),
capacity: 2,
size: '25m²',
beds: '1',
image: 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
gallery: [
'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80'
],
amenities: ['Mini Mutfak', 'Klima', 'Balkon', 'WiFi', 'TV', 'Saç Kurutma Makinesi']
},
{
slug: '1-1-daire-a',
name: tRooms('room_list.1_1_daire_a.name'),
description: tRooms('room_list.1_1_daire_a.desc'),
capacity: 2,
size: '40m²',
beds: '1 + Sofa',
image: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
gallery: [
'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80'
],
amenities: ['Ayrı Mutfak', 'Klima', 'Balkon', 'WiFi', 'Oturma Alanı', 'Çamaşır Makinesi']
},
{
slug: '1-1-daire-b',
name: tRooms('room_list.1_1_daire_b.name'),
description: tRooms('room_list.1_1_daire_b.desc'),
capacity: 4,
size: '50m²',
beds: '2',
image: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
gallery: [
'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80'
],
amenities: ['2 Oda', 'Ayrı Mutfak', 'Klima', 'Balkon', 'WiFi', 'Geniş Oturma Alanı']
}
];
const room = await db.room.findUnique({
where: { slug }
})
const room = MOCK_ROOMS.find(r => r.slug === slug);
if (!room) {
if (!room || !room.available) {
notFound();
}
const name = locale === 'tr' ? room.nameTr : locale === 'de' ? room.nameDe : room.nameEn
const description = locale === 'tr' ? room.descriptionTr : locale === 'de' ? room.descriptionDe : room.descriptionEn
const image = room.imageUrl || 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80'
const isExternalImage = image.startsWith('http') && !image.includes('media.ayris.tech')
const gallery = room.images && room.images.length > 0 ? room.images : [image]
// Size helper mapping based on room type
const getRoomSize = (type: string) => {
if (type === 'STUDIO_1_0') return '25m²'
if (type === 'SUITE_1_1') return '45m²'
return '20m²'
}
// Bed helper based on capacity/type roughly
const getBeds = (type: string, capacity: number) => {
if (type === 'STUDIO_1_0') return '1'
if (type === 'SUITE_1_1') return '2'
if (capacity === 3) return '1 + Ek Yatak'
return '1'
}
return (
<div className="bg-background min-h-screen text-on-surface">
{/* Hero Section */}
<section className="relative h-[60vh] md:h-[70vh] w-full">
<Image
src={room.image}
alt={room.name}
fill
sizes="100vw"
className="object-cover"
priority
/>
{isExternalImage ? (
<img
src={image}
alt={name}
className="object-cover w-full h-full"
/>
) : (
<Image
src={image}
alt={name}
fill
loader={openinaryLoader}
sizes="100vw"
className="object-cover"
priority
/>
)}
<div className="absolute inset-0 bg-black/40" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<h1 className="font-serif text-[48px] md:text-[64px] text-white font-bold drop-shadow-md">
{room.name}
{name}
</h1>
</div>
</div>
@@ -102,55 +88,71 @@ export default async function RoomDetailPage({ params }: { params: Promise<{ loc
</div>
<div className="flex items-center gap-2 bg-surface-container px-4 py-2 rounded-full text-secondary font-medium">
<Maximize className="w-5 h-5 text-primary" />
<span>{room.size}</span>
<span>{getRoomSize(room.type)}</span>
</div>
<div className="flex items-center gap-2 bg-surface-container px-4 py-2 rounded-full text-secondary font-medium">
<BedDouble className="w-5 h-5 text-primary" />
<span>{room.beds} Yatak</span>
<span>{getBeds(room.type, room.capacity)}</span>
</div>
</div>
{/* Description */}
<div className="prose prose-lg text-on-surface-variant">
<p className="text-[18px] leading-relaxed">
{room.description}
<p className="text-[18px] leading-relaxed whitespace-pre-wrap">
{description}
</p>
</div>
{/* Amenities */}
<div>
<h3 className="font-serif text-[28px] text-secondary font-semibold mb-6">
{tRooms('details.features')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{room.amenities.map((amenity, index) => (
<div key={index} className="flex items-center gap-3">
<CheckCircle2 className="w-5 h-5 text-accent-green flex-shrink-0" />
<span className="text-on-surface-variant">{amenity}</span>
</div>
))}
{room.amenities.length > 0 && (
<div>
<h3 className="font-serif text-[28px] text-secondary font-semibold mb-6">
{tRooms('details.features')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{room.amenities.map((amenity, index) => (
<div key={index} className="flex items-center gap-3">
<CheckCircle2 className="w-5 h-5 text-accent-green flex-shrink-0" />
<span className="text-on-surface-variant">{amenity}</span>
</div>
))}
</div>
</div>
</div>
)}
{/* Mini Gallery */}
<div>
<h3 className="font-serif text-[28px] text-secondary font-semibold mb-6">
{tRooms('details.gallery')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{room.gallery.map((img, i) => (
<div key={i} className="relative aspect-square rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">
<Image
src={img}
alt={`${room.name} gallery ${i + 1}`}
fill
sizes="(max-width: 640px) 100vw, 33vw"
className="object-cover hover:scale-105 transition-transform duration-500"
/>
</div>
))}
{gallery.length > 0 && (
<div>
<h3 className="font-serif text-[28px] text-secondary font-semibold mb-6">
{tRooms('details.gallery')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{gallery.map((img, i) => {
const isGalleryExternal = img.startsWith('http') && !img.includes('media.ayris.tech')
return (
<div key={i} className="relative aspect-square rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">
{isGalleryExternal ? (
<img
src={img}
alt={`${name} gallery ${i + 1}`}
className="object-cover w-full h-full hover:scale-105 transition-transform duration-500"
/>
) : (
<Image
src={img}
alt={`${name} gallery ${i + 1}`}
fill
loader={openinaryLoader}
sizes="(max-width: 640px) 100vw, 33vw"
className="object-cover hover:scale-105 transition-transform duration-500"
/>
)}
</div>
)
})}
</div>
</div>
</div>
)}
</div>
+73 -71
View File
@@ -2,6 +2,8 @@ import { getTranslations, setRequestLocale } from 'next-intl/server'
import { Link } from '@/i18n/routing'
import Image from 'next/image'
import { Users, Square, Info } from 'lucide-react'
import { db } from '@/lib/db'
import openinaryLoader from '@/lib/openinary-loader'
export default async function RoomsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
@@ -10,35 +12,17 @@ export default async function RoomsPage({ params }: { params: Promise<{ locale:
const tNav = await getTranslations('nav')
const tRooms = await getTranslations('rooms')
const MOCK_ROOMS = [
{
slug: '1-0-daire',
name: tRooms('room_list.1_0_daire.name'),
description: tRooms('room_list.1_0_daire.desc'),
capacity: 2,
size: '25m²',
image: 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
amenities: ['Mini Mutfak', 'Klima', 'Balkon', 'WiFi']
},
{
slug: '1-1-daire-a',
name: tRooms('room_list.1_1_daire_a.name'),
description: tRooms('room_list.1_1_daire_a.desc'),
capacity: 2,
size: '40m²',
image: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
amenities: ['Ayrı Mutfak', 'Klima', 'Balkon', 'WiFi', 'Oturma Alanı']
},
{
slug: '1-1-daire-b',
name: tRooms('room_list.1_1_daire_b.name'),
description: tRooms('room_list.1_1_daire_b.desc'),
capacity: 4,
size: '50m²',
image: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
amenities: ['2 Oda', 'Ayrı Mutfak', 'Klima', 'Balkon', 'WiFi']
}
];
const dbRooms = await db.room.findMany({
where: { available: true },
orderBy: { createdAt: 'asc' }
})
// Size helper mapping based on room type
const getRoomSize = (type: string) => {
if (type === 'STUDIO_1_0') return '25m²'
if (type === 'SUITE_1_1') return '45m²'
return '20m²'
}
return (
<div className="flex-1 bg-gray-50 py-12 pt-32">
@@ -51,55 +35,73 @@ export default async function RoomsPage({ params }: { params: Promise<{ locale:
</div>
<div className="space-y-12 max-w-5xl mx-auto">
{MOCK_ROOMS.map((room) => (
<div key={room.slug} className="bg-white rounded-2xl overflow-hidden border shadow-sm flex flex-col md:flex-row group">
<div className="relative w-full md:w-2/5 h-64 md:h-auto bg-gray-100">
<Image
src={room.image}
alt={room.name}
fill
sizes="(max-width: 768px) 100vw, 40vw"
className="object-cover group-hover:scale-105 transition-transform duration-500"
/>
</div>
<div className="p-6 md:p-8 flex-1 flex flex-col">
<div className="flex justify-between items-start mb-4">
<h2 className="text-2xl font-bold text-gray-900">{room.name}</h2>
{dbRooms.map((room) => {
const name = locale === 'tr' ? room.nameTr : locale === 'de' ? room.nameDe : room.nameEn
const description = locale === 'tr' ? room.descriptionTr : locale === 'de' ? room.descriptionDe : room.descriptionEn
const image = room.imageUrl || 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80'
const isExternal = image.startsWith('http') && !image.includes('media.ayris.tech')
return (
<div key={room.id} className="bg-white rounded-2xl overflow-hidden border shadow-sm flex flex-col md:flex-row group">
<div className="relative w-full md:w-2/5 h-64 md:h-auto bg-gray-100">
{isExternal ? (
<img
src={image}
alt={name}
className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-500"
/>
) : (
<Image
src={image}
alt={name}
fill
loader={openinaryLoader}
sizes="(max-width: 768px) 100vw, 40vw"
className="object-cover group-hover:scale-105 transition-transform duration-500"
/>
)}
</div>
<p className="text-gray-600 mb-6 flex-1">
{room.description}
</p>
<div className="p-6 md:p-8 flex-1 flex flex-col">
<div className="flex justify-between items-start mb-4">
<h2 className="text-2xl font-bold text-gray-900">{name}</h2>
</div>
<p className="text-gray-600 mb-6 flex-1 line-clamp-3">
{description}
</p>
<div className="flex flex-wrap gap-4 mb-8">
<div className="flex items-center gap-1.5 text-sm text-gray-500 font-medium">
<Users className="w-4 h-4 text-[#2FB6C4]" />
<span>{room.capacity} {tRooms('capacity')}</span>
<div className="flex flex-wrap gap-4 mb-8">
<div className="flex items-center gap-1.5 text-sm text-gray-500 font-medium">
<Users className="w-4 h-4 text-[#2FB6C4]" />
<span>{room.capacity} {tRooms('capacity')}</span>
</div>
<div className="flex items-center gap-1.5 text-sm text-gray-500 font-medium">
<Square className="w-4 h-4 text-[#2FB6C4]" />
<span>{getRoomSize(room.type)}</span>
</div>
<div className="flex items-center gap-1.5 text-sm text-gray-500 font-medium">
<Info className="w-4 h-4 text-[#2FB6C4]" />
<span>{room.amenities.length} {tRooms('amenities')}</span>
</div>
</div>
<div className="flex items-center gap-1.5 text-sm text-gray-500 font-medium">
<Square className="w-4 h-4 text-[#2FB6C4]" />
<span>{room.size}</span>
</div>
<div className="flex items-center gap-1.5 text-sm text-gray-500 font-medium">
<Info className="w-4 h-4 text-[#2FB6C4]" />
<span>{room.amenities.length} {tRooms('amenities')}</span>
</div>
</div>
<div className="flex items-center justify-between mt-auto border-t pt-6">
<div className="text-sm text-gray-500">
<span className="font-bold text-gray-900 text-lg">{tRooms('ask_price')}</span> {tRooms('per_night')}
<div className="flex items-center justify-between mt-auto border-t pt-6">
<div className="text-sm text-gray-500">
<span className="font-bold text-gray-900 text-lg">
{room.price ? `${room.price}` : tRooms('ask_price')}
</span> {room.price ? '' : tRooms('per_night')}
</div>
<Link
href={`/dairelerimiz/${room.slug}`}
className="px-6 py-2.5 bg-[#2FB6C4] hover:bg-[#2597a3] text-white rounded-md font-medium transition-colors duration-300 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2FB6C4] focus-visible:ring-offset-2"
>
{tRooms('details_btn')}
</Link>
</div>
<Link
href={`/dairelerimiz/${room.slug}`}
className="px-6 py-2.5 bg-[#2FB6C4] hover:bg-[#2597a3] text-white rounded-md font-medium transition-colors duration-300 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2FB6C4] focus-visible:ring-offset-2"
>
{tRooms('details_btn')}
</Link>
</div>
</div>
</div>
))}
)
})}
</div>
</div>
</div>
+24 -78
View File
@@ -6,68 +6,34 @@ import Image from 'next/image'
import BougainvilleaMotif from '@/components/BougainvilleaMotif'
import { useTranslations } from 'next-intl'
import { X } from 'lucide-react'
import openinaryLoader from '@/lib/openinary-loader'
export default function GalleryPage() {
const t = useTranslations('gallery')
const [activeFilter, setActiveFilter] = useState('all')
const [selectedImage, setSelectedImage] = useState<string | null>(null)
const filters = [
{ id: 'all', label: t('filters.all') },
{ id: 'rooms', label: t('filters.rooms') },
{ id: 'exterior', label: t('filters.exterior') },
{ id: 'view', label: t('filters.view') },
{ id: 'balcony', label: t('filters.balcony') },
const galleryImages = [
'DSC01477.jpg',
'DSC01478.jpg',
'DSC01479.jpg',
'DSC01480.jpg',
'DSC01481.jpg',
'DSC01482.jpg',
'DSC01483.jpg',
'DSC01487.jpg',
'DSC01535.jpg',
'DSC01544.jpg',
'DSC01554.jpg',
'DSC01560.jpg',
'DSC01563.jpg'
]
const galleryItems = [
{
id: 1,
category: 'rooms',
title: t('items.1'),
src: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
colSpan: 'col-span-1',
},
{
id: 2,
category: 'exterior',
title: t('items.2'),
src: 'https://images.unsplash.com/photo-1596394516093-501ba68a0ba6?auto=format&fit=crop&q=80',
colSpan: 'col-span-1 md:col-span-2 lg:col-span-1',
},
{
id: 3,
category: 'view',
title: t('items.3'),
src: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
colSpan: 'col-span-1',
},
{
id: 4,
category: 'balcony',
title: t('items.4'),
src: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
colSpan: 'col-span-1 md:col-span-2',
},
{
id: 5,
category: 'rooms',
title: t('items.5'),
src: 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
colSpan: 'col-span-1',
},
{
id: 6,
category: 'view',
title: t('items.6'),
src: 'https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80',
colSpan: 'col-span-1 md:col-span-2 lg:col-span-1',
},
]
const filteredItems = galleryItems.filter(item =>
activeFilter === 'all' || item.category === activeFilter
)
const galleryItems = galleryImages.map((file, idx) => ({
id: idx + 1,
title: t('items.' + ((idx % 6) + 1)), // Use existing translations just to not break them
src: `https://media.ayris.tech/t/starapart/gallery/${file}`,
colSpan: idx % 4 === 1 ? 'col-span-1 md:col-span-2 lg:col-span-1' : 'col-span-1'
}))
return (
<div className="bg-background min-h-screen text-on-surface pt-32 pb-section-gap container mx-auto px-4">
@@ -84,27 +50,10 @@ export default function GalleryPage() {
</p>
</header>
{/* Filter Bar */}
<div className="flex flex-wrap items-center gap-4 mb-12">
{filters.map((filter) => (
<button
key={filter.id}
onClick={() => setActiveFilter(filter.id)}
className={`px-6 py-2 rounded-full font-sans font-bold text-[14px] uppercase tracking-wider transition-all duration-300 border-2 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
activeFilter === filter.id
? 'border-primary text-primary bg-primary/10'
: 'border-transparent text-secondary hover:border-primary/30'
}`}
>
{filter.label}
</button>
))}
</div>
{/* Masonry-like Grid */}
<motion.div layout className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<AnimatePresence>
{filteredItems.map((item) => (
{galleryItems.map((item) => (
<motion.div
key={item.id}
layout
@@ -127,14 +76,10 @@ export default function GalleryPage() {
src={item.src}
alt={item.title}
fill
loader={openinaryLoader}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transform group-hover:scale-105 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-6">
<span className="text-white font-serif text-[24px] font-medium drop-shadow-md">
{item.title}
</span>
</div>
</div>
</motion.div>
))}
@@ -169,6 +114,7 @@ export default function GalleryPage() {
src={selectedImage}
alt="Enlarged view"
fill
loader={openinaryLoader}
className="object-contain"
sizes="100vw"
quality={90}
+11 -1
View File
@@ -3,7 +3,7 @@
import { motion } from 'framer-motion'
import Image from 'next/image'
import BougainvilleaMotif from '@/components/BougainvilleaMotif'
import { MapPin, Waves, ShoppingBasket, Coffee, PersonStanding, Phone, Mail } from 'lucide-react'
import { MapPin, Waves, ShoppingBasket, Coffee, PersonStanding, Phone, Mail, Clock } from 'lucide-react'
import { useTranslations } from 'next-intl'
const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1]
@@ -175,6 +175,16 @@ export default function ContactPage() {
<p className="font-sans text-[16px] text-secondary font-medium">iletisim@sitarapart.com</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
<Clock className="w-5 h-5" />
</div>
<div>
<p className="font-sans font-bold text-[12px] text-on-surface-variant uppercase tracking-wider">{t('connect.check_in')}</p>
<p className="font-sans text-[16px] text-secondary font-medium">{t('connect.check_out')}</p>
</div>
</div>
</div>
{/* Decorative Image */}
+9 -7
View File
@@ -6,6 +6,7 @@ import { motion, useReducedMotion } from 'framer-motion'
import { MapPin, Wifi, UtensilsCrossed, Waves, Car } from 'lucide-react'
import Image from 'next/image'
import BougainvilleaMotif from '@/components/BougainvilleaMotif'
import openinaryLoader from '@/lib/openinary-loader'
const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1]
@@ -36,7 +37,7 @@ export default function HomePage() {
<header className="relative h-screen w-full flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80"
src="https://media.ayris.tech/t/starapart/hero/media__1784311319733.jpg"
alt="Sitar Apart Hotel View"
fill
className="object-cover"
@@ -125,22 +126,22 @@ export default function HomePage() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-gutter">
{[
{
id: '1-0-daire',
id: 'standart-oda',
title: tRooms('room_1.title'),
desc: tRooms('room_1.desc'),
img: 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80'
img: 'https://media.ayris.tech/t/starapart/SUITE_1_1/DSC01491 (1).JPG'
},
{
id: '1-1-daire-a',
id: 'kucuk-oda',
title: tRooms('room_2.title'),
desc: tRooms('room_2.desc'),
img: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80'
img: 'https://media.ayris.tech/t/starapart/STUDIO_1_0/DSC01465 (1).JPG'
},
{
id: '1-1-daire-b',
id: '1-1-daire',
title: tRooms('room_3.title'),
desc: tRooms('room_3.desc'),
img: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80'
img: 'https://media.ayris.tech/t/starapart/SUITE_1_1/DSC01491 (1).JPG'
}
].map((room, idx) => (
<motion.div
@@ -156,6 +157,7 @@ export default function HomePage() {
src={room.img}
alt={room.title}
fill
loader={openinaryLoader}
className="object-cover transition-transform duration-700 group-hover:scale-110"
/>
</div>