first commit
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import Link from 'next/link'
|
||||
import { LayoutDashboard, Home, MessageSquare, Images, LogOut } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
const navItems = [
|
||||
{ href: '/tr/admin', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '/tr/admin/units', label: 'Daireler', icon: Home },
|
||||
{ href: '/tr/admin/messages', label: 'Mesajlar', icon: MessageSquare },
|
||||
{ href: '/tr/admin/gallery', label: 'Galeri', icon: Images },
|
||||
]
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-vesta-dark flex flex-col min-h-screen fixed left-0 top-0 bottom-0">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<Image
|
||||
src="https://cdn.prod.website-files.com/6693a42300f08d15d3514511/6694e59f468a02af6267826d_H%20FULL%20LOGO%20-%20WHITE.png"
|
||||
alt="Vesta Muğla"
|
||||
width={130}
|
||||
height={32}
|
||||
className="h-7 w-auto object-contain"
|
||||
/>
|
||||
<p className="text-white/30 text-xs mt-1">Admin Paneli</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-white/60 hover:text-white hover:bg-white/10 transition-colors text-sm"
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/10">
|
||||
<Link
|
||||
href="/tr"
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-white/40 hover:text-white/60 transition-colors text-sm"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Siteye Dön
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main */}
|
||||
<main className="ml-64 flex-1 p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Image from 'next/image'
|
||||
|
||||
export default function AdminLoginPage() {
|
||||
const router = useRouter()
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
// Basit şifre kontrolü — ileride API'ye bağlanabilir
|
||||
const adminPass = process.env.NEXT_PUBLIC_ADMIN_PASSWORD || 'vesta2025'
|
||||
if (password === adminPass) {
|
||||
sessionStorage.setItem('admin_auth', '1')
|
||||
router.push('/tr/admin')
|
||||
} else {
|
||||
setError('Şifre hatalı.')
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-vesta-dark flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="text-center mb-8">
|
||||
<Image
|
||||
src="https://cdn.prod.website-files.com/6693a42300f08d15d3514511/6694e59f468a02af6267826d_H%20FULL%20LOGO%20-%20WHITE.png"
|
||||
alt="Vesta Muğla"
|
||||
width={160}
|
||||
height={40}
|
||||
className="h-8 w-auto object-contain mx-auto mb-2"
|
||||
/>
|
||||
<p className="text-white/40 text-sm">Admin Paneli</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="bg-white/5 border border-white/10 rounded-2xl p-8 space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Şifre"
|
||||
required
|
||||
className="w-full bg-white/10 border border-white/10 rounded-xl px-4 py-3 text-white placeholder:text-white/30 focus:outline-none focus:ring-2 focus:ring-vesta-earth/50"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">{error}</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-vesta-earth text-white py-3 rounded-xl text-sm font-medium hover:bg-vesta-earth/80 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Giriş yapılıyor...' : 'Giriş Yap'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { prisma } from '@/lib/db'
|
||||
import { MOCK_MESSAGES } from '@/lib/mock'
|
||||
import { Mail, Phone, Clock } from 'lucide-react'
|
||||
|
||||
const USE_MOCK = process.env.USE_MOCK === 'true'
|
||||
|
||||
async function getMessages() {
|
||||
if (USE_MOCK) return MOCK_MESSAGES
|
||||
return prisma.contactMessage.findMany({
|
||||
where: { deletedAt: null },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
}
|
||||
|
||||
export default async function MessagesPage() {
|
||||
const messages = await getMessages()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Mesajlar</h1>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
{messages.filter((m) => !m.read).length} okunmamış mesaj
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`bg-white rounded-2xl p-6 shadow-sm border-l-4 ${
|
||||
!msg.read ? 'border-vesta-earth' : 'border-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{msg.fullName}</h3>
|
||||
<div className="flex items-center gap-4 mt-1">
|
||||
<a
|
||||
href={`mailto:${msg.email}`}
|
||||
className="flex items-center gap-1 text-gray-400 hover:text-gray-600 text-sm"
|
||||
>
|
||||
<Mail className="w-3.5 h-3.5" />
|
||||
{msg.email}
|
||||
</a>
|
||||
{msg.phone && (
|
||||
<a
|
||||
href={`tel:${msg.phone}`}
|
||||
className="flex items-center gap-1 text-gray-400 hover:text-gray-600 text-sm"
|
||||
>
|
||||
<Phone className="w-3.5 h-3.5" />
|
||||
{msg.phone}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-gray-300 text-xs">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{new Date(msg.createdAt).toLocaleDateString('tr-TR')}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">{msg.message}</p>
|
||||
{!msg.read && (
|
||||
<span className="inline-block mt-3 text-xs bg-vesta-earth/10 text-vesta-earth px-2 py-0.5 rounded-full">
|
||||
Yeni
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center py-16 text-gray-300">
|
||||
<Mail className="w-10 h-10 mx-auto mb-3 opacity-30" />
|
||||
<p>Henüz mesaj yok</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Home, MessageSquare, Images, Users } from 'lucide-react'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { MOCK_UNITS, MOCK_MESSAGES, MOCK_GALLERY } from '@/lib/mock'
|
||||
|
||||
const USE_MOCK = process.env.USE_MOCK === 'true'
|
||||
|
||||
async function getStats() {
|
||||
if (USE_MOCK) {
|
||||
return {
|
||||
units: MOCK_UNITS.length,
|
||||
messages: MOCK_MESSAGES.length,
|
||||
unread: MOCK_MESSAGES.filter((m) => !m.read).length,
|
||||
gallery: MOCK_GALLERY.length,
|
||||
}
|
||||
}
|
||||
const [units, messages, gallery] = await Promise.all([
|
||||
prisma.unit.count({ where: { deletedAt: null } }),
|
||||
prisma.contactMessage.count({ where: { deletedAt: null } }),
|
||||
prisma.gallery.count({ where: { deletedAt: null } }),
|
||||
])
|
||||
const unread = await prisma.contactMessage.count({ where: { deletedAt: null, read: false } })
|
||||
return { units, messages, unread, gallery }
|
||||
}
|
||||
|
||||
export default async function AdminDashboard() {
|
||||
const stats = await getStats()
|
||||
|
||||
const cards = [
|
||||
{ label: 'Daire Tipi', value: stats.units, icon: Home, color: 'bg-blue-50 text-blue-600' },
|
||||
{
|
||||
label: 'Mesaj',
|
||||
value: stats.messages,
|
||||
sub: `${stats.unread} okunmamış`,
|
||||
icon: MessageSquare,
|
||||
color: 'bg-amber-50 text-amber-600',
|
||||
},
|
||||
{ label: 'Galeri', value: stats.gallery, icon: Images, color: 'bg-green-50 text-green-600' },
|
||||
{ label: 'Kullanıcı', value: 1, icon: Users, color: 'bg-purple-50 text-purple-600' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-2">Dashboard</h1>
|
||||
<p className="text-gray-400 text-sm mb-8">Vesta Muğla yönetim paneline hoş geldiniz.</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon
|
||||
return (
|
||||
<div key={card.label} className="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center mb-4 ${card.color}`}>
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-gray-900">{card.value}</p>
|
||||
<p className="text-gray-400 text-sm mt-1">{card.label}</p>
|
||||
{card.sub && <p className="text-xs text-amber-500 mt-1">{card.sub}</p>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { prisma } from '@/lib/db'
|
||||
import { MOCK_UNITS } from '@/lib/mock'
|
||||
import { BedDouble, Maximize2, CheckCircle2, XCircle } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
const USE_MOCK = process.env.USE_MOCK === 'true'
|
||||
|
||||
async function getUnits() {
|
||||
if (USE_MOCK) return MOCK_UNITS
|
||||
return prisma.unit.findMany({ where: { deletedAt: null }, orderBy: { order: 'asc' } })
|
||||
}
|
||||
|
||||
export default async function UnitsAdminPage() {
|
||||
const units = await getUnits()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Daireler</h1>
|
||||
<p className="text-gray-400 text-sm mt-1">{units.length} daire tipi</p>
|
||||
</div>
|
||||
<button className="bg-vesta-dark text-white px-5 py-2.5 rounded-xl text-sm hover:bg-vesta-forest transition-colors">
|
||||
+ Yeni Daire
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{units.map((unit) => (
|
||||
<div key={unit.id} className="bg-white rounded-2xl p-5 shadow-sm flex items-center gap-6">
|
||||
{unit.imageUrl && (
|
||||
<div className="w-20 h-20 rounded-xl overflow-hidden shrink-0">
|
||||
<Image
|
||||
src={unit.imageUrl}
|
||||
alt={unit.typeTr}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h3 className="font-medium text-gray-900">{unit.typeTr}</h3>
|
||||
{unit.available ? (
|
||||
<span className="flex items-center gap-1 text-green-600 text-xs">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" /> Müsait
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-gray-400 text-xs">
|
||||
<XCircle className="w-3.5 h-3.5" /> Satıldı
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-gray-400 text-sm">
|
||||
<span className="flex items-center gap-1">
|
||||
<Maximize2 className="w-3.5 h-3.5" /> {unit.size} m²
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<BedDouble className="w-3.5 h-3.5" /> {unit.rooms} oda
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-gray-400 hover:text-gray-600 text-sm px-3 py-1.5 border border-gray-200 rounded-lg transition-colors">
|
||||
Düzenle
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import { getMessages } from 'next-intl/server'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { routing } from '@/i18n/routing'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Vesta Muğla | Evinizde Doğa, Doğada Huzur',
|
||||
description:
|
||||
"Muğla'nın doğal güzellikleri içinde yer alan lüks rezidans projesi. Modern tasarımlar, sosyal alanlar ve doğayla iç içe huzurlu bir yaşam.",
|
||||
openGraph: {
|
||||
title: 'Vesta Muğla',
|
||||
description: "Muğla'nın orman içinde lüks konut projesi",
|
||||
images: ['https://images.unsplash.com/photo-1580587771525-78b9dba3b914?w=1200&q=80'],
|
||||
},
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }))
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
|
||||
if (!routing.locales.includes(locale as 'tr' | 'en')) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const messages = await getMessages()
|
||||
|
||||
return (
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import Navigation from '@/components/Navigation'
|
||||
import CloudRevealSection from '@/components/sections/CloudRevealSection'
|
||||
import AboutSection from '@/components/sections/AboutSection'
|
||||
import AmenitiesSection from '@/components/sections/AmenitiesSection'
|
||||
import LocationSection from '@/components/sections/LocationSection'
|
||||
import UnitsSection from '@/components/sections/UnitsSection'
|
||||
import GallerySection from '@/components/sections/GallerySection'
|
||||
import ContactSection from '@/components/sections/ContactSection'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
export default async function HomePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Navigation locale={locale} />
|
||||
<CloudRevealSection />
|
||||
<AboutSection />
|
||||
<AmenitiesSection />
|
||||
<LocationSection />
|
||||
<UnitsSection />
|
||||
<GallerySection />
|
||||
<ContactSection />
|
||||
<Footer locale={locale} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user