first commit
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/components/Footer'
|
||||
import ListingCard from '@/components/ListingCard'
|
||||
import { notFound } from 'next/navigation'
|
||||
import Image from 'next/image'
|
||||
import { Phone, Globe, MapPin, Clock, Star, MessageSquare } from 'lucide-react'
|
||||
|
||||
interface DetailPageProps {
|
||||
params: Promise<{ locale: string; category: string; slug: string }>
|
||||
}
|
||||
|
||||
export default async function ListingDetailPage({ params }: DetailPageProps) {
|
||||
const { locale, category, slug } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const t = await getTranslations('detail')
|
||||
|
||||
const listing = await mockDb.getListingBySlug(slug)
|
||||
if (!listing) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
// Fetch similar listings in same category (limit to 3, excluding current)
|
||||
const allCategoryListings = await mockDb.getListings({
|
||||
categoryId: listing.categoryId
|
||||
})
|
||||
const relatedListings = allCategoryListings
|
||||
.filter(l => l.id !== listing.id)
|
||||
.slice(0, 3)
|
||||
|
||||
// Localized values
|
||||
const name =
|
||||
locale === 'ru'
|
||||
? listing.nameRu
|
||||
: locale === 'en'
|
||||
? listing.nameEn
|
||||
: listing.nameTr
|
||||
|
||||
const description =
|
||||
locale === 'ru'
|
||||
? listing.descriptionRu
|
||||
: locale === 'en'
|
||||
? listing.descriptionEn
|
||||
: listing.descriptionTr
|
||||
|
||||
const priceSymbols = '₺'.repeat(listing.priceRange)
|
||||
|
||||
// Format WhatsApp Link
|
||||
const getWhatsAppLink = (number: string) => {
|
||||
// Clean spaces, parenthesis, plus sign
|
||||
const cleanNum = number.replace(/\D/g, '')
|
||||
return `https://wa.me/${cleanNum}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||||
<Navbar />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 flex-1">
|
||||
|
||||
{/* Breadcrumb / Category Link */}
|
||||
<div className="mb-6 text-xs font-mono uppercase tracking-wider text-shutter flex items-center gap-2">
|
||||
<span className="hover:text-turquoise transition-colors">marmaris local</span>
|
||||
<span>/</span>
|
||||
<span className="hover:text-turquoise transition-colors">
|
||||
{locale === 'ru' ? listing.category?.nameRu : locale === 'en' ? listing.category?.nameEn : listing.category?.nameTr}
|
||||
</span>
|
||||
<span>/</span>
|
||||
<span className="text-pine font-bold">{name}</span>
|
||||
</div>
|
||||
|
||||
{/* Hero Details Block */}
|
||||
<div className="bg-paper rounded-3xl border border-pine/8 p-6 sm:p-10 shadow-sm mb-10 space-y-8">
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-10">
|
||||
|
||||
{/* Left: Gallery Panel */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="aspect-[16/10] w-full relative rounded-2xl overflow-hidden bg-stone-deep shadow-sm">
|
||||
<Image
|
||||
src={listing.images && listing.images.length > 0 ? listing.images[0].url : 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&auto=format&fit=crop&q=80'}
|
||||
alt={name}
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Thumbnails if multiple images exist */}
|
||||
{listing.images && listing.images.length > 1 && (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{listing.images.slice(1, 5).map((img, idx) => (
|
||||
<div key={img.id} className="aspect-square relative rounded-xl overflow-hidden bg-stone-deep border border-pine/5 shadow-sm">
|
||||
<Image
|
||||
src={img.url}
|
||||
alt={`${name} thumbnail ${idx + 1}`}
|
||||
fill
|
||||
className="object-cover hover:scale-105 transition duration-300"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: Info Panel */}
|
||||
<div className="flex-1 flex flex-col justify-between space-y-6">
|
||||
|
||||
{/* Badge & Title */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-2.5 items-center">
|
||||
<span className="text-[10px] font-mono text-bougainvillea font-bold uppercase tracking-wider bg-bougainvillea/5 border border-bougainvillea/10 px-2.5 py-1 rounded-full">
|
||||
{locale === 'ru' ? listing.category?.nameRu : locale === 'en' ? listing.category?.nameEn : listing.category?.nameTr}
|
||||
</span>
|
||||
|
||||
{listing.isLocalApproved && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-mono text-turquoise font-bold uppercase tracking-wider bg-turquoise/5 border border-turquoise/10 px-2.5 py-1 rounded-full">
|
||||
★ {t('approved')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="font-heading font-extrabold text-2xl sm:text-4xl text-pine leading-tight lowercase">
|
||||
{name}
|
||||
</h1>
|
||||
|
||||
{/* Stars and Price level */}
|
||||
<div className="flex items-center gap-6 font-mono text-sm border-b border-dashed border-pine/8 pb-4">
|
||||
{listing.rating && (
|
||||
<div className="flex items-center gap-1.5 text-gold font-bold">
|
||||
<Star className="w-4 h-4 fill-gold stroke-gold" />
|
||||
<span>{t('rating')}: {listing.rating.toFixed(1)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-pine font-semibold">
|
||||
<span>{t('price')}: {priceSymbols}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="text-ink/80 text-sm leading-relaxed font-medium">
|
||||
{description}
|
||||
</div>
|
||||
|
||||
{/* Contact Actions */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-heading font-bold text-xs uppercase tracking-wider text-shutter">{t('contact')}</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{listing.phone && (
|
||||
<a
|
||||
href={`tel:${listing.phone}`}
|
||||
className="flex items-center justify-center gap-2 bg-pine hover:bg-pine/90 text-stone font-bold text-xs py-3.5 px-4 rounded-xl transition"
|
||||
>
|
||||
<Phone className="w-4 h-4" />
|
||||
{t('call')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{listing.whatsapp && (
|
||||
<a
|
||||
href={getWhatsAppLink(listing.whatsapp)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 bg-turquoise hover:bg-turquoise/90 text-paper font-bold text-xs py-3.5 px-4 rounded-xl transition"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
{t('whatsapp')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* External links */}
|
||||
<div className="flex gap-4 pt-2 text-xs font-semibold text-shutter">
|
||||
{listing.website && (
|
||||
<a href={listing.website} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 hover:text-turquoise transition">
|
||||
<Globe className="w-4 h-4" />
|
||||
{t('website')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{listing.instagram && (
|
||||
<a href={listing.instagram} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 hover:text-turquoise transition">
|
||||
<Globe className="w-4 h-4" />
|
||||
{t('instagram')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Details footer (Hours & Location map) */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 pt-8 border-t border-dashed border-pine/12">
|
||||
|
||||
{/* Address */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 font-heading font-bold text-xs text-pine uppercase tracking-wider">
|
||||
<MapPin className="w-4 h-4 text-turquoise" />
|
||||
<span>{t('address')}</span>
|
||||
</div>
|
||||
<p className="text-xs text-ink/75 font-medium leading-relaxed">
|
||||
{listing.address}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Hours */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 font-heading font-bold text-xs text-pine uppercase tracking-wider">
|
||||
<Clock className="w-4 h-4 text-turquoise" />
|
||||
<span>{t('hours')}</span>
|
||||
</div>
|
||||
<div className="text-xs text-ink/75 font-medium">
|
||||
{listing.openingHours ? (
|
||||
<p>{(listing.openingHours as any).all || t('noHours')}</p>
|
||||
) : (
|
||||
<p>{t('noHours')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Map Frame */}
|
||||
{listing.latitude && listing.longitude && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 font-heading font-bold text-xs text-pine uppercase tracking-wider">
|
||||
<Globe className="w-4 h-4 text-turquoise" />
|
||||
<span>{t('location')}</span>
|
||||
</div>
|
||||
<div className="rounded-xl overflow-hidden border border-pine/8 aspect-[16/10] sm:aspect-auto sm:h-36">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
marginHeight={0}
|
||||
marginWidth={0}
|
||||
src={`https://maps.google.com/maps?q=${listing.latitude},${listing.longitude}&t=&z=15&ie=UTF8&iwloc=&output=embed`}
|
||||
className="w-full h-full shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Related Section */}
|
||||
{relatedListings.length > 0 && (
|
||||
<div className="space-y-6 pt-10">
|
||||
<h3 className="text-xl font-heading font-extrabold text-pine lowercase">
|
||||
{t('related')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{relatedListings.map((listingItem) => (
|
||||
<ListingCard key={listingItem.id} listing={listingItem} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
|
||||
export default async function AdminCategoriesPage() {
|
||||
const categories = await mockDb.getCategories()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">kategoriler</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
Sistemde listelenen işletmelerin sınıflandırıldığı ana kategoriler.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-paper border border-pine/8 rounded-2xl shadow-sm overflow-hidden max-w-4xl">
|
||||
<table className="min-w-full divide-y divide-pine/8 text-sm">
|
||||
<thead className="bg-stone-deep/40 text-shutter font-mono text-[10px] uppercase tracking-wider">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left">ID</th>
|
||||
<th className="px-6 py-4 text-left">Slug</th>
|
||||
<th className="px-6 py-4 text-left">Adı (TR)</th>
|
||||
<th className="px-6 py-4 text-left">Name (EN)</th>
|
||||
<th className="px-6 py-4 text-left">Имя (RU)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dashed divide-pine/8 text-ink/85 font-medium">
|
||||
{categories.map((cat) => (
|
||||
<tr key={cat.id} className="hover:bg-stone/20 transition duration-150">
|
||||
<td className="px-6 py-4 font-mono text-xs text-shutter">{cat.id}</td>
|
||||
<td className="px-6 py-4 font-mono text-xs font-bold text-turquoise">{cat.slug}</td>
|
||||
<td className="px-6 py-4 font-heading font-bold text-pine lowercase text-sm">{cat.nameTr}</td>
|
||||
<td className="px-6 py-4 text-xs">{cat.nameEn}</td>
|
||||
<td className="px-6 py-4 text-xs">{cat.nameRu}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
'use client'
|
||||
|
||||
import { signOut } from 'next-auth/react'
|
||||
import { Link, usePathname } from '@/i18n/routing'
|
||||
import { LayoutDashboard, FileText, Inbox, ClipboardList, Map, 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: 'Mekanlar', href: '/admin/listings', icon: ClipboardList },
|
||||
{ name: 'Başvurular', href: '/admin/submissions', icon: FileText },
|
||||
{ name: 'Mesajlar', href: '/admin/messages', icon: Inbox },
|
||||
{ name: 'Kategoriler', href: '/admin/categories', icon: LayoutDashboard },
|
||||
{ name: 'Mahalleler', href: '/admin/neighborhoods', icon: Map },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone text-ink flex font-sans">
|
||||
{/* Mobile sidebar backdrop */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-pine/80 lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className={`
|
||||
fixed inset-y-0 left-0 z-50 w-64 bg-pine border-r border-white/10
|
||||
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 justify-between">
|
||||
<div>
|
||||
{/* Sidebar Brand Header */}
|
||||
<div className="h-20 flex items-center px-6 border-b border-white/10 gap-3">
|
||||
<Link href="/" className="flex items-center gap-2.5">
|
||||
<div className="w-9 h-9 rounded-full border border-stone/30 flex items-center justify-center relative bg-paper shrink-0">
|
||||
<div className="absolute inset-[2px] rounded-full border border-dashed border-turquoise/50" />
|
||||
<span className="font-heading font-extrabold text-pine text-xs tracking-tighter">ML</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-heading font-extrabold text-sm text-stone tracking-tight leading-none lowercase">
|
||||
marmaris <span className="text-turquoise">local</span>
|
||||
</h1>
|
||||
<span className="text-[8px] font-mono text-shutter tracking-wider uppercase">backoffice</span>
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
className="ml-auto lg:hidden text-stone/70 hover:text-stone"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation links */}
|
||||
<nav className="px-3 py-6 space-y-1.5 overflow-y-auto">
|
||||
{navigation.map((item) => {
|
||||
const isActive = pathname === item.href || (item.href !== '/admin' && pathname.startsWith(item.href))
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`
|
||||
flex items-center px-4 py-3 text-xs font-semibold rounded-xl transition-all duration-150
|
||||
${isActive
|
||||
? 'bg-white/5 border-l-4 border-turquoise text-stone pl-3'
|
||||
: 'text-stone/75 hover:bg-white/5 hover:text-stone'}
|
||||
`}
|
||||
>
|
||||
<item.icon className={`mr-3 flex-shrink-0 h-4.5 w-4.5 ${isActive ? 'text-turquoise' : 'text-stone/50'}`} />
|
||||
{item.name}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Logout Action */}
|
||||
<div className="p-4 border-t border-white/10">
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: '/' })}
|
||||
className="flex w-full items-center px-4 py-3 text-xs font-semibold text-red-400 hover:text-red-300 rounded-xl hover:bg-red-950/20 transition-colors"
|
||||
>
|
||||
<LogOut className="mr-3 h-4.5 w-4.5 text-red-400/70" />
|
||||
Çıkış Yap
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
{/* Mobile Header Bar */}
|
||||
<header className="h-16 flex items-center lg:hidden bg-pine border-b border-white/10 px-4 shrink-0 text-stone">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="text-stone hover:text-turquoise focus:outline-none"
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
</button>
|
||||
<div className="ml-4 flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full border border-stone/20 flex items-center justify-center relative bg-paper shrink-0">
|
||||
<span className="font-heading font-extrabold text-pine text-[10px] tracking-tighter">ML</span>
|
||||
</div>
|
||||
<span className="text-sm font-heading font-bold lowercase">marmaris local <span className="text-turquoise">backoffice</span></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Area */}
|
||||
<main className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { createOrUpdateListingAction } from '@/app/actions'
|
||||
import { ArrowLeft, Save } from 'lucide-react'
|
||||
import { Link } from '@/i18n/routing'
|
||||
|
||||
interface Option {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
listing: any | null
|
||||
categories: Option[]
|
||||
neighborhoods: Option[]
|
||||
}
|
||||
|
||||
export default function ListingForm({ listing, categories, neighborhoods }: FormProps) {
|
||||
const router = useRouter()
|
||||
const [activeTab, setActiveTab] = useState<'tr' | 'en' | 'ru'>('tr')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setSuccess(false)
|
||||
|
||||
const formData = new FormData(e.currentTarget)
|
||||
if (listing) {
|
||||
formData.append('id', listing.id)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await createOrUpdateListingAction(formData)
|
||||
if (res.error) {
|
||||
setError(res.error)
|
||||
} else {
|
||||
setSuccess(true)
|
||||
setTimeout(() => {
|
||||
router.push('/admin/listings')
|
||||
router.refresh()
|
||||
}, 1500)
|
||||
}
|
||||
} catch (err) {
|
||||
setError('İşlem sırasında bir hata oluştu.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const initialImages = listing?.images || []
|
||||
const img1 = initialImages[0]?.url || ''
|
||||
const img2 = initialImages[1]?.url || ''
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-bougainvillea/10 border border-bougainvillea/25 text-bougainvillea p-4 rounded-xl text-xs font-semibold">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="bg-turquoise/10 border border-turquoise/25 text-turquoise p-4 rounded-xl text-xs font-bold">
|
||||
Mekan başarıyla kaydedildi! Yönlendiriliyorsunuz...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Language tabs */}
|
||||
<div className="border-b border-pine/8">
|
||||
<div className="flex gap-2">
|
||||
{(['tr', 'en', 'ru'] as const).map((lang) => {
|
||||
const label = lang === 'tr' ? '🇹🇷 Türkçe' : lang === 'en' ? '🇬🇧 English' : '🇷🇺 Русский'
|
||||
const isTabActive = activeTab === lang
|
||||
return (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(lang)}
|
||||
className={`py-3 px-4 font-heading font-bold text-xs border-b-2 transition lowercase ${
|
||||
isTabActive
|
||||
? 'border-turquoise text-turquoise'
|
||||
: 'border-transparent text-shutter hover:text-pine'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Multilingual input fields */}
|
||||
<div className="space-y-4">
|
||||
{/* TR Tab */}
|
||||
{activeTab === 'tr' && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Mekan Adı (TR) *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="nameTr"
|
||||
required
|
||||
defaultValue={listing?.nameTr || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
placeholder="Örn: İskele Balık Ocakbaşı"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Açıklama (TR) *</label>
|
||||
<textarea
|
||||
name="descriptionTr"
|
||||
required
|
||||
rows={4}
|
||||
defaultValue={listing?.descriptionTr || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
placeholder="Türkçe tanıtım metni..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* EN Tab */}
|
||||
{activeTab === 'en' && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Mekan Adı (EN) *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="nameEn"
|
||||
required
|
||||
defaultValue={listing?.nameEn || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
placeholder="Örn: Iskele Fish & Grill"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Açıklama (EN) *</label>
|
||||
<textarea
|
||||
name="descriptionEn"
|
||||
required
|
||||
rows={4}
|
||||
defaultValue={listing?.descriptionEn || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
placeholder="English description..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RU Tab */}
|
||||
{activeTab === 'ru' && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Mekan Adı (RU) *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="nameRu"
|
||||
required
|
||||
defaultValue={listing?.nameRu || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
placeholder="Örn: Рыбный Гриль Искеле"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Açıklama (RU) *</label>
|
||||
<textarea
|
||||
name="descriptionRu"
|
||||
required
|
||||
rows={4}
|
||||
defaultValue={listing?.descriptionRu || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
placeholder="Russian description..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr className="border-dashed border-pine/8" />
|
||||
|
||||
{/* Global fields */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Slug */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">URL Slug *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="slug"
|
||||
required
|
||||
defaultValue={listing?.slug || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
|
||||
placeholder="iskele-balik-ocakbasi"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Kategori *</label>
|
||||
<select
|
||||
name="categoryId"
|
||||
required
|
||||
defaultValue={listing?.categoryId || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink appearance-none"
|
||||
>
|
||||
<option value="">Kategori seçin...</option>
|
||||
{categories.map((c) => (
|
||||
<option key={c.value} value={c.value}>
|
||||
{c.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Neighborhoods */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Mahalle/Bölge *</label>
|
||||
<select
|
||||
name="neighborhoodId"
|
||||
required
|
||||
defaultValue={listing?.neighborhoodId || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink appearance-none"
|
||||
>
|
||||
<option value="">Mahalle seçin...</option>
|
||||
{neighborhoods.map((n) => (
|
||||
<option key={n.value} value={n.value}>
|
||||
{n.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Açık Adres *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
required
|
||||
defaultValue={listing?.address || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
placeholder="Yat Limanı No:12, Marmaris"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4">
|
||||
{/* Phone */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Telefon</label>
|
||||
<input
|
||||
type="text"
|
||||
name="phone"
|
||||
defaultValue={listing?.phone || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
|
||||
placeholder="+90 252..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Whatsapp */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">WhatsApp</label>
|
||||
<input
|
||||
type="text"
|
||||
name="whatsapp"
|
||||
defaultValue={listing?.whatsapp || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
|
||||
placeholder="+90 532..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Web Sitesi</label>
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
defaultValue={listing?.website || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Instagram */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Instagram</label>
|
||||
<input
|
||||
type="text"
|
||||
name="instagram"
|
||||
defaultValue={listing?.instagram || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
|
||||
placeholder="https://instagram.com/..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4">
|
||||
{/* Price range */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Fiyat Aralığı (1-3) *</label>
|
||||
<select
|
||||
name="priceRange"
|
||||
required
|
||||
defaultValue={listing?.priceRange || 2}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
>
|
||||
<option value="1">₺ (Ekonomik)</option>
|
||||
<option value="2">₺₺ (Orta)</option>
|
||||
<option value="3">₺₺₺ (Lüks)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Rating */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Puan (0.0 - 5.0)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="rating"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="5"
|
||||
defaultValue={listing?.rating || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
|
||||
placeholder="4.8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Latitude */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Enlem (Lat)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="latitude"
|
||||
step="0.0001"
|
||||
defaultValue={listing?.latitude || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
|
||||
placeholder="36.8524"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Longitude */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Boylam (Lng)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="longitude"
|
||||
step="0.0001"
|
||||
defaultValue={listing?.longitude || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
|
||||
placeholder="28.2741"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Opening hours */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Çalışma Saatleri</label>
|
||||
<input
|
||||
type="text"
|
||||
name="openingHours"
|
||||
defaultValue={listing?.openingHours?.all || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
placeholder="Örn: 12:00 - 00:00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Approved toggle */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Yerel Onaylı Mührü</label>
|
||||
<select
|
||||
name="isLocalApproved"
|
||||
defaultValue={listing?.isLocalApproved ? 'true' : 'false'}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
|
||||
>
|
||||
<option value="false">Normal Mekan</option>
|
||||
<option value="true">Yerel Onaylı (Mühürlü)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Upload fields */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 border-t border-dashed border-pine/8 pt-6">
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Görsel 1 (Dosya Yükle)</label>
|
||||
<input
|
||||
type="file"
|
||||
name="imageFile1"
|
||||
accept="image/*"
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink file:mr-3 file:py-1 file:px-2.5 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-pine file:text-stone hover:file:opacity-90 cursor-pointer"
|
||||
/>
|
||||
{img1 && (
|
||||
<div className="mt-2 text-xs flex items-center gap-2">
|
||||
<span className="text-shutter/65">Mevcut Görsel:</span>
|
||||
<a href={img1} target="_blank" rel="noopener noreferrer" className="text-turquoise hover:underline font-mono truncate max-w-xs">{img1}</a>
|
||||
<input type="hidden" name="imageUrl1" value={img1} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Görsel 2 (Dosya Yükle)</label>
|
||||
<input
|
||||
type="file"
|
||||
name="imageFile2"
|
||||
accept="image/*"
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink file:mr-3 file:py-1 file:px-2.5 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-pine file:text-stone hover:file:opacity-90 cursor-pointer"
|
||||
/>
|
||||
{img2 && (
|
||||
<div className="mt-2 text-xs flex items-center gap-2">
|
||||
<span className="text-shutter/65">Mevcut Görsel:</span>
|
||||
<a href={img2} target="_blank" rel="noopener noreferrer" className="text-turquoise hover:underline font-mono truncate max-w-xs">{img2}</a>
|
||||
<input type="hidden" name="imageUrl2" value={img2} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-4 border-t border-dashed border-pine/8">
|
||||
<Link
|
||||
href="/admin/listings"
|
||||
className="inline-flex items-center gap-1.5 px-5 py-3 border border-pine/20 rounded-xl text-xs font-bold text-pine hover:bg-stone/30 transition duration-150"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
İptal
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="inline-flex items-center gap-1.5 px-5 py-3 bg-turquoise hover:bg-turquoise/90 disabled:opacity-75 text-paper rounded-xl text-xs font-bold shadow-sm transition duration-150"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{loading ? 'Kaydediliyor...' : 'Kaydet'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import ListingForm from './ListingForm'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ locale: string; id: string }>
|
||||
}
|
||||
|
||||
export default async function AdminListingEditPage({ params }: Props) {
|
||||
const { locale, id } = await params
|
||||
|
||||
const categories = await mockDb.getCategories()
|
||||
const neighborhoods = await mockDb.getNeighborhoods()
|
||||
|
||||
let listing = null
|
||||
|
||||
if (id !== 'new') {
|
||||
listing = await mockDb.getListingById(id)
|
||||
if (!listing) {
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
||||
const categoryOptions = categories.map(c => ({
|
||||
value: c.id,
|
||||
label: c.nameTr
|
||||
}))
|
||||
|
||||
const neighborhoodOptions = neighborhoods.map(n => ({
|
||||
value: n.id,
|
||||
label: n.nameTr
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<div>
|
||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{id === 'new' ? 'yeni mekan ekle' : 'mekanı düzenle'}
|
||||
</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
{id === 'new'
|
||||
? 'Rehbere eklenecek yeni işletme bilgilerini girin.'
|
||||
: 'Mevcut mekan bilgilerini güncelleyin.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-paper border border-pine/8 rounded-3xl shadow-sm p-6 sm:p-8">
|
||||
<ListingForm
|
||||
listing={listing}
|
||||
categories={categoryOptions}
|
||||
neighborhoods={neighborhoodOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import { deleteListingAction } from '@/app/actions'
|
||||
import { Link } from '@/i18n/routing'
|
||||
import { Edit, Trash, Plus, CheckCircle } from 'lucide-react'
|
||||
|
||||
export default async function AdminListingsPage() {
|
||||
const listings = await mockDb.getListings()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">mekanlar</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
Marmaris Local rehberinde kayıtlı olan restoran, apart otel ve yerel hizmetlerin listesi.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/listings/new"
|
||||
className="inline-flex items-center gap-1.5 bg-turquoise hover:bg-turquoise/90 text-paper text-xs font-bold py-3 px-5 rounded-xl shadow-sm transition active:scale-95 duration-150"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Yeni Mekan Ekle
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-paper border border-pine/8 rounded-2xl shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-pine/8 text-sm">
|
||||
<thead className="bg-stone-deep/40 text-shutter font-mono text-[10px] uppercase tracking-wider">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left">Görsel</th>
|
||||
<th className="px-6 py-4 text-left">Mekan</th>
|
||||
<th className="px-6 py-4 text-left">Kategori & Konum</th>
|
||||
<th className="px-6 py-4 text-left">Fiyat & Puan</th>
|
||||
<th className="px-6 py-4 text-left">Mühür</th>
|
||||
<th className="px-6 py-4 text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dashed divide-pine/8 text-ink/80">
|
||||
{listings.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-12 text-center text-xs text-shutter font-medium">
|
||||
Henüz kayıtlı mekan bulunmamaktadır.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
listings.map((l) => {
|
||||
const mainImage = l.images && l.images.length > 0 ? l.images[0].url : 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&auto=format&fit=crop&q=80'
|
||||
return (
|
||||
<tr key={l.id} className="hover:bg-stone/20 transition duration-150">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="h-12 w-16 relative rounded-lg overflow-hidden bg-stone border border-pine/8">
|
||||
<img src={mainImage} alt={l.nameTr} className="object-cover w-full h-full" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-heading font-bold text-pine lowercase text-sm">{l.nameTr}</div>
|
||||
<div className="text-xs text-shutter font-mono mt-1">/{l.slug}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-xs font-semibold">
|
||||
<div className="text-pine">Kategori: {l.category?.nameTr}</div>
|
||||
<div className="text-shutter mt-0.5">Bölge: {l.neighborhood?.nameTr}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap font-mono text-xs">
|
||||
<div className="text-pine font-semibold">Fiyat: {'₺'.repeat(l.priceRange)}</div>
|
||||
{l.rating && <div className="text-gold font-bold mt-0.5">★ {l.rating.toFixed(1)}</div>}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{l.isLocalApproved ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-paper px-2.5 py-0.5 text-[10px] font-mono font-bold text-turquoise border border-turquoise/20 uppercase tracking-wider">
|
||||
<CheckCircle className="w-3.5 h-3.5 fill-turquoise/5" />
|
||||
Yerel Onaylı
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-paper px-2.5 py-0.5 text-[10px] font-mono font-medium text-shutter/60 border border-pine/8 uppercase tracking-wider">
|
||||
Normal
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right whitespace-nowrap">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link
|
||||
href={`/admin/listings/${l.id}`}
|
||||
className="p-1.5 bg-paper text-shutter hover:text-turquoise hover:bg-turquoise/5 rounded-lg border border-pine/10 hover:border-turquoise/25 transition shadow-sm"
|
||||
title="Düzenle"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Link>
|
||||
|
||||
<form action={async () => {
|
||||
'use server'
|
||||
await deleteListingAction(l.id)
|
||||
}}>
|
||||
<button
|
||||
type="submit"
|
||||
className="p-1.5 bg-paper text-shutter hover:text-bougainvillea hover:bg-bougainvillea/5 rounded-lg border border-pine/10 hover:border-bougainvillea/25 transition shadow-sm"
|
||||
title="Sil"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import { markMessageReadAction } from '@/app/actions'
|
||||
import { Check, MailOpen, Mail } from 'lucide-react'
|
||||
|
||||
export default async function AdminMessagesPage() {
|
||||
const messages = await mockDb.getMessages()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">iletisim mesajları</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
Kullanıcılar tarafından `/iletisim` formu üzerinden gönderilen genel mesajlar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-paper border border-pine/8 rounded-2xl shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-pine/8 text-sm">
|
||||
<thead className="bg-stone-deep/40 text-shutter font-mono text-[10px] uppercase tracking-wider">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left">Tarih</th>
|
||||
<th className="px-6 py-4 text-left">Gönderen</th>
|
||||
<th className="px-6 py-4 text-left">Konu</th>
|
||||
<th className="px-6 py-4 text-left">Mesaj</th>
|
||||
<th className="px-6 py-4 text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dashed divide-pine/8 text-ink/80">
|
||||
{messages.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-12 text-center text-xs text-shutter font-medium">
|
||||
Gelen iletişim mesajı bulunmamaktadır.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
messages.map((msg) => {
|
||||
return (
|
||||
<tr key={msg.id} className={`hover:bg-stone/20 transition duration-150 ${!msg.isRead ? 'bg-turquoise/5' : ''}`}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-xs text-shutter font-mono">
|
||||
{new Date(msg.createdAt).toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-pine font-heading font-bold text-xs lowercase">{msg.name}</div>
|
||||
<a href={`mailto:${msg.email}`} className="text-xs text-turquoise hover:underline">
|
||||
{msg.email}
|
||||
</a>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="inline-flex items-center rounded-md bg-paper px-2.5 py-0.5 text-[10px] font-mono font-bold text-shutter border border-pine/10 uppercase tracking-wider">
|
||||
{msg.subject}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 max-w-md">
|
||||
<p className="text-xs break-words leading-relaxed whitespace-pre-line font-medium">{msg.message}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right whitespace-nowrap">
|
||||
{!msg.isRead ? (
|
||||
<form action={async () => {
|
||||
'use server'
|
||||
await markMessageReadAction(msg.id)
|
||||
}}>
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-paper hover:bg-turquoise/5 text-turquoise rounded-lg border border-turquoise/20 text-xs font-bold transition shadow-sm"
|
||||
>
|
||||
<MailOpen className="w-3.5 h-3.5" />
|
||||
Okundu İşaretle
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<span className="text-shutter/60 inline-flex items-center gap-1 text-xs font-semibold">
|
||||
<Check className="w-4 h-4 text-turquoise" />
|
||||
Okundu
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
|
||||
export default async function AdminNeighborhoodsPage() {
|
||||
const neighborhoods = await mockDb.getNeighborhoods()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">mahalleler</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
Marmaris Local rehberindeki mekanların filtrelendiği mahalle/bölgeler.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-paper border border-pine/8 rounded-2xl shadow-sm overflow-hidden max-w-4xl">
|
||||
<table className="min-w-full divide-y divide-pine/8 text-sm">
|
||||
<thead className="bg-stone-deep/40 text-shutter font-mono text-[10px] uppercase tracking-wider">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left">ID</th>
|
||||
<th className="px-6 py-4 text-left">Slug</th>
|
||||
<th className="px-6 py-4 text-left">Adı (TR)</th>
|
||||
<th className="px-6 py-4 text-left">Name (EN)</th>
|
||||
<th className="px-6 py-4 text-left">Имя (RU)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dashed divide-pine/8 text-ink/85 font-medium">
|
||||
{neighborhoods.map((neigh) => (
|
||||
<tr key={neigh.id} className="hover:bg-stone/20 transition duration-150">
|
||||
<td className="px-6 py-4 font-mono text-xs text-shutter">{neigh.id}</td>
|
||||
<td className="px-6 py-4 font-mono text-xs font-bold text-turquoise">{neigh.slug}</td>
|
||||
<td className="px-6 py-4 font-heading font-bold text-pine lowercase text-sm">{neigh.nameTr}</td>
|
||||
<td className="px-6 py-4 text-xs">{neigh.nameEn}</td>
|
||||
<td className="px-6 py-4 text-xs">{neigh.nameRu}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { auth } from '@/lib/auth'
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import { Link } from '@/i18n/routing'
|
||||
import { FileText, Inbox, ClipboardList, Map, Clock } from 'lucide-react'
|
||||
|
||||
export default async function AdminDashboardPage() {
|
||||
const session = await auth()
|
||||
|
||||
// Load stats
|
||||
const listings = await mockDb.getListings()
|
||||
const submissions = await mockDb.getSubmissions()
|
||||
const messages = await mockDb.getMessages()
|
||||
const categories = await mockDb.getCategories()
|
||||
const neighborhoods = await mockDb.getNeighborhoods()
|
||||
|
||||
const pendingSubmissions = submissions.filter(s => s.status === 'PENDING')
|
||||
const unreadMessages = messages.filter(m => !m.isRead)
|
||||
|
||||
const stats = [
|
||||
{ name: 'Toplam Mekan', stat: listings.length.toString(), icon: ClipboardList, color: 'text-pine bg-stone-deep border-pine/8' },
|
||||
{ name: 'Bekleyen Başvuru', stat: pendingSubmissions.length.toString(), icon: FileText, color: 'text-gold bg-paper border-gold/15' },
|
||||
{ name: 'Okunmamış Mesaj', stat: unreadMessages.length.toString(), icon: Inbox, color: 'text-turquoise bg-stone-deep border-turquoise/15' },
|
||||
{ name: 'Kategori / Bölge', stat: `${categories.length} / ${neighborhoods.length}`, icon: Map, color: 'text-shutter bg-paper border-shutter/15' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">dashboard</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
Hoş geldiniz, {session?.user?.name || session?.user?.email}. Marmaris Local için genel rehber durumu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{stats.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="bg-paper rounded-2xl border border-pine/8 p-5 shadow-sm flex items-center gap-4 hover:border-turquoise/30 transition-all duration-300"
|
||||
>
|
||||
<div className={`p-3.5 rounded-xl border ${item.color}`}>
|
||||
<item.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<dt className="truncate text-[10px] font-mono uppercase tracking-wider text-shutter">{item.name}</dt>
|
||||
<dd className="mt-1 text-2xl font-heading font-extrabold text-pine">
|
||||
{item.stat}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
|
||||
{/* Recent Submissions */}
|
||||
<div className="bg-paper border border-pine/8 rounded-2xl shadow-sm p-6 sm:p-8 space-y-6">
|
||||
<div className="flex items-center justify-between border-b border-dashed border-pine/8 pb-4">
|
||||
<h3 className="text-lg font-heading font-bold text-pine lowercase flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-gold" />
|
||||
onay bekleyen başvurular
|
||||
</h3>
|
||||
<Link href="/admin/submissions" className="text-xs font-mono font-bold text-turquoise hover:underline uppercase tracking-wide">
|
||||
tümünü gör
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-dashed divide-pine/8">
|
||||
{pendingSubmissions.length === 0 ? (
|
||||
<div className="py-8 text-center text-xs text-shutter font-medium">
|
||||
Onay bekleyen yeni işletme başvurusu bulunmuyor.
|
||||
</div>
|
||||
) : (
|
||||
pendingSubmissions.slice(0, 5).map((sub) => (
|
||||
<div key={sub.id} className="py-4 flex items-center justify-between first:pt-0 last:pb-0">
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-heading font-bold text-sm text-pine lowercase">{sub.businessName}</h4>
|
||||
<p className="text-xs text-ink/65 font-medium">{sub.contactName} ({sub.contactEmail})</p>
|
||||
</div>
|
||||
<span className="inline-flex items-center rounded-full bg-paper px-3 py-1 text-[10px] font-mono font-bold text-gold border border-gold/20 uppercase">
|
||||
Bekliyor
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Messages */}
|
||||
<div className="bg-paper border border-pine/8 rounded-2xl shadow-sm p-6 sm:p-8 space-y-6">
|
||||
<div className="flex items-center justify-between border-b border-dashed border-pine/8 pb-4">
|
||||
<h3 className="text-lg font-heading font-bold text-pine lowercase flex items-center gap-2">
|
||||
<Inbox className="w-5 h-5 text-turquoise" />
|
||||
okunmamış mesajlar
|
||||
</h3>
|
||||
<Link href="/admin/messages" className="text-xs font-mono font-bold text-turquoise hover:underline uppercase tracking-wide">
|
||||
tümünü gör
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-dashed divide-pine/8">
|
||||
{unreadMessages.length === 0 ? (
|
||||
<div className="py-8 text-center text-xs text-shutter font-medium">
|
||||
Okunmamış yeni mesaj bulunmuyor.
|
||||
</div>
|
||||
) : (
|
||||
unreadMessages.slice(0, 5).map((msg) => (
|
||||
<div key={msg.id} className="py-4 flex items-center justify-between first:pt-0 last:pb-0">
|
||||
<div className="space-y-1 pr-4 flex-1">
|
||||
<h4 className="font-heading font-bold text-sm text-pine lowercase">{msg.name}</h4>
|
||||
<p className="text-xs text-ink/70 font-medium line-clamp-1">{msg.message}</p>
|
||||
</div>
|
||||
<span className="text-[10px] text-shutter font-mono shrink-0">
|
||||
{new Date(msg.createdAt).toLocaleDateString('tr-TR')}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import { approveSubmissionAction, rejectSubmissionAction } from '@/app/actions'
|
||||
import { Check, X } from 'lucide-react'
|
||||
|
||||
export default async function AdminSubmissionsPage() {
|
||||
const submissions = await mockDb.getSubmissions()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">işletme başvuruları</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
Kullanıcılar tarafından `/isletme-ekle` formu üzerinden gönderilen ve onay bekleyen başvurular.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-paper border border-pine/8 rounded-2xl shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-pine/8 text-sm">
|
||||
<thead className="bg-stone-deep/40 text-shutter font-mono text-[10px] uppercase tracking-wider">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left">İşletme</th>
|
||||
<th className="px-6 py-4 text-left">Konum & Kategori</th>
|
||||
<th className="px-6 py-4 text-left">Gönderen</th>
|
||||
<th className="px-6 py-4 text-left">Görsel</th>
|
||||
<th className="px-6 py-4 text-left">Durum</th>
|
||||
<th className="px-6 py-4 text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dashed divide-pine/8 text-ink/80">
|
||||
{submissions.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-12 text-center text-xs text-shutter font-medium">
|
||||
Kayıtlı işletme başvurusu bulunmamaktadır.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
submissions.map((sub) => {
|
||||
return (
|
||||
<tr key={sub.id} className="hover:bg-stone/20 transition duration-150">
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-heading font-bold text-pine lowercase text-sm">{sub.businessName}</div>
|
||||
<div className="text-xs text-ink/70 mt-1 max-w-xs line-clamp-2 leading-relaxed">{sub.description}</div>
|
||||
<div className="text-[11px] text-shutter mt-1.5">{sub.address}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-mono text-xs">
|
||||
<div className="text-pine font-semibold">Kategori: {sub.categoryId}</div>
|
||||
<div className="text-shutter mt-0.5">Bölge: {sub.neighborhoodId}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-xs font-semibold text-pine">{sub.contactName}</div>
|
||||
<a href={`mailto:${sub.contactEmail}`} className="text-xs text-turquoise hover:underline">
|
||||
{sub.contactEmail}
|
||||
</a>
|
||||
{sub.phone && <div className="text-[11px] text-ink/65 font-mono mt-1">{sub.phone}</div>}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{sub.imageUrl ? (
|
||||
<div className="w-16 h-12 relative rounded-lg border border-pine/8 overflow-hidden">
|
||||
<img src={sub.imageUrl} alt={sub.businessName} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[10px] font-mono text-shutter">Görsel Yok</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-[10px] font-mono font-bold border uppercase tracking-wider ${
|
||||
sub.status === 'PENDING' ? 'bg-paper text-gold border-gold/20' :
|
||||
sub.status === 'APPROVED' ? 'bg-paper text-turquoise border-turquoise/20' :
|
||||
'bg-paper text-bougainvillea border-bougainvillea/20'
|
||||
}`}>
|
||||
{sub.status === 'PENDING' ? 'Bekliyor' : sub.status === 'APPROVED' ? 'Onaylandı' : 'Reddedildi'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
{sub.status === 'PENDING' && (
|
||||
<div className="flex justify-end gap-2">
|
||||
<form action={async () => {
|
||||
'use server'
|
||||
await approveSubmissionAction(sub.id)
|
||||
}}>
|
||||
<button
|
||||
type="submit"
|
||||
className="p-1.5 bg-paper text-turquoise hover:bg-turquoise/5 rounded-lg border border-turquoise/20 transition shadow-sm"
|
||||
title="Onayla ve Mekanlara Ekle"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form action={async () => {
|
||||
'use server'
|
||||
await rejectSubmissionAction(sub.id)
|
||||
}}>
|
||||
<button
|
||||
type="submit"
|
||||
className="p-1.5 bg-paper text-bougainvillea hover:bg-bougainvillea/5 rounded-lg border border-bougainvillea/20 transition shadow-sm"
|
||||
title="Reddet"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/components/Footer'
|
||||
import ListingCard from '@/components/ListingCard'
|
||||
import { SlidersHorizontal } from 'lucide-react'
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ locale: string }>
|
||||
searchParams: Promise<{
|
||||
search?: string
|
||||
neighborhood?: string
|
||||
price?: string
|
||||
approved?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function ApartsPage({ params, searchParams }: PageProps) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const { search, neighborhood, price, approved } = await searchParams
|
||||
|
||||
const t = await getTranslations('categories')
|
||||
|
||||
// Find Category Apart
|
||||
const categories = await mockDb.getCategories()
|
||||
const currentCategory = categories.find(c => c.slug === 'apart')
|
||||
const categoryId = currentCategory?.id
|
||||
|
||||
// Get active neighborhoods for filter
|
||||
const neighborhoods = await mockDb.getNeighborhoods()
|
||||
|
||||
// Selected filters
|
||||
const selectedNeighborhoodId = neighborhood || undefined
|
||||
const selectedPriceRange = price ? parseInt(price) : undefined
|
||||
const isApprovedOnly = approved === 'true'
|
||||
|
||||
const listings = await mockDb.getListings({
|
||||
categoryId,
|
||||
neighborhoodId: selectedNeighborhoodId,
|
||||
priceRange: selectedPriceRange,
|
||||
isLocalApproved: isApprovedOnly ? true : undefined,
|
||||
search: search
|
||||
})
|
||||
|
||||
const getLocalizedName = (obj: any) => {
|
||||
if (!obj) return ''
|
||||
return locale === 'ru' ? obj.nameRu : locale === 'en' ? obj.nameEn : obj.nameTr
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||||
<Navbar />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 flex-1">
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{t('apart')}
|
||||
</h1>
|
||||
<p className="text-xs text-shutter font-mono uppercase tracking-wider mt-1">
|
||||
marmaris local • {listings.length} {locale === 'tr' ? 'sonuç' : locale === 'en' ? 'results' : 'результатов'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filters Panel */}
|
||||
<div className="bg-paper p-5 rounded-2xl border border-pine/8 shadow-sm mb-10">
|
||||
<div className="flex items-center gap-2 mb-4 font-heading font-bold text-sm text-pine lowercase border-b border-dashed border-pine/8 pb-3">
|
||||
<SlidersHorizontal className="w-4 h-4 text-turquoise" />
|
||||
<span>filtreler</span>
|
||||
</div>
|
||||
|
||||
<form method="GET" className="grid grid-cols-1 sm:grid-cols-4 gap-4 items-end">
|
||||
{/* Search Input */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-shutter">Arama</label>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
defaultValue={search || ''}
|
||||
placeholder="İsim veya adres..."
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-turquoise outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Neighborhood select */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-shutter">{t('filterNeighborhood')}</label>
|
||||
<select
|
||||
name="neighborhood"
|
||||
defaultValue={neighborhood || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-turquoise outline-none appearance-none"
|
||||
>
|
||||
<option value="">{t('allNeighborhoods')}</option>
|
||||
{neighborhoods.map((n) => (
|
||||
<option key={n.id} value={n.id}>
|
||||
{getLocalizedName(n)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Price range select */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-shutter">{t('filterPrice')}</label>
|
||||
<select
|
||||
name="price"
|
||||
defaultValue={price || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-turquoise outline-none"
|
||||
>
|
||||
<option value="">{t('allPrices')}</option>
|
||||
<option value="1">₺ (Ekonomik)</option>
|
||||
<option value="2">₺₺ (Orta)</option>
|
||||
<option value="3">₺₺₺ (Lüks)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Submit / Checkbox area */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none text-xs font-semibold py-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="approved"
|
||||
value="true"
|
||||
defaultChecked={isApprovedOnly}
|
||||
className="rounded border-pine/10 text-turquoise focus:ring-turquoise w-4 h-4"
|
||||
/>
|
||||
<span className="text-pine">{t('filterApproved')}</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 bg-turquoise hover:bg-turquoise/90 text-paper text-xs font-bold py-2.5 px-4 rounded-xl transition text-center"
|
||||
>
|
||||
Filtrele
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{listings.length === 0 ? (
|
||||
<div className="bg-paper/50 rounded-2xl border border-dashed border-pine/12 p-12 text-center text-shutter">
|
||||
<p className="text-sm font-medium">{t('noResults')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{listings.map((listing) => (
|
||||
<ListingCard key={listing.id} listing={listing} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/components/Footer'
|
||||
import { Link } from '@/i18n/routing'
|
||||
import { CheckCircle, ShieldAlert, Award, Star } from 'lucide-react'
|
||||
|
||||
interface AboutPageProps {
|
||||
params: Promise<{ locale: string }>
|
||||
}
|
||||
|
||||
export default async function AboutPage({ params }: AboutPageProps) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const t = await getTranslations('hero')
|
||||
const navT = await getTranslations('nav')
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||||
<Navbar />
|
||||
|
||||
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 flex-1 space-y-12">
|
||||
|
||||
{/* Title */}
|
||||
<div className="text-center space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-14 h-14 rounded-full border-2 border-turquoise flex items-center justify-center relative bg-paper -rotate-6 shadow-sm">
|
||||
<div className="absolute inset-[3px] rounded-full border border-dashed border-turquoise/60" />
|
||||
<span className="font-heading font-extrabold text-pine text-xs tracking-tighter">ML</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-heading font-extrabold text-pine lowercase leading-tight">
|
||||
{locale === 'tr' ? 'hakkımızda' : locale === 'en' ? 'about us' : 'о нас'}
|
||||
</h1>
|
||||
<p className="text-xs font-mono uppercase tracking-wider text-shutter">
|
||||
marmaris local • yerel bilgi rehberi
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-paper p-8 sm:p-12 rounded-3xl border border-pine/8 shadow-sm space-y-8 leading-relaxed font-medium text-sm sm:text-base text-ink/80">
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl sm:text-2xl font-heading font-bold text-pine lowercase">
|
||||
{locale === 'tr' ? 'turistin göremediği yerel bilgi' : locale === 'en' ? 'local knowledge hidden from tourists' : 'местные знания, скрытые от туристов'}
|
||||
</h2>
|
||||
<p>
|
||||
{locale === 'tr' && 'Marmaris Local, popüler tatil beldemiz Marmaris\'teki restoranları, apart otelleri ve tekne kiralama ya da dalış merkezleri gibi çeşitli yerel işletmeleri tek bir küratörlü rehberde toplayan bağımsız bir dizin sitesidir. Temel amacımız, yerli ve yabancı turistleri Marmaris\'in gerçek yerel halkının gittiği, kalitesinden ve samimiyetinden emin olduğu işletmelerle buluşturmaktır.'}
|
||||
{locale === 'en' && 'Marmaris Local is an independent directory site that gathers restaurants, apart hotels, and various local businesses such as boat rentals or diving centers in our popular holiday destination Marmaris into a single curated guide. Our main goal is to connect domestic and foreign tourists with establishments that the real local people of Marmaris visit, confident in their quality and friendliness.'}
|
||||
{locale === 'ru' && 'Marmaris Local — это независимый каталог, объединяющий рестораны, апарт-отели и различные местные предприятия, такие как аренда лодок или дайвинг-центры в нашем популярном месте отдыха Мармарис, в единый курируемый гид. Наша главная цель — познакомить местных и иностранных туристов с заведениями, которые посещают настоящие местные жители Мармариса, будучи уверенными в их качестве и дружелюбии.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr className="border-dashed border-pine/10" />
|
||||
|
||||
{/* How local approved works */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full border-2 border-turquoise bg-stone/30 flex items-center justify-center shrink-0 text-turquoise">
|
||||
<Award className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-xl sm:text-2xl font-heading font-bold text-pine lowercase leading-tight">
|
||||
{locale === 'tr' ? 'yerel onaylı mührü nedir?' : locale === 'en' ? 'what is the local approved seal?' : 'что такое печать качества?'}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p className="text-xs sm:text-sm text-ink/70">
|
||||
{t('approvedExplain')}
|
||||
</p>
|
||||
|
||||
{/* Steps / Criteria */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 pt-4 text-xs font-semibold text-pine font-mono">
|
||||
<div className="bg-stone/35 p-5 rounded-2xl border border-pine/5 space-y-2">
|
||||
<div className="text-turquoise text-lg font-bold">01</div>
|
||||
<div className="uppercase tracking-wider">
|
||||
{locale === 'tr' ? 'editör ziyareti' : locale === 'en' ? 'editor visit' : 'визит редактора'}
|
||||
</div>
|
||||
<p className="text-[11px] font-sans font-medium text-ink/65 normal-case leading-relaxed">
|
||||
{locale === 'tr' ? 'Her mekan editörlerimiz tarafından gizlice ziyaret edilir.' : locale === 'en' ? 'Every place is visited secretly by our editors.' : 'Каждое место тайно посещается нашими редакторами.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-stone/35 p-5 rounded-2xl border border-pine/5 space-y-2">
|
||||
<div className="text-turquoise text-lg font-bold">02</div>
|
||||
<div className="uppercase tracking-wider">
|
||||
{locale === 'tr' ? 'fiyat/performans' : locale === 'en' ? 'value for money' : 'цена / качество'}
|
||||
</div>
|
||||
<p className="text-[11px] font-sans font-medium text-ink/65 normal-case leading-relaxed">
|
||||
{locale === 'tr' ? 'Hizmet kalitesi ile fiyat dengesi yerel standartlara göre değerlendirilir.' : locale === 'en' ? 'The balance between service quality and price is evaluated according to local standards.' : 'Баланс качества услуг и цены оценивается по местным стандартам.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-stone/35 p-5 rounded-2xl border border-pine/5 space-y-2">
|
||||
<div className="text-turquoise text-lg font-bold">03</div>
|
||||
<div className="uppercase tracking-wider">
|
||||
{locale === 'tr' ? 'yerel onay' : locale === 'en' ? 'local approval' : 'местное одобрение'}
|
||||
</div>
|
||||
<p className="text-[11px] font-sans font-medium text-ink/65 normal-case leading-relaxed">
|
||||
{locale === 'tr' ? 'Marmaris sakinlerinin tavsiye ve memnuniyet oranları kontrol edilir.' : locale === 'en' ? 'Recommendations and satisfaction rates of Marmaris residents are checked.' : 'Проверяются рекомендации и уровень удовлетворенности жителей Мармариса.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-dashed border-pine/10" />
|
||||
|
||||
{/* Action Link */}
|
||||
<div className="text-center pt-4 space-y-4">
|
||||
<h3 className="font-heading font-bold text-pine lowercase text-lg">
|
||||
{locale === 'tr' ? 'kendi işletmenizi önermek ister misiniz?' : locale === 'en' ? 'would you like to suggest your own business?' : 'хотите предложить свой бизнес?'}
|
||||
</h3>
|
||||
<Link
|
||||
href="/isletme-ekle"
|
||||
className="inline-flex items-center gap-2 bg-turquoise hover:bg-turquoise/90 text-paper font-bold text-xs py-3.5 px-6 rounded-xl transition shadow-sm"
|
||||
>
|
||||
{navT('addBusiness')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { submitContactMessageAction } from '@/app/actions'
|
||||
|
||||
interface Translations {
|
||||
name: string
|
||||
email: string
|
||||
subject: string
|
||||
message: string
|
||||
submit: string
|
||||
sending: string
|
||||
success: string
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
translations: Translations
|
||||
}
|
||||
|
||||
export default function ContactForm({ translations }: FormProps) {
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
const formData = new FormData(e.currentTarget)
|
||||
try {
|
||||
const res = await submitContactMessageAction(formData)
|
||||
if (res.error) {
|
||||
setError(res.error)
|
||||
} else {
|
||||
setSuccess(true)
|
||||
e.currentTarget.reset()
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Bir hata oluştu. Lütfen tekrar deneyin.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="bg-turquoise/10 border border-turquoise/20 text-turquoise p-6 rounded-2xl text-center space-y-3">
|
||||
<h3 className="font-heading font-bold text-lg lowercase">teşekkürler!</h3>
|
||||
<p className="text-sm font-medium">{translations.success}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-bougainvillea/10 border border-bougainvillea/20 text-bougainvillea p-4 rounded-xl text-xs font-semibold">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Name */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.name} *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="Adınız Soyadınız"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.email} *</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="eposta@adresiniz.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Subject */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.subject} *</label>
|
||||
<select
|
||||
name="subject"
|
||||
required
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink appearance-none"
|
||||
>
|
||||
<option value="">Konu seçin...</option>
|
||||
<option value="Genel">Genel Sorular</option>
|
||||
<option value="İşbirliği">İşbirliği</option>
|
||||
<option value="Hata Bildirimi">Hata Bildirimi</option>
|
||||
<option value="Diğer">Diğer</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.message} *</label>
|
||||
<textarea
|
||||
name="message"
|
||||
required
|
||||
rows={5}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="Mesajınızı buraya yazın..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-turquoise hover:bg-turquoise/90 disabled:opacity-75 disabled:cursor-not-allowed text-paper font-bold text-sm py-4.5 px-4 rounded-xl transition duration-300 shadow-sm"
|
||||
>
|
||||
{loading ? translations.sending : translations.submit}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/components/Footer'
|
||||
import ContactForm from './ContactForm'
|
||||
|
||||
interface ContactPageProps {
|
||||
params: Promise<{ locale: string }>
|
||||
}
|
||||
|
||||
export default async function ContactPage({ params }: ContactPageProps) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const t = await getTranslations('forms')
|
||||
const navT = await getTranslations('nav')
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||||
<Navbar />
|
||||
|
||||
<main className="max-w-xl mx-auto px-4 sm:px-6 lg:px-8 py-12 flex-1 w-full">
|
||||
<div className="bg-paper p-8 rounded-3xl border border-pine/8 shadow-sm">
|
||||
|
||||
<div className="mb-8 border-b border-dashed border-pine/8 pb-6">
|
||||
<h1 className="text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{navT('contact')}
|
||||
</h1>
|
||||
<p className="text-xs text-shutter font-mono uppercase tracking-wider mt-1.5">
|
||||
marmaris local • bize ulaşın
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ContactForm
|
||||
translations={{
|
||||
name: t('name'),
|
||||
email: t('email'),
|
||||
subject: t('subject'),
|
||||
message: t('message'),
|
||||
submit: t('submit'),
|
||||
sending: t('sending'),
|
||||
success: t('contactSuccess')
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { submitBusinessAction } from '@/app/actions'
|
||||
|
||||
interface Option {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface Translations {
|
||||
businessName: string
|
||||
category: string
|
||||
neighborhood: string
|
||||
address: string
|
||||
phone: string
|
||||
whatsapp: string
|
||||
description: string
|
||||
image: string
|
||||
submit: string
|
||||
sending: string
|
||||
success: string
|
||||
contactName: string
|
||||
contactEmail: string
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
translations: Translations
|
||||
categories: Option[]
|
||||
neighborhoods: Option[]
|
||||
}
|
||||
|
||||
export default function BusinessForm({ translations, categories, neighborhoods }: FormProps) {
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
const formData = new FormData(e.currentTarget)
|
||||
try {
|
||||
const res = await submitBusinessAction(formData)
|
||||
if (res.error) {
|
||||
setError(res.error)
|
||||
} else {
|
||||
setSuccess(true)
|
||||
e.currentTarget.reset()
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Bir hata oluştu. Lütfen tekrar deneyin.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="bg-turquoise/10 border border-turquoise/20 text-turquoise p-6 rounded-2xl text-center space-y-3">
|
||||
<h3 className="font-heading font-bold text-lg lowercase">teşekkürler!</h3>
|
||||
<p className="text-sm font-medium">{translations.success}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-bougainvillea/10 border border-bougainvillea/20 text-bougainvillea p-4 rounded-xl text-xs font-semibold">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Business name */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.businessName} *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="businessName"
|
||||
required
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="Örn: İskele Balık Ocakbaşı"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Category */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.category} *</label>
|
||||
<select
|
||||
name="categoryId"
|
||||
required
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink appearance-none"
|
||||
>
|
||||
<option value="">Kategori seçin...</option>
|
||||
{categories.map((c) => (
|
||||
<option key={c.value} value={c.value}>
|
||||
{c.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Neighborhood */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.neighborhood} *</label>
|
||||
<select
|
||||
name="neighborhoodId"
|
||||
required
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink appearance-none"
|
||||
>
|
||||
<option value="">Mahalle seçin...</option>
|
||||
{neighborhoods.map((n) => (
|
||||
<option key={n.value} value={n.value}>
|
||||
{n.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.address} *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
required
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="Örn: Yat Limanı No:12, Marmaris"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Phone */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.phone}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="phone"
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="Örn: +90 252 412 34 56"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Whatsapp */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.whatsapp}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="whatsapp"
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="Örn: +90 532 123 45 67"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.description} *</label>
|
||||
<textarea
|
||||
name="description"
|
||||
required
|
||||
rows={4}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="İşletmenizi tanıtan kısa bir açıklama yazın..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Image File */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.image}</label>
|
||||
<input
|
||||
type="file"
|
||||
name="imageFile"
|
||||
accept="image/*"
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink file:mr-4 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-pine file:text-stone hover:file:opacity-90 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Contact Name & Email */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 border-t border-dashed border-pine/10 pt-6">
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.contactName} *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="contactName"
|
||||
required
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="Adınız Soyadınız"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">{translations.contactEmail} *</label>
|
||||
<input
|
||||
type="email"
|
||||
name="contactEmail"
|
||||
required
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink placeholder:text-ink/30"
|
||||
placeholder="eposta@adresiniz.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-turquoise hover:bg-turquoise/90 disabled:opacity-75 disabled:cursor-not-allowed text-paper font-bold text-sm py-4.5 px-4 rounded-xl transition duration-300 shadow-sm"
|
||||
>
|
||||
{loading ? translations.sending : translations.submit}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/components/Footer'
|
||||
import BusinessForm from './BusinessForm'
|
||||
|
||||
interface AddBusinessPageProps {
|
||||
params: Promise<{ locale: string }>
|
||||
}
|
||||
|
||||
export default async function AddBusinessPage({ params }: AddBusinessPageProps) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const t = await getTranslations('forms')
|
||||
const navT = await getTranslations('nav')
|
||||
|
||||
// Fetch categories and neighborhoods to populate form select options
|
||||
const categories = await mockDb.getCategories()
|
||||
const neighborhoods = await mockDb.getNeighborhoods()
|
||||
|
||||
const getLocalizedName = (obj: any) => {
|
||||
if (!obj) return ''
|
||||
return locale === 'ru' ? obj.nameRu : locale === 'en' ? obj.nameEn : obj.nameTr
|
||||
}
|
||||
|
||||
const categoryOptions = categories.map(c => ({
|
||||
value: c.id,
|
||||
label: getLocalizedName(c)
|
||||
}))
|
||||
|
||||
const neighborhoodOptions = neighborhoods.map(n => ({
|
||||
value: n.id,
|
||||
label: getLocalizedName(n)
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||||
<Navbar />
|
||||
|
||||
<main className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12 flex-1 w-full">
|
||||
<div className="bg-paper p-8 rounded-3xl border border-pine/8 shadow-sm">
|
||||
|
||||
<div className="mb-8 border-b border-dashed border-pine/8 pb-6">
|
||||
<h1 className="text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{navT('addBusiness')}
|
||||
</h1>
|
||||
<p className="text-xs text-shutter font-mono uppercase tracking-wider mt-1.5">
|
||||
marmaris local • yeni başvuru
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BusinessForm
|
||||
translations={{
|
||||
businessName: t('businessName'),
|
||||
category: t('category'),
|
||||
neighborhood: t('neighborhood'),
|
||||
address: t('address'),
|
||||
phone: t('phone'),
|
||||
whatsapp: t('whatsapp'),
|
||||
description: t('description'),
|
||||
image: t('image'),
|
||||
submit: t('submit'),
|
||||
sending: t('sending'),
|
||||
success: t('success'),
|
||||
contactName: 'Yetkili Adı Soyadı',
|
||||
contactEmail: 'İletişim E-postası'
|
||||
}}
|
||||
categories={categoryOptions}
|
||||
neighborhoods={neighborhoodOptions}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/components/Footer'
|
||||
import ListingCard from '@/components/ListingCard'
|
||||
import { SlidersHorizontal } from 'lucide-react'
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ locale: string }>
|
||||
searchParams: Promise<{
|
||||
search?: string
|
||||
neighborhood?: string
|
||||
price?: string
|
||||
approved?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function BusinessesPage({ params, searchParams }: PageProps) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const { search, neighborhood, price, approved } = await searchParams
|
||||
|
||||
const t = await getTranslations('categories')
|
||||
|
||||
// Find Category Isletme
|
||||
const categories = await mockDb.getCategories()
|
||||
const currentCategory = categories.find(c => c.slug === 'isletme')
|
||||
const categoryId = currentCategory?.id
|
||||
|
||||
// Get active neighborhoods for filter
|
||||
const neighborhoods = await mockDb.getNeighborhoods()
|
||||
|
||||
// Selected filters
|
||||
const selectedNeighborhoodId = neighborhood || undefined
|
||||
const selectedPriceRange = price ? parseInt(price) : undefined
|
||||
const isApprovedOnly = approved === 'true'
|
||||
|
||||
const listings = await mockDb.getListings({
|
||||
categoryId,
|
||||
neighborhoodId: selectedNeighborhoodId,
|
||||
priceRange: selectedPriceRange,
|
||||
isLocalApproved: isApprovedOnly ? true : undefined,
|
||||
search: search
|
||||
})
|
||||
|
||||
const getLocalizedName = (obj: any) => {
|
||||
if (!obj) return ''
|
||||
return locale === 'ru' ? obj.nameRu : locale === 'en' ? obj.nameEn : obj.nameTr
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||||
<Navbar />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 flex-1">
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{t('isletme')}
|
||||
</h1>
|
||||
<p className="text-xs text-shutter font-mono uppercase tracking-wider mt-1">
|
||||
marmaris local • {listings.length} {locale === 'tr' ? 'sonuç' : locale === 'en' ? 'results' : 'результатов'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filters Panel */}
|
||||
<div className="bg-paper p-5 rounded-2xl border border-pine/8 shadow-sm mb-10">
|
||||
<div className="flex items-center gap-2 mb-4 font-heading font-bold text-sm text-pine lowercase border-b border-dashed border-pine/8 pb-3">
|
||||
<SlidersHorizontal className="w-4 h-4 text-turquoise" />
|
||||
<span>filtreler</span>
|
||||
</div>
|
||||
|
||||
<form method="GET" className="grid grid-cols-1 sm:grid-cols-4 gap-4 items-end">
|
||||
{/* Search Input */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-shutter">Arama</label>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
defaultValue={search || ''}
|
||||
placeholder="İsim veya adres..."
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-turquoise outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Neighborhood select */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-shutter">{t('filterNeighborhood')}</label>
|
||||
<select
|
||||
name="neighborhood"
|
||||
defaultValue={neighborhood || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-turquoise outline-none appearance-none"
|
||||
>
|
||||
<option value="">{t('allNeighborhoods')}</option>
|
||||
{neighborhoods.map((n) => (
|
||||
<option key={n.id} value={n.id}>
|
||||
{getLocalizedName(n)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Price range select */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-shutter">{t('filterPrice')}</label>
|
||||
<select
|
||||
name="price"
|
||||
defaultValue={price || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-turquoise outline-none"
|
||||
>
|
||||
<option value="">{t('allPrices')}</option>
|
||||
<option value="1">₺ (Ekonomik)</option>
|
||||
<option value="2">₺₺ (Orta)</option>
|
||||
<option value="3">₺₺₺ (Lüks)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Submit / Checkbox area */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none text-xs font-semibold py-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="approved"
|
||||
value="true"
|
||||
defaultChecked={isApprovedOnly}
|
||||
className="rounded border-pine/10 text-turquoise focus:ring-turquoise w-4 h-4"
|
||||
/>
|
||||
<span className="text-pine">{t('filterApproved')}</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 bg-turquoise hover:bg-turquoise/90 text-paper text-xs font-bold py-2.5 px-4 rounded-xl transition text-center"
|
||||
>
|
||||
Filtrele
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{listings.length === 0 ? (
|
||||
<div className="bg-paper/50 rounded-2xl border border-dashed border-pine/12 p-12 text-center text-shutter">
|
||||
<p className="text-sm font-medium">{t('noResults')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{listings.map((listing) => (
|
||||
<ListingCard key={listing.id} listing={listing} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Unbounded, Golos_Text, IBM_Plex_Mono } from "next/font/google";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages, setRequestLocale } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import "../globals.css";
|
||||
|
||||
const unbounded = Unbounded({
|
||||
variable: "--font-unbounded",
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["400", "600", "800"],
|
||||
});
|
||||
|
||||
const golosText = Golos_Text({
|
||||
variable: "--font-golos",
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["400", "500", "600"],
|
||||
});
|
||||
|
||||
const ibmPlexMono = IBM_Plex_Mono({
|
||||
variable: "--font-mono",
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["400", "500"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Marmaris Local — Yerel Rehber",
|
||||
description: "Marmaris'in en iyi yerel mekanları, restoranları ve saklı apart otelleri.",
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({locale}));
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}>) {
|
||||
const { locale } = await params;
|
||||
|
||||
if (!routing.locales.includes(locale as any)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
setRequestLocale(locale);
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
className={`${unbounded.variable} ${golosText.variable} ${ibmPlexMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col font-sans" suppressHydrationWarning>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { signIn } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
const result = await signIn('credentials', {
|
||||
redirect: false,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
if (result?.error) {
|
||||
setError('Geçersiz e-posta veya şifre')
|
||||
setLoading(false)
|
||||
} else {
|
||||
router.push('/admin')
|
||||
router.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
|
||||
<div className="w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-100 dark:border-gray-800 overflow-hidden">
|
||||
<div className="p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Admin Girişi</h1>
|
||||
<p className="text-sm text-gray-500 mt-2">Yönetim paneline erişmek için giriş yapın</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 text-red-600 p-3 rounded-md text-sm mb-6 border border-red-100">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" htmlFor="email">
|
||||
E-posta
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-white transition-colors"
|
||||
placeholder="admin@ayris.tech"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" htmlFor="password">
|
||||
Şifre
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-white transition-colors"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-4 rounded-md transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Giriş yapılıyor...' : 'Giriş Yap'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-xs text-gray-400">
|
||||
Demo credentials: admin@ayris.tech / admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { setRequestLocale } from 'next-intl/server'
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/components/Footer'
|
||||
import ListingCard from '@/components/ListingCard'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { MapPin } from 'lucide-react'
|
||||
|
||||
interface NeighborhoodPageProps {
|
||||
params: Promise<{ locale: string; slug: string }>
|
||||
}
|
||||
|
||||
export default async function NeighborhoodPage({ params }: NeighborhoodPageProps) {
|
||||
const { locale, slug } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const neighborhood = await mockDb.getNeighborhoodBySlug(slug)
|
||||
if (!neighborhood) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const listings = await mockDb.getListings({
|
||||
neighborhoodId: neighborhood.id
|
||||
})
|
||||
|
||||
const name =
|
||||
locale === 'ru'
|
||||
? neighborhood.nameRu
|
||||
: locale === 'en'
|
||||
? neighborhood.nameEn
|
||||
: neighborhood.nameTr
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||||
<Navbar />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 flex-1">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-10 pb-6 border-b border-pine/8">
|
||||
<div className="p-3 bg-paper rounded-xl border border-pine/8 text-turquoise shadow-sm">
|
||||
<MapPin className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{name}
|
||||
</h1>
|
||||
<p className="text-xs text-shutter font-mono uppercase tracking-wider mt-1">
|
||||
{locale === 'tr' ? 'mahalle rehberi' : locale === 'en' ? 'neighborhood directory' : 'гид по району'} • {listings.length} {locale === 'tr' ? 'mekan' : locale === 'en' ? 'places' : 'заведений'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{listings.length === 0 ? (
|
||||
<div className="bg-paper/50 rounded-2xl border border-dashed border-pine/12 p-16 text-center text-shutter">
|
||||
<p className="text-sm font-medium">
|
||||
{locale === 'tr' ? 'Bu mahallede henüz kayıtlı mekan bulunmamaktadır.' : locale === 'en' ? 'No registered places in this neighborhood yet.' : 'В этом районе пока нет зарегистрированных мест.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{listings.map((listing) => (
|
||||
<ListingCard key={listing.id} listing={listing} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/components/Footer'
|
||||
import ListingCard from '@/components/ListingCard'
|
||||
import { Link } from '@/i18n/routing'
|
||||
import { Search, MapPin, CheckCircle, ArrowRight } from 'lucide-react'
|
||||
|
||||
export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const t = await getTranslations('hero')
|
||||
const homeT = await getTranslations('home')
|
||||
const navT = await getTranslations('nav')
|
||||
|
||||
// Get active listings and filter for featured (Local Approved)
|
||||
const allListings = await mockDb.getListings()
|
||||
const featuredListings = allListings.filter(l => l.isLocalApproved).slice(0, 3)
|
||||
|
||||
const categories = await mockDb.getCategories()
|
||||
const neighborhoods = await mockDb.getNeighborhoods()
|
||||
|
||||
// Get localized names
|
||||
const getLocalizedName = (obj: any) => {
|
||||
if (!obj) return ''
|
||||
return locale === 'ru' ? obj.nameRu : locale === 'en' ? obj.nameEn : obj.nameTr
|
||||
}
|
||||
|
||||
// Map category slug to lucide icons or nice visual cues
|
||||
const categoryImages: Record<string, string> = {
|
||||
restoran: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&auto=format&fit=crop&q=80',
|
||||
apart: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800&auto=format&fit=crop&q=80',
|
||||
isletme: 'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=800&auto=format&fit=crop&q=80'
|
||||
}
|
||||
|
||||
const categoryPaths: Record<string, string> = {
|
||||
restoran: '/restoranlar',
|
||||
apart: '/apartlar',
|
||||
isletme: '/isletmeler'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||||
<Navbar />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="bg-pine text-stone pt-20 pb-24 relative overflow-hidden">
|
||||
{/* Decorative background shapes */}
|
||||
<div className="absolute inset-0 opacity-5 pointer-events-none">
|
||||
<div className="absolute -top-40 -right-40 w-96 h-96 rounded-full bg-turquoise blur-3xl" />
|
||||
<div className="absolute -bottom-45 -left-40 w-96 h-96 rounded-full bg-bougainvillea blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10 space-y-8">
|
||||
|
||||
{/* Logo stamp effect */}
|
||||
<div className="flex justify-center">
|
||||
<div className="w-16 h-16 rounded-full border-2 border-stone/30 flex items-center justify-center relative bg-white/5 backdrop-blur-sm -rotate-6">
|
||||
<div className="absolute inset-[3.5px] rounded-full border border-dashed border-turquoise/50" />
|
||||
<span className="font-heading font-extrabold text-stone text-lg tracking-tighter">ML</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="font-heading font-extrabold text-3xl sm:text-5xl lg:text-6xl text-stone leading-tight tracking-tight max-w-3xl mx-auto lowercase">
|
||||
{locale === 'tr' && (
|
||||
<>En iyi yerel adresler,<br /><span className="text-turquoise">turistin göremediği yerde.</span></>
|
||||
)}
|
||||
{locale === 'en' && (
|
||||
<>The best local spots,<br /><span className="text-turquoise">hidden from plain sight.</span></>
|
||||
)}
|
||||
{locale === 'ru' && (
|
||||
<>Лучшие места,<br /><span className="text-turquoise">которые знают местные.</span></>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<p className="text-sm sm:text-base text-stone/70 max-w-xl mx-auto font-medium">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
|
||||
{/* Search Form */}
|
||||
<form
|
||||
action={`/${locale}/restoranlar`}
|
||||
method="GET"
|
||||
className="max-w-xl mx-auto bg-paper p-2 rounded-2xl flex items-center shadow-lg border border-white/10"
|
||||
>
|
||||
<div className="flex items-center flex-1 px-3">
|
||||
<Search className="w-5 h-5 text-shutter shrink-0" />
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder={t('searchPlaceholder')}
|
||||
className="w-full bg-transparent border-0 focus:ring-0 text-sm py-2 px-3 text-ink placeholder:text-ink/40 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-turquoise hover:bg-turquoise/90 text-paper font-medium text-xs px-5 py-3 rounded-xl transition"
|
||||
>
|
||||
{t('cta')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Local Approved Banner */}
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 text-xs">
|
||||
<span className="inline-block w-2.5 h-2.5 rounded-full bg-turquoise animate-pulse" />
|
||||
<span className="font-semibold text-turquoise">{t('approvedBadge')}</span>
|
||||
<span className="text-stone/60">mührü ile güvenli rehber</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Category Grid Section */}
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="text-center mb-12">
|
||||
<h3 className="text-2xl sm:text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{homeT('categories')}
|
||||
</h3>
|
||||
<p className="text-sm text-ink/65 mt-2 font-medium">
|
||||
{homeT('categoriesSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{categories.map((cat) => {
|
||||
const imageUrl = categoryImages[cat.slug] || categoryImages.isletme
|
||||
const path = categoryPaths[cat.slug] || '/isletmeler'
|
||||
const catName = getLocalizedName(cat)
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={cat.id}
|
||||
href={path}
|
||||
className="group relative h-64 rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition duration-300 border border-pine/5 flex items-end p-6"
|
||||
>
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={catName}
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-pine/90 via-pine/30 to-transparent" />
|
||||
</div>
|
||||
<div className="relative z-10 space-y-1">
|
||||
<h4 className="font-heading font-extrabold text-xl text-stone group-hover:text-turquoise transition-colors lowercase">
|
||||
{catName}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1 text-[11px] font-mono text-turquoise">
|
||||
<span>{homeT('explore')}</span>
|
||||
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Local Approved Featured Section */}
|
||||
<section className="bg-stone-deep py-20 border-t border-b border-pine/5">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-end justify-between mb-12 gap-4">
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full border border-turquoise/20 bg-turquoise/5 text-[10px] font-mono text-turquoise font-semibold uppercase tracking-wider mb-3">
|
||||
{t('approvedBadge')}
|
||||
</div>
|
||||
<h3 className="text-2xl sm:text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{homeT('featured')}
|
||||
</h3>
|
||||
<p className="text-sm text-ink/65 mt-2 font-medium">
|
||||
{homeT('featuredSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/restoranlar?approved=true"
|
||||
className="flex items-center gap-1 text-xs font-bold text-pine hover:text-turquoise transition-colors border-b border-pine/20 hover:border-turquoise pb-1 w-fit"
|
||||
>
|
||||
<span>{locale === 'tr' ? 'tüm onaylı mekanlar' : locale === 'en' ? 'all approved places' : 'все проверенные места'}</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{featuredListings.map((listing) => (
|
||||
<ListingCard key={listing.id} listing={listing} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Neighborhoods Section */}
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="text-center mb-12">
|
||||
<h3 className="text-2xl sm:text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{homeT('neighborhoods')}
|
||||
</h3>
|
||||
<p className="text-sm text-ink/65 mt-2 font-medium">
|
||||
{homeT('neighborhoodsSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{neighborhoods.map((neigh) => {
|
||||
const name = getLocalizedName(neigh)
|
||||
return (
|
||||
<Link
|
||||
key={neigh.id}
|
||||
href={`/mahalle/${neigh.slug}`}
|
||||
className="bg-paper p-5 rounded-xl border border-pine/8 text-center hover:border-turquoise/40 hover:bg-paper/90 transition shadow-sm group flex flex-col items-center gap-2"
|
||||
>
|
||||
<MapPin className="w-5 h-5 text-shutter group-hover:text-turquoise transition-colors" />
|
||||
<span className="font-heading font-bold text-xs text-pine lowercase group-hover:text-turquoise transition-colors">
|
||||
{name}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Trust Badge Explainer */}
|
||||
<section className="bg-pine text-stone py-16 border-t border-white/5">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center space-y-6">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-14 h-14 rounded-full border-2 border-turquoise flex items-center justify-center relative bg-paper -rotate-6">
|
||||
<div className="absolute inset-[2px] rounded-full border border-dashed border-turquoise/60" />
|
||||
<span className="font-mono text-[8px] text-center font-bold text-turquoise tracking-tight leading-none uppercase">
|
||||
YEREL<br />ONAYLI
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl sm:text-2xl font-heading font-extrabold lowercase">
|
||||
yerel onay mührü nedir?
|
||||
</h3>
|
||||
<p className="text-xs sm:text-sm text-stone/70 max-w-xl mx-auto font-medium leading-relaxed">
|
||||
{t('approvedExplain')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/components/Footer'
|
||||
import ListingCard from '@/components/ListingCard'
|
||||
import { Link } from '@/i18n/routing'
|
||||
import { MapPin, SlidersHorizontal, Check } from 'lucide-react'
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ locale: string }>
|
||||
searchParams: Promise<{
|
||||
search?: string
|
||||
neighborhood?: string
|
||||
price?: string
|
||||
approved?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function RestaurantsPage({ params, searchParams }: PageProps) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
|
||||
const { search, neighborhood, price, approved } = await searchParams
|
||||
|
||||
const t = await getTranslations('categories')
|
||||
const navT = await getTranslations('nav')
|
||||
|
||||
// Find Category Restoran
|
||||
const categories = await mockDb.getCategories()
|
||||
const currentCategory = categories.find(c => c.slug === 'restoran')
|
||||
const categoryId = currentCategory?.id
|
||||
|
||||
// Get active neighborhoods for filter
|
||||
const neighborhoods = await mockDb.getNeighborhoods()
|
||||
|
||||
// Selected filters
|
||||
const selectedNeighborhoodId = neighborhood || undefined
|
||||
const selectedPriceRange = price ? parseInt(price) : undefined
|
||||
const isApprovedOnly = approved === 'true'
|
||||
|
||||
const listings = await mockDb.getListings({
|
||||
categoryId,
|
||||
neighborhoodId: selectedNeighborhoodId,
|
||||
priceRange: selectedPriceRange,
|
||||
isLocalApproved: isApprovedOnly ? true : undefined,
|
||||
search: search
|
||||
})
|
||||
|
||||
const getLocalizedName = (obj: any) => {
|
||||
if (!obj) return ''
|
||||
return locale === 'ru' ? obj.nameRu : locale === 'en' ? obj.nameEn : obj.nameTr
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||||
<Navbar />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 flex-1">
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{t('restoran')}
|
||||
</h1>
|
||||
<p className="text-xs text-shutter font-mono uppercase tracking-wider mt-1">
|
||||
marmaris local • {listings.length} {locale === 'tr' ? 'sonuç' : locale === 'en' ? 'results' : 'результатов'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filters Panel */}
|
||||
<div className="bg-paper p-5 rounded-2xl border border-pine/8 shadow-sm mb-10">
|
||||
<div className="flex items-center gap-2 mb-4 font-heading font-bold text-sm text-pine lowercase border-b border-dashed border-pine/8 pb-3">
|
||||
<SlidersHorizontal className="w-4 h-4 text-turquoise" />
|
||||
<span>filtreler</span>
|
||||
</div>
|
||||
|
||||
<form method="GET" className="grid grid-cols-1 sm:grid-cols-4 gap-4 items-end">
|
||||
{/* Search Input */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-shutter">Arama</label>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
defaultValue={search || ''}
|
||||
placeholder="İsim veya adres..."
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-turquoise outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Neighborhood select */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-shutter">{t('filterNeighborhood')}</label>
|
||||
<select
|
||||
name="neighborhood"
|
||||
defaultValue={neighborhood || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-turquoise outline-none appearance-none"
|
||||
>
|
||||
<option value="">{t('allNeighborhoods')}</option>
|
||||
{neighborhoods.map((n) => (
|
||||
<option key={n.id} value={n.id}>
|
||||
{getLocalizedName(n)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Price range select */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-shutter">{t('filterPrice')}</label>
|
||||
<select
|
||||
name="price"
|
||||
defaultValue={price || ''}
|
||||
className="w-full bg-stone border border-pine/10 rounded-xl px-3 py-2 text-xs focus:ring-1 focus:ring-turquoise outline-none"
|
||||
>
|
||||
<option value="">{t('allPrices')}</option>
|
||||
<option value="1">₺ (Ekonomik)</option>
|
||||
<option value="2">₺₺ (Orta)</option>
|
||||
<option value="3">₺₺₺ (Lüks)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Submit / Checkbox area */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none text-xs font-semibold py-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="approved"
|
||||
value="true"
|
||||
defaultChecked={isApprovedOnly}
|
||||
className="rounded border-pine/10 text-turquoise focus:ring-turquoise w-4 h-4"
|
||||
/>
|
||||
<span className="text-pine">{t('filterApproved')}</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 bg-turquoise hover:bg-turquoise/90 text-paper text-xs font-bold py-2.5 px-4 rounded-xl transition text-center"
|
||||
>
|
||||
Filtrele
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{listings.length === 0 ? (
|
||||
<div className="bg-paper/50 rounded-2xl border border-dashed border-pine/12 p-12 text-center text-shutter">
|
||||
<p className="text-sm font-medium">{t('noResults')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{listings.map((listing) => (
|
||||
<ListingCard key={listing.id} listing={listing} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
'use server'
|
||||
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import { uploadToOpeninary } from '@/lib/openinary'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function submitBusinessAction(formData: FormData) {
|
||||
const businessName = formData.get('businessName') as string
|
||||
const categoryId = formData.get('categoryId') as string
|
||||
const neighborhoodId = formData.get('neighborhoodId') as string
|
||||
const address = formData.get('address') as string
|
||||
const phone = formData.get('phone') as string
|
||||
const whatsapp = formData.get('whatsapp') as string
|
||||
const description = formData.get('description') as string
|
||||
const contactName = formData.get('contactName') as string
|
||||
const contactEmail = formData.get('contactEmail') as string
|
||||
const imageFile = formData.get('imageFile') as File | null
|
||||
|
||||
if (!businessName || !categoryId || !neighborhoodId || !address || !description || !contactName || !contactEmail) {
|
||||
return { error: 'Lütfen tüm zorunlu alanları doldurun.' }
|
||||
}
|
||||
|
||||
let imageUrl = null
|
||||
if (imageFile && imageFile.size > 0) {
|
||||
try {
|
||||
imageUrl = await uploadToOpeninary(imageFile, 'submissions')
|
||||
} catch (e: any) {
|
||||
console.error('Openinary upload error:', e)
|
||||
return { error: `Görsel yüklenemedi: ${e.message}` }
|
||||
}
|
||||
}
|
||||
|
||||
await mockDb.createSubmission({
|
||||
businessName,
|
||||
categoryId,
|
||||
neighborhoodId,
|
||||
address,
|
||||
phone,
|
||||
whatsapp,
|
||||
description,
|
||||
contactName,
|
||||
contactEmail,
|
||||
imageUrl
|
||||
})
|
||||
|
||||
revalidatePath('/admin/submissions')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function submitContactMessageAction(formData: FormData) {
|
||||
const name = formData.get('name') as string
|
||||
const email = formData.get('email') as string
|
||||
const subject = formData.get('subject') as string
|
||||
const message = formData.get('message') as string
|
||||
|
||||
if (!name || !email || !subject || !message) {
|
||||
return { error: 'Lütfen tüm alanları doldurun.' }
|
||||
}
|
||||
|
||||
await mockDb.createMessage({
|
||||
name,
|
||||
email,
|
||||
subject,
|
||||
message
|
||||
})
|
||||
|
||||
revalidatePath('/admin/messages')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function approveSubmissionAction(id: string) {
|
||||
const submission = await mockDb.getSubmissionById(id)
|
||||
if (!submission) return { error: 'Başvuru bulunamadı' }
|
||||
|
||||
const slug = submission.businessName.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
|
||||
await mockDb.createListing({
|
||||
slug,
|
||||
categoryId: submission.categoryId,
|
||||
neighborhoodId: submission.neighborhoodId,
|
||||
city: 'marmaris',
|
||||
nameTr: submission.businessName,
|
||||
nameEn: submission.businessName,
|
||||
nameRu: submission.businessName,
|
||||
descriptionTr: submission.description,
|
||||
descriptionEn: submission.description,
|
||||
descriptionRu: submission.description,
|
||||
address: submission.address,
|
||||
phone: submission.phone,
|
||||
whatsapp: submission.whatsapp,
|
||||
priceRange: 2,
|
||||
rating: 5.0,
|
||||
isLocalApproved: false,
|
||||
images: submission.imageUrl ? [submission.imageUrl] : []
|
||||
})
|
||||
|
||||
await mockDb.updateSubmissionStatus(id, 'APPROVED')
|
||||
|
||||
revalidatePath('/admin/submissions')
|
||||
revalidatePath('/restoranlar')
|
||||
revalidatePath('/apartlar')
|
||||
revalidatePath('/isletmeler')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function rejectSubmissionAction(id: string) {
|
||||
await mockDb.updateSubmissionStatus(id, 'REJECTED')
|
||||
revalidatePath('/admin/submissions')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function deleteListingAction(id: string) {
|
||||
await mockDb.deleteListing(id)
|
||||
revalidatePath('/admin/listings')
|
||||
revalidatePath('/restoranlar')
|
||||
revalidatePath('/apartlar')
|
||||
revalidatePath('/isletmeler')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function markMessageReadAction(id: string) {
|
||||
await mockDb.markMessageAsRead(id)
|
||||
revalidatePath('/admin/messages')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function createOrUpdateListingAction(formData: FormData) {
|
||||
const id = formData.get('id') as string | null
|
||||
const slug = formData.get('slug') as string
|
||||
const categoryId = formData.get('categoryId') as string
|
||||
const neighborhoodId = formData.get('neighborhoodId') as string
|
||||
|
||||
const nameTr = formData.get('nameTr') as string
|
||||
const nameEn = formData.get('nameEn') as string
|
||||
const nameRu = formData.get('nameRu') as string
|
||||
|
||||
const descriptionTr = formData.get('descriptionTr') as string
|
||||
const descriptionEn = formData.get('descriptionEn') as string
|
||||
const descriptionRu = formData.get('descriptionRu') as string
|
||||
|
||||
const address = formData.get('address') as string
|
||||
const phone = formData.get('phone') as string || null
|
||||
const whatsapp = formData.get('whatsapp') as string || null
|
||||
const website = formData.get('website') as string || null
|
||||
const instagram = formData.get('instagram') as string || null
|
||||
|
||||
const priceRange = parseInt(formData.get('priceRange') as string)
|
||||
const rating = formData.get('rating') ? parseFloat(formData.get('rating') as string) : null
|
||||
const isLocalApproved = formData.get('isLocalApproved') === 'true'
|
||||
|
||||
const latitude = formData.get('latitude') ? parseFloat(formData.get('latitude') as string) : null
|
||||
const longitude = formData.get('longitude') ? parseFloat(formData.get('longitude') as string) : null
|
||||
|
||||
const openingHoursStr = formData.get('openingHours') as string
|
||||
const openingHours = openingHoursStr ? { all: openingHoursStr } : null
|
||||
|
||||
// Handle images
|
||||
const images: string[] = []
|
||||
|
||||
const imageFile1 = formData.get('imageFile1') as File | null
|
||||
const imageUrl1 = formData.get('imageUrl1') as string | null
|
||||
const imageFile2 = formData.get('imageFile2') as File | null
|
||||
const imageUrl2 = formData.get('imageUrl2') as string | null
|
||||
|
||||
// Process first image
|
||||
if (imageFile1 && imageFile1.size > 0) {
|
||||
try {
|
||||
const url = await uploadToOpeninary(imageFile1, `listings/${slug}`)
|
||||
images.push(url)
|
||||
} catch (e: any) {
|
||||
console.error('Image 1 upload error:', e)
|
||||
return { success: false, error: `1. Görsel yüklenemedi: ${e.message}` }
|
||||
}
|
||||
} else if (imageUrl1) {
|
||||
images.push(imageUrl1)
|
||||
}
|
||||
|
||||
// Process second image
|
||||
if (imageFile2 && imageFile2.size > 0) {
|
||||
try {
|
||||
const url = await uploadToOpeninary(imageFile2, `listings/${slug}`)
|
||||
images.push(url)
|
||||
} catch (e: any) {
|
||||
console.error('Image 2 upload error:', e)
|
||||
return { success: false, error: `2. Görsel yüklenemedi: ${e.message}` }
|
||||
}
|
||||
} else if (imageUrl2) {
|
||||
images.push(imageUrl2)
|
||||
}
|
||||
|
||||
const data = {
|
||||
slug,
|
||||
categoryId,
|
||||
neighborhoodId,
|
||||
city: 'marmaris',
|
||||
nameTr,
|
||||
nameEn,
|
||||
nameRu,
|
||||
descriptionTr,
|
||||
descriptionEn,
|
||||
descriptionRu,
|
||||
address,
|
||||
phone,
|
||||
whatsapp,
|
||||
website,
|
||||
instagram,
|
||||
priceRange,
|
||||
rating,
|
||||
isLocalApproved,
|
||||
latitude,
|
||||
longitude,
|
||||
openingHours,
|
||||
images
|
||||
}
|
||||
|
||||
try {
|
||||
if (id && id !== 'new') {
|
||||
await mockDb.updateListing(id, data)
|
||||
} else {
|
||||
await mockDb.createListing(data)
|
||||
}
|
||||
revalidatePath('/admin/listings')
|
||||
revalidatePath('/restoranlar')
|
||||
revalidatePath('/apartlar')
|
||||
revalidatePath('/isletmeler')
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message || 'Mekan kaydedilemedi.' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/lib/auth"
|
||||
|
||||
export const { GET, POST } = handlers
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+153
@@ -0,0 +1,153 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-golos);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-heading: var(--font-unbounded);
|
||||
|
||||
--color-pine: var(--pine);
|
||||
--color-turquoise: var(--turquoise);
|
||||
--color-shutter: var(--shutter);
|
||||
--color-gold: var(--gold);
|
||||
--color-bougainvillea: var(--bougainvillea);
|
||||
--color-stone: var(--stone);
|
||||
--color-stone-deep: var(--stone-deep);
|
||||
--color-paper: var(--paper);
|
||||
--color-ink: var(--ink);
|
||||
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--stone: #EDEEE3;
|
||||
--stone-deep: #E2E4D5;
|
||||
--pine: #123238;
|
||||
--turquoise: #2E9C9A;
|
||||
--shutter: #4F7C93;
|
||||
--gold: #E8A23D;
|
||||
--bougainvillea: #E85D6E;
|
||||
--ink: #21231F;
|
||||
--paper: #FBFAF6;
|
||||
|
||||
/* shadcn mappings */
|
||||
--background: var(--stone);
|
||||
--foreground: var(--ink);
|
||||
--card: var(--paper);
|
||||
--card-foreground: var(--ink);
|
||||
--popover: var(--paper);
|
||||
--popover-foreground: var(--ink);
|
||||
--primary: var(--pine);
|
||||
--primary-foreground: var(--stone);
|
||||
--secondary: var(--turquoise);
|
||||
--secondary-foreground: var(--paper);
|
||||
--muted: var(--stone-deep);
|
||||
--muted-foreground: var(--shutter);
|
||||
--accent: var(--turquoise);
|
||||
--accent-foreground: var(--paper);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: rgba(18, 50, 56, 0.08);
|
||||
--input: rgba(18, 50, 56, 0.08);
|
||||
--ring: var(--turquoise);
|
||||
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.75rem;
|
||||
|
||||
--sidebar: var(--paper);
|
||||
--sidebar-foreground: var(--ink);
|
||||
--sidebar-primary: var(--pine);
|
||||
--sidebar-primary-foreground: var(--stone);
|
||||
--sidebar-accent: var(--stone);
|
||||
--sidebar-accent-foreground: var(--pine);
|
||||
--sidebar-border: rgba(18, 50, 56, 0.08);
|
||||
--sidebar-ring: var(--turquoise);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #123238;
|
||||
--foreground: #EDEEE3;
|
||||
--card: #21231F;
|
||||
--card-foreground: #EDEEE3;
|
||||
--popover: #21231F;
|
||||
--popover-foreground: #EDEEE3;
|
||||
--primary: #2E9C9A;
|
||||
--primary-foreground: #123238;
|
||||
--secondary: #4F7C93;
|
||||
--secondary-foreground: #EDEEE3;
|
||||
--muted: #123238;
|
||||
--muted-foreground: #4F7C93;
|
||||
--accent: #2E9C9A;
|
||||
--accent-foreground: #123238;
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: rgba(237, 238, 227, 0.1);
|
||||
--input: rgba(237, 238, 227, 0.1);
|
||||
--ring: #2E9C9A;
|
||||
|
||||
--sidebar: #21231F;
|
||||
--sidebar-foreground: #EDEEE3;
|
||||
--sidebar-primary: #2E9C9A;
|
||||
--sidebar-primary-foreground: #123238;
|
||||
--sidebar-accent: #123238;
|
||||
--sidebar-accent-foreground: #EDEEE3;
|
||||
--sidebar-border: rgba(237, 238, 227, 0.1);
|
||||
--sidebar-ring: #2E9C9A;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-heading;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user