first commit

This commit is contained in:
2026-08-05 19:40:55 +03:00
commit 3952b61edf
55 changed files with 10964 additions and 0 deletions
+61
View File
@@ -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>
)
}
+66
View File
@@ -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>
)
}
+82
View File
@@ -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>
)
}
+63
View File
@@ -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>
)
}
+72
View File
@@ -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>
)
}
+42
View File
@@ -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>
)
}
+31
View File
@@ -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>
)
}
+21
View File
@@ -0,0 +1,21 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { requireAdmin } from '@/lib/auth-helpers'
import { MOCK_MESSAGES } from '@/lib/mock'
const USE_MOCK = process.env.USE_MOCK === 'true'
export async function GET() {
try {
await requireAdmin()
const data = USE_MOCK
? MOCK_MESSAGES
: await prisma.contactMessage.findMany({
where: { deletedAt: null },
orderBy: { createdAt: 'desc' },
})
return NextResponse.json({ data })
} catch {
return NextResponse.json({ error: 'Server error' }, { status: 500 })
}
}
+31
View File
@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { requireAdmin } from '@/lib/auth-helpers'
import { MOCK_UNITS } from '@/lib/mock'
import { UnitSchema } from '@/lib/validations'
const USE_MOCK = process.env.USE_MOCK === 'true'
export async function GET() {
try {
const data = USE_MOCK
? MOCK_UNITS
: await prisma.unit.findMany({ where: { deletedAt: null }, orderBy: { order: 'asc' } })
return NextResponse.json({ data })
} catch {
return NextResponse.json({ error: 'Server error' }, { status: 500 })
}
}
export async function POST(req: NextRequest) {
try {
await requireAdmin()
const body = await req.json()
const parsed = UnitSchema.safeParse(body)
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })
const record = await prisma.unit.create({ data: parsed.data })
return NextResponse.json({ data: record }, { status: 201 })
} catch {
return NextResponse.json({ error: 'Server error' }, { status: 500 })
}
}
+9
View File
@@ -0,0 +1,9 @@
import { NextResponse } from 'next/server'
// NextAuth kaldırıldı
export async function GET() {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
export async function POST() {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
+34
View File
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { ContactSchema } from '@/lib/validations'
export async function POST(req: NextRequest) {
try {
const body = await req.json()
const parsed = ContactSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })
}
const message = await prisma.contactMessage.create({
data: parsed.data,
})
return NextResponse.json({ data: message }, { status: 201 })
} catch (error) {
console.error('Contact form error:', error)
return NextResponse.json({ error: 'Server error' }, { status: 500 })
}
}
export async function GET() {
try {
const messages = await prisma.contactMessage.findMany({
where: { deletedAt: null },
orderBy: { createdAt: 'desc' },
})
return NextResponse.json({ data: messages })
} catch {
return NextResponse.json({ error: 'Server error' }, { status: 500 })
}
}
+27
View File
@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from 'next/server'
import { uploadImage, deleteImage } from '@/lib/cloudinary'
import { requireAdmin } from '@/lib/auth-helpers'
export async function POST(req: NextRequest) {
try {
await requireAdmin()
const { file, folder = 'vesta-mugla' } = await req.json()
if (!file) return NextResponse.json({ error: 'File required' }, { status: 400 })
const result = await uploadImage(file, folder)
return NextResponse.json({ data: result })
} catch (error) {
console.error('Upload error:', error)
return NextResponse.json({ error: 'Upload failed' }, { status: 500 })
}
}
export async function DELETE(req: NextRequest) {
try {
await requireAdmin()
const { publicId } = await req.json()
await deleteImage(publicId)
return NextResponse.json({ success: true })
} catch {
return NextResponse.json({ error: 'Delete failed' }, { status: 500 })
}
}
+82
View File
@@ -0,0 +1,82 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Inter:wght@300;400;500;600&display=swap');
:root {
--font-sans: 'Inter', system-ui, sans-serif;
--font-serif: 'Cormorant Garamond', Georgia, serif;
--font-display: 'Inter', system-ui, sans-serif;
--radius: 0.75rem;
/* shadcn/ui CSS variables */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-sans);
background-color: #1a1a1a;
color: #1a1a1a;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Serif font utility */
.font-serif {
font-family: var(--font-serif) !important;
}
/* Smooth scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #1a1a1a;
}
::-webkit-scrollbar-thumb {
background: #3a3a3a;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* GSAP kullanıldığı için overflow-x gizle */
body {
overflow-x: hidden;
}
/* Subtle text selection */
::selection {
background: rgba(139, 115, 85, 0.3);
color: inherit;
}
+9
View File
@@ -0,0 +1,9 @@
import './globals.css'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html suppressHydrationWarning>
<body suppressHydrationWarning>{children}</body>
</html>
)
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation'
export default function RootPage() {
redirect('/tr')
}