diff --git a/app/(admin-routes)/admin/layout.tsx b/app/(admin-routes)/admin/layout.tsx new file mode 100644 index 0000000..8080665 --- /dev/null +++ b/app/(admin-routes)/admin/layout.tsx @@ -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 ( + +
+ +
+ +
+
+ {children} +
+
+
+
+
+ ) +} diff --git a/app/(admin-routes)/admin/page.tsx b/app/(admin-routes)/admin/page.tsx new file mode 100644 index 0000000..b258f35 --- /dev/null +++ b/app/(admin-routes)/admin/page.tsx @@ -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 ( +
+ +

Dashboard

+

+ Hoş geldiniz, {session?.user?.name || session?.user?.email || 'Admin'}. İşte projenizin genel görünümü. +

+
+ + + {stats.map((stat, i) => ( + + + + + {stat.name} + + + + +
{stat.stat}
+
+
+
+ ))} +
+ + + + + Son Aktiviteler + + +
+ Henüz aktivite kaydı bulunmuyor. +
+
+
+
+
+ ) +} diff --git a/app/(admin-routes)/admin/rooms/[id]/edit/page.tsx b/app/(admin-routes)/admin/rooms/[id]/edit/page.tsx new file mode 100644 index 0000000..3427d2f --- /dev/null +++ b/app/(admin-routes)/admin/rooms/[id]/edit/page.tsx @@ -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 ( +
+
+ + + +
+

Odayı Düzenle

+

+ Oda bilgilerini güncellemek için aşağıdaki formu kullanın. +

+
+
+ + +
+ ) +} diff --git a/app/(admin-routes)/admin/rooms/create/page.tsx b/app/(admin-routes)/admin/rooms/create/page.tsx new file mode 100644 index 0000000..1f9fc84 --- /dev/null +++ b/app/(admin-routes)/admin/rooms/create/page.tsx @@ -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 ( +
+
+ + + +
+

Yeni Oda Ekle

+

+ Sisteme yeni bir oda eklemek için aşağıdaki formu doldurun. +

+
+
+ + +
+ ) +} diff --git a/app/(admin-routes)/admin/rooms/page.tsx b/app/(admin-routes)/admin/rooms/page.tsx new file mode 100644 index 0000000..cde1844 --- /dev/null +++ b/app/(admin-routes)/admin/rooms/page.tsx @@ -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 ( +
+
+
+

Odalar

+

+ Sistemdeki tüm odaları buradan yönetebilirsiniz. +

+
+ + + +
+ +
+ + + + Resimler + Ad (TR) + Tip + Kapasite + Fiyat + Durum + İşlemler + + + + {rooms.length === 0 ? ( + + + Henüz oda bulunmuyor. + + + ) : ( + rooms.map((room) => ( + + +
+ {room.imageUrl ? ( + {room.nameTr} + ) : ( +
+ Yok +
+ )} + + {room.images && room.images.length > 0 && ( +
+ {room.images.slice(0, 3).map((img, i) => ( + galeri + ))} + {room.images.length > 3 && ( +
+ +{room.images.length - 3} +
+ )} +
+ )} +
+
+ + {room.nameTr} + + + {room.type === 'STUDIO_1_0' ? 'Stüdyo (1+0)' : 'Süit (1+1)'} + + + {room.capacity} Kişi + + + {room.price ? `₺${room.price}` : '-'} + + + {room.available ? ( + + Müsait + + ) : ( + + Kapalı + + )} + + + + }> + Menüyü aç + + + + + İşlemler + + + }> + + Düzenle + + { + 'use server' + const { deleteRoom } = await import('@/lib/actions/room') + await deleteRoom(room.id) + }} />}> + + + + + +
+ )) + )} +
+
+
+
+ ) +} diff --git a/app/(admin-routes)/layout.tsx b/app/(admin-routes)/layout.tsx new file mode 100644 index 0000000..a1aa560 --- /dev/null +++ b/app/(admin-routes)/layout.tsx @@ -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 ( + + + {children} + + + ); +} diff --git a/app/[locale]/login/page.tsx b/app/(admin-routes)/login/page.tsx similarity index 100% rename from app/[locale]/login/page.tsx rename to app/(admin-routes)/login/page.tsx diff --git a/app/[locale]/admin/layout.tsx b/app/[locale]/admin/layout.tsx deleted file mode 100644 index d16615e..0000000 --- a/app/[locale]/admin/layout.tsx +++ /dev/null @@ -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 ( -
- {/* Mobile sidebar backdrop */} - {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - - {/* Sidebar */} -
-
-
-

Admin Paneli

- -
- - - -
- -
-
-
- - {/* Main content */} -
-
- - Admin Paneli -
- -
- {children} -
-
-
- ) -} diff --git a/app/[locale]/admin/page.tsx b/app/[locale]/admin/page.tsx deleted file mode 100644 index 4b16051..0000000 --- a/app/[locale]/admin/page.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { auth } from '@/lib/auth' - -export default async function AdminDashboardPage() { - const session = await auth() - - return ( -
-
-

Dashboard

-

- Hoş geldiniz, {session?.user?.name || session?.user?.email}. İşte projenizin genel görünümü. -

-
- -
- {/* 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) => ( -
-
{item.name}
-
- {item.stat} -
-
- ))} -
- -
-
-

Son Aktiviteler

-
-
- Henüz aktivite kaydı bulunmuyor. -
-
-
-
-
- ) -} diff --git a/app/[locale]/dairelerimiz/[slug]/page.tsx b/app/[locale]/dairelerimiz/[slug]/page.tsx index b71fd0c..729f4fc 100644 --- a/app/[locale]/dairelerimiz/[slug]/page.tsx +++ b/app/[locale]/dairelerimiz/[slug]/page.tsx @@ -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 (
{/* Hero Section */}
- {room.name} + {isExternalImage ? ( + {name} + ) : ( + {name} + )}

- {room.name} + {name}

@@ -102,55 +88,71 @@ export default async function RoomDetailPage({ params }: { params: Promise<{ loc
- {room.size} + {getRoomSize(room.type)}
- {room.beds} Yatak + {getBeds(room.type, room.capacity)}
{/* Description */}
-

- {room.description} +

+ {description}

{/* Amenities */} -
-

- {tRooms('details.features')} -

-
- {room.amenities.map((amenity, index) => ( -
- - {amenity} -
- ))} + {room.amenities.length > 0 && ( +
+

+ {tRooms('details.features')} +

+
+ {room.amenities.map((amenity, index) => ( +
+ + {amenity} +
+ ))} +
-
+ )} {/* Mini Gallery */} -
-

- {tRooms('details.gallery')} -

-
- {room.gallery.map((img, i) => ( -
- {`${room.name} -
- ))} + {gallery.length > 0 && ( +
+

+ {tRooms('details.gallery')} +

+
+ {gallery.map((img, i) => { + const isGalleryExternal = img.startsWith('http') && !img.includes('media.ayris.tech') + return ( +
+ {isGalleryExternal ? ( + {`${name} + ) : ( + {`${name} + )} +
+ ) + })} +
-
+ )}
diff --git a/app/[locale]/dairelerimiz/page.tsx b/app/[locale]/dairelerimiz/page.tsx index 4956b64..3872c78 100644 --- a/app/[locale]/dairelerimiz/page.tsx +++ b/app/[locale]/dairelerimiz/page.tsx @@ -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 (
@@ -51,55 +35,73 @@ export default async function RoomsPage({ params }: { params: Promise<{ locale:
- {MOCK_ROOMS.map((room) => ( -
-
- {room.name} -
-
-
-

{room.name}

+ {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 ( +
+
+ {isExternal ? ( + {name} + ) : ( + {name} + )}
- -

- {room.description} -

+
+
+

{name}

+
+ +

+ {description} +

-
-
- - {room.capacity} {tRooms('capacity')} +
+
+ + {room.capacity} {tRooms('capacity')} +
+
+ + {getRoomSize(room.type)} +
+
+ + {room.amenities.length} {tRooms('amenities')} +
-
- - {room.size} -
-
- - {room.amenities.length} {tRooms('amenities')} -
-
-
-
- {tRooms('ask_price')} {tRooms('per_night')} +
+
+ + {room.price ? `${room.price}₺` : tRooms('ask_price')} + {room.price ? '' : tRooms('per_night')} +
+ + {tRooms('details_btn')} +
- - {tRooms('details_btn')} -
-
- ))} + ) + })}
diff --git a/app/[locale]/galeri/page.tsx b/app/[locale]/galeri/page.tsx index 64486c8..8e411f0 100644 --- a/app/[locale]/galeri/page.tsx +++ b/app/[locale]/galeri/page.tsx @@ -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(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 (
@@ -84,27 +50,10 @@ export default function GalleryPage() {

- {/* Filter Bar */} -
- {filters.map((filter) => ( - - ))} -
- {/* Masonry-like Grid */} - {filteredItems.map((item) => ( + {galleryItems.map((item) => ( -
- - {item.title} - -
))} @@ -169,6 +114,7 @@ export default function GalleryPage() { src={selectedImage} alt="Enlarged view" fill + loader={openinaryLoader} className="object-contain" sizes="100vw" quality={90} diff --git a/app/[locale]/iletisim/page.tsx b/app/[locale]/iletisim/page.tsx index 33eceee..70f4c07 100644 --- a/app/[locale]/iletisim/page.tsx +++ b/app/[locale]/iletisim/page.tsx @@ -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() {

iletisim@sitarapart.com

+ +
+
+ +
+
+

{t('connect.check_in')}

+

{t('connect.check_out')}

+
+
{/* Decorative Image */} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index ba40208..b7950dd 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -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() {
Sitar Apart Hotel View {[ { - 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) => (
diff --git a/components/admin/AdminHeader.tsx b/components/admin/AdminHeader.tsx new file mode 100644 index 0000000..b55e2d7 --- /dev/null +++ b/components/admin/AdminHeader.tsx @@ -0,0 +1,122 @@ +'use client' + +import { useState } from 'react' +import { signOut } from 'next-auth/react' +import { Menu, LogOut, Settings, User } from 'lucide-react' +import { usePathname } from 'next/navigation' +import Link from 'next/link' +import { ADMIN_NAVIGATION } from './AdminSidebar' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + DropdownMenuGroup, +} from '@/components/ui/dropdown-menu' +import { Avatar, AvatarFallback } from '@/components/ui/avatar' +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet' + +interface AdminHeaderProps { + userEmail?: string | null + userName?: string | null +} + +export function AdminHeader({ userEmail, userName }: AdminHeaderProps) { + const pathname = usePathname() + const [open, setOpen] = useState(false) + + const initials = userName + ? userName.slice(0, 2).toUpperCase() + : userEmail?.slice(0, 2).toUpperCase() || 'AD' + + return ( +
+ + {/* Mobile Menu */} +
+ + }> + + Menüyü aç + + + + + Sitar Admin + + + + + + Admin +
+ +
+ {/* User Dropdown */} + + }> + + + {initials} + + + + + + +
+

+ {userName || 'Admin'} +

+

+ {userEmail} +

+
+
+
+ + }> + + Ayarlar + + }> + + Siteye Git + + + signOut({ callbackUrl: '/' })} + > + + Çıkış Yap + +
+
+
+
+ ) +} diff --git a/components/admin/AdminSidebar.tsx b/components/admin/AdminSidebar.tsx new file mode 100644 index 0000000..e48bef8 --- /dev/null +++ b/components/admin/AdminSidebar.tsx @@ -0,0 +1,50 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { LayoutDashboard, Users, Settings, BedDouble } from 'lucide-react' + +export const ADMIN_NAVIGATION = [ + { name: 'Dashboard', href: '/admin', icon: LayoutDashboard }, + { name: 'Odalar', href: '/admin/rooms', icon: BedDouble }, + { name: 'Kullanıcılar', href: '/admin/users', icon: Users }, + { name: 'Ayarlar', href: '/admin/settings', icon: Settings }, +] + +export function AdminSidebar() { + const pathname = usePathname() + + return ( +
+
+

+ Sitar Admin +

+
+
+ +
+
+ ) +} diff --git a/components/admin/ImageUploadModal.tsx b/components/admin/ImageUploadModal.tsx new file mode 100644 index 0000000..df81cf7 --- /dev/null +++ b/components/admin/ImageUploadModal.tsx @@ -0,0 +1,249 @@ +'use client' + +import { useState, useRef } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, + DialogTrigger, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { uploadImageAction } from '@/lib/actions/upload' +import { ImagePlus, X, UploadCloud, Loader2, CheckCircle2, AlertCircle } from 'lucide-react' + +interface ImageUploadModalProps { + onUploadComplete: (urls: string[]) => void + multiple?: boolean + triggerText?: string + folderName?: string +} + +export function ImageUploadModal({ + onUploadComplete, + multiple = false, + triggerText = "Resim Yükle", + folderName = "starapart" +}: ImageUploadModalProps) { + const [open, setOpen] = useState(false) + const [selectedFiles, setSelectedFiles] = useState([]) + const [previews, setPreviews] = useState([]) + const [isUploading, setIsUploading] = useState(false) + const [progress, setProgress] = useState(0) // 0 to total files + const [error, setError] = useState(null) + + const fileInputRef = useRef(null) + + const resetState = () => { + setSelectedFiles([]) + previews.forEach(p => URL.revokeObjectURL(p)) + setPreviews([]) + setIsUploading(false) + setProgress(0) + setError(null) + } + + const handleOpenChange = (newOpen: boolean) => { + if (isUploading) return // Prevent closing while uploading + setOpen(newOpen) + if (!newOpen) { + resetState() + } + } + + const handleFileChange = (e: React.ChangeEvent) => { + if (!e.target.files?.length) return + + const files = Array.from(e.target.files) + const newFiles = multiple ? [...selectedFiles, ...files] : [files[0]] + + setSelectedFiles(newFiles) + + // Create preview URLs + const newPreviews = newFiles.map(file => URL.createObjectURL(file)) + if (!multiple) { + previews.forEach(p => URL.revokeObjectURL(p)) // cleanup old previews + setPreviews(newPreviews) + } else { + setPreviews([...previews, ...newPreviews]) + } + + setError(null) + + // Reset file input so same file can be selected again if removed + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + + const removeFile = (index: number) => { + const newFiles = [...selectedFiles] + newFiles.splice(index, 1) + + const newPreviews = [...previews] + URL.revokeObjectURL(newPreviews[index]) + newPreviews.splice(index, 1) + + setSelectedFiles(newFiles) + setPreviews(newPreviews) + } + + const handleUpload = async () => { + if (!selectedFiles.length) return + + setIsUploading(true) + setError(null) + setProgress(0) + + const uploadedUrls: string[] = [] + + try { + // Upload one by one to prevent timeout and body size limits + for (let i = 0; i < selectedFiles.length; i++) { + const file = selectedFiles[i] + const fd = new FormData() + fd.append('file', file) + fd.append('folder', folderName) + + const res = await uploadImageAction(fd) + if (res.success && res.url) { + uploadedUrls.push(res.url) + } else { + throw new Error(res.error || `${file.name} yüklenemedi.`) + } + + setProgress(i + 1) + } + + onUploadComplete(uploadedUrls) + setOpen(false) + resetState() + } catch (err: any) { + setError(err.message || "Yükleme sırasında bir hata oluştu.") + setIsUploading(false) + } + } + + return ( + + + + {triggerText} + + } /> + + + + Resim Yükle + + {multiple ? "Birden fazla resim seçip yükleyebilirsiniz." : "Odanın ana görselini seçin."} + + + +
+ {!isUploading && ( +
+ + +
+ )} + + {error && ( +
+ + {error} +
+ )} + + {previews.length > 0 && ( +
+

Seçilen Resimler ({selectedFiles.length})

+
+ {previews.map((preview, idx) => ( +
+ preview + {!isUploading && ( + + )} + {isUploading && idx < progress && ( +
+ +
+ )} + {isUploading && idx === progress && ( +
+ +
+ )} +
+ ))} +
+
+ )} + + {isUploading && ( +
+
+ Yükleniyor... + {progress} / {selectedFiles.length} +
+
+
+
+
+ )} +
+ + + + + + +
+ ) +} diff --git a/components/admin/RoomForm.tsx b/components/admin/RoomForm.tsx new file mode 100644 index 0000000..da90362 --- /dev/null +++ b/components/admin/RoomForm.tsx @@ -0,0 +1,253 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { createRoom, updateRoom, RoomInput } from '@/lib/actions/room' +import { uploadImageAction } from '@/lib/actions/upload' +import { Loader2 } from 'lucide-react' +import { ImageUploadModal } from '@/components/admin/ImageUploadModal' + +interface RoomFormProps { + initialData?: RoomInput & { id: string } +} + +export function RoomForm({ initialData }: RoomFormProps) { + const router = useRouter() + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const [formData, setFormData] = useState(initialData || { + slug: '', + type: 'STUDIO_1_0', + nameTr: '', + nameEn: '', + nameDe: '', + descriptionTr: '', + descriptionEn: '', + descriptionDe: '', + capacity: 2, + price: null, + imageUrl: '', + images: [], + amenities: [], + available: true, + featured: false, + }) + + // Helper for comma-separated arrays + const handleArrayChange = (field: 'images' | 'amenities', value: string) => { + const arrayValue = value.split(',').map(s => s.trim()).filter(Boolean) + setFormData(prev => ({ ...prev, [field]: arrayValue })) + } + + const handleMainImageComplete = (urls: string[]) => { + if (urls.length > 0) { + setFormData(prev => ({ ...prev, imageUrl: urls[0] })) + } + } + + const handleGalleryComplete = (urls: string[]) => { + if (urls.length > 0) { + setFormData(prev => ({ ...prev, images: [...prev.images, ...urls] })) + } + } + + const removeGalleryImage = (index: number) => { + setFormData(prev => ({ + ...prev, + images: prev.images.filter((_, i) => i !== index) + })) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError(null) + + try { + let result + if (initialData?.id) { + result = await updateRoom(initialData.id, formData) + } else { + result = await createRoom(formData) + } + + if (result?.success) { + router.push('/admin/rooms') + router.refresh() + } else { + setError(result?.error || 'Bir hata oluştu.') + } + } catch (err) { + setError('Bir hata oluştu.') + } finally { + setLoading(false) + } + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + setFormData(prev => ({ ...prev, slug: e.target.value }))} + placeholder="ornek-oda-1" + /> +
+ +
+ + setFormData(prev => ({ ...prev, nameTr: e.target.value }))} /> +
+
+ + setFormData(prev => ({ ...prev, nameEn: e.target.value }))} /> +
+
+ + setFormData(prev => ({ ...prev, nameDe: e.target.value }))} /> +
+ +
+ + setFormData(prev => ({ ...prev, capacity: parseInt(e.target.value) || 0 }))} /> +
+
+ + setFormData(prev => ({ ...prev, price: e.target.value ? parseInt(e.target.value) : null }))} /> +
+ +
+ +