b
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import { useRouter } from '@/i18n/routing'
|
||||
import { saveWidgetPartnerAction } from '@/app/actions'
|
||||
import { CheckCircle, XCircle, ArrowLeft, Copy, Code } from 'lucide-react'
|
||||
import { Link } from '@/i18n/routing'
|
||||
import { WidgetPartner } from '@prisma/client'
|
||||
|
||||
type Neighborhood = {
|
||||
id: string
|
||||
slug: string
|
||||
nameTr: string
|
||||
}
|
||||
|
||||
export default function WidgetPartnerForm({
|
||||
partner,
|
||||
neighborhoods,
|
||||
locale
|
||||
}: {
|
||||
partner: WidgetPartner | null
|
||||
neighborhoods: Neighborhood[]
|
||||
locale: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const [error, setError] = useState<string>('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
|
||||
const isEditing = !!partner
|
||||
|
||||
const embedCode = partner
|
||||
? `<iframe
|
||||
src="https://marmarislocal.com/tr/widget${partner.neighborhoodSlug ? `?neighborhood=${partner.neighborhoodSlug}` : ''}"
|
||||
width="100%"
|
||||
height="600"
|
||||
style="border:none; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"
|
||||
title="Marmaris Local Önerileri"
|
||||
></iframe>`
|
||||
: ''
|
||||
|
||||
const handleCopy = () => {
|
||||
if (embedCode) {
|
||||
navigator.clipboard.writeText(embedCode)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
if (!formRef.current) return
|
||||
|
||||
try {
|
||||
const formData = new FormData(formRef.current)
|
||||
|
||||
const result = await saveWidgetPartnerAction(formData)
|
||||
if (result.error) {
|
||||
setError(result.error)
|
||||
} else {
|
||||
router.push('/admin/widget-partners')
|
||||
router.refresh()
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Bir hata oluştu')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin/widget-partners" className="p-2 hover:bg-stone/20 rounded-full transition-colors">
|
||||
<ArrowLeft className="w-5 h-5 text-shutter" />
|
||||
</Link>
|
||||
<div>
|
||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">
|
||||
{isEditing ? 'widget partneri düzenle' : 'yeni widget partneri'}
|
||||
</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
Harici web sitesi bilgilerini girin ve embed kodunu alın.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="bg-pine/5 border border-pine/10 p-6 rounded-2xl">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-heading font-bold text-pine flex items-center gap-2">
|
||||
<Code className="w-5 h-5 text-turquoise" />
|
||||
Widget Embed Kodu
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-pine/10 rounded-lg text-xs font-bold text-pine hover:border-turquoise hover:text-turquoise transition-colors"
|
||||
>
|
||||
{copied ? <CheckCircle className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? 'Kopyalandı' : 'Kodu Kopyala'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-ink/70 mb-4">
|
||||
Aşağıdaki kodu kopyalayarak partnerin web sitesine yerleştirin. Bu kod, sitenizden backlink almayı sağlar.
|
||||
</p>
|
||||
<pre className="bg-stone-deep/80 text-paper p-4 rounded-xl text-xs overflow-x-auto font-mono whitespace-pre-wrap">
|
||||
{embedCode}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form ref={formRef} onSubmit={handleSubmit} className="bg-paper border border-pine/8 rounded-2xl p-6 shadow-sm space-y-6">
|
||||
<input type="hidden" name="id" value={partner?.id || 'new'} />
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-bougainvillea/10 text-bougainvillea rounded-xl text-sm flex items-start gap-3">
|
||||
<XCircle className="w-5 h-5 shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="name" className="text-xs font-bold text-pine block">Site Adı <span className="text-bougainvillea">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
defaultValue={partner?.name || ''}
|
||||
className="w-full bg-stone/30 border border-pine/10 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-turquoise focus:ring-1 focus:ring-turquoise transition-all"
|
||||
placeholder="Örn: Marmaris Haber"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="url" className="text-xs font-bold text-pine block">Site Linki (URL) <span className="text-bougainvillea">*</span></label>
|
||||
<input
|
||||
type="url"
|
||||
id="url"
|
||||
name="url"
|
||||
required
|
||||
defaultValue={partner?.url || ''}
|
||||
className="w-full bg-stone/30 border border-pine/10 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-turquoise focus:ring-1 focus:ring-turquoise transition-all"
|
||||
placeholder="https://marmarishaber.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="neighborhoodSlug" className="text-xs font-bold text-pine block">Hangi Bölgenin Widget'ı?</label>
|
||||
<select
|
||||
id="neighborhoodSlug"
|
||||
name="neighborhoodSlug"
|
||||
defaultValue={partner?.neighborhoodSlug || ''}
|
||||
className="w-full bg-stone/30 border border-pine/10 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-turquoise focus:ring-1 focus:ring-turquoise transition-all"
|
||||
>
|
||||
<option value="">Tüm Marmaris (Karışık)</option>
|
||||
{neighborhoods.map(n => (
|
||||
<option key={n.id} value={n.slug}>{n.nameTr}</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-[10px] text-shutter mt-1">Eğer seçilirse widget sadece o bölgedeki mekanları gösterir.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center pt-6">
|
||||
<label className="flex items-center gap-3 cursor-pointer group">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="isActive"
|
||||
value="true"
|
||||
defaultChecked={partner ? partner.isActive : true}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-stone-deep/20 rounded-full peer peer-checked:bg-turquoise transition-colors duration-300"></div>
|
||||
<div className="absolute top-1 left-1 bg-white w-4 h-4 rounded-full transition-transform duration-300 peer-checked:translate-x-5 shadow-sm"></div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-bold text-pine block group-hover:text-turquoise transition-colors">Aktif Durum</span>
|
||||
<span className="text-[10px] text-shutter">Bu widget yayında mı?</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4 border-t border-pine/8">
|
||||
{isEditing && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
if (confirm('Bu partneri silmek istediğinize emin misiniz?')) {
|
||||
setLoading(true)
|
||||
try {
|
||||
await import('@/app/actions').then(m => m.deleteWidgetPartnerAction(partner!.id))
|
||||
router.push('/admin/widget-partners')
|
||||
router.refresh()
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="text-bougainvillea hover:text-bougainvillea/80 px-4 py-2 text-sm font-bold transition-colors"
|
||||
>
|
||||
Sil
|
||||
</button>
|
||||
)}
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-pine text-white px-6 py-2.5 rounded-xl font-bold text-sm hover:bg-turquoise transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{loading ? 'Kaydediliyor...' : 'Kaydet'}
|
||||
{!loading && <CheckCircle className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import WidgetPartnerForm from './WidgetPartnerForm'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function AdminWidgetPartnerEditPage(props: {
|
||||
params: Promise<{ id: string, locale: string }>
|
||||
}) {
|
||||
const params = await props.params
|
||||
const t = await getTranslations('Admin')
|
||||
let partner = null
|
||||
|
||||
if (params.id !== 'new') {
|
||||
partner = await mockDb.getWidgetPartner(params.id)
|
||||
if (!partner) {
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
||||
// Get neighborhoods for the dropdown
|
||||
const neighborhoods = await mockDb.getNeighborhoods()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<WidgetPartnerForm partner={partner} neighborhoods={neighborhoods} locale={params.locale} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +1,37 @@
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import { Link } from '@/i18n/routing'
|
||||
import { Globe, Calendar, CheckCircle, ExternalLink } from 'lucide-react'
|
||||
import { Globe, Calendar, CheckCircle, ExternalLink, Plus, Edit } from 'lucide-react'
|
||||
|
||||
export default async function AdminWidgetPartnersPage() {
|
||||
const partners = await mockDb.getWidgetPartners()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">widget ortakları (backlinks)</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
Ajans sinerjisi kapsamında Marmaris Local bölgesel öneri widget'ını kendi web sitelerine yerleştiren anlaşmalı işletmeler.
|
||||
</p>
|
||||
<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">widget ortakları (harici)</h2>
|
||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||
Kendi eklediğiniz veya anlaştığınız harici siteler. Bu sitelere widget embed kodunu vererek gösterim yapabilirsiniz.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/widget-partners/new"
|
||||
className="inline-flex items-center justify-center gap-2 bg-pine text-white px-5 py-2.5 rounded-xl font-bold text-sm hover:bg-turquoise transition-colors shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Yeni Ekle
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="bg-paper p-6 rounded-2xl border border-pine/8 shadow-sm">
|
||||
<span className="text-[10px] font-mono text-shutter uppercase tracking-wider block mb-1">Toplam Partner</span>
|
||||
<span className="text-3xl font-heading font-extrabold text-pine leading-none">{partners.length}</span>
|
||||
</div>
|
||||
<div className="bg-paper p-6 rounded-2xl border border-pine/8 shadow-sm">
|
||||
<span className="text-[10px] font-mono text-shutter uppercase tracking-wider block mb-1">Aktif Backlinks</span>
|
||||
<span className="text-3xl font-heading font-extrabold text-turquoise leading-none">{partners.filter(p => p.widgetSiteUrl).length}</span>
|
||||
</div>
|
||||
<div className="bg-paper p-6 rounded-2xl border border-pine/8 shadow-sm">
|
||||
<span className="text-[10px] font-mono text-shutter uppercase tracking-wider block mb-1">Pilot Bölge</span>
|
||||
<span className="text-3xl font-heading font-extrabold text-bougainvillea leading-none">yat limanı</span>
|
||||
<span className="text-[10px] font-mono text-shutter uppercase tracking-wider block mb-1">Aktif Olanlar</span>
|
||||
<span className="text-3xl font-heading font-extrabold text-turquoise leading-none">{partners.filter(p => p.isActive).length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,18 +41,19 @@ export default async function AdminWidgetPartnersPage() {
|
||||
<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">Mekan</th>
|
||||
<th className="px-6 py-4 text-left">Mahalle</th>
|
||||
<th className="px-6 py-4 text-left">Kurulum Tarihi</th>
|
||||
<th className="px-6 py-4 text-left">Müşteri Web Sitesi</th>
|
||||
<th className="px-6 py-4 text-left">Site Adı</th>
|
||||
<th className="px-6 py-4 text-left">Site URL</th>
|
||||
<th className="px-6 py-4 text-left">Kayıt Tarihi</th>
|
||||
<th className="px-6 py-4 text-left">Bölge (Opsiyonel)</th>
|
||||
<th className="px-6 py-4 text-left">Durum</th>
|
||||
<th className="px-6 py-4 text-right">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dashed divide-pine/8 text-ink/80">
|
||||
{partners.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-12 text-center text-xs text-shutter font-medium">
|
||||
Henüz widget kurulumu yapılmış partner bulunmuyor.
|
||||
<td colSpan={6} className="px-6 py-12 text-center text-xs text-shutter font-medium">
|
||||
Henüz eklenmiş bir widget partneri bulunmuyor.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
@@ -55,45 +61,50 @@ export default async function AdminWidgetPartnersPage() {
|
||||
return (
|
||||
<tr key={partner.id} className="hover:bg-stone/20 transition duration-150">
|
||||
<td className="px-6 py-4 font-medium">
|
||||
<Link href={`/admin/listings/${partner.id}`} className="font-heading font-bold text-pine lowercase text-sm hover:text-turquoise transition-colors">
|
||||
{partner.nameTr}
|
||||
</Link>
|
||||
<div className="text-[10px] text-shutter font-mono uppercase tracking-wider mt-0.5">{partner.category?.nameTr}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-medium text-xs">
|
||||
{partner.neighborhood?.nameTr}
|
||||
<span className="font-heading font-bold text-pine lowercase text-sm">
|
||||
{partner.name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-mono text-xs">
|
||||
{partner.widgetInstalledAt ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="w-3.5 h-3.5 text-shutter/60" />
|
||||
{new Date(partner.widgetInstalledAt).toLocaleDateString('tr-TR')}
|
||||
</div>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
<a
|
||||
href={partner.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-turquoise hover:underline flex items-center gap-1 font-medium"
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5 shrink-0" />
|
||||
{partner.url.replace('https://', '')}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-mono text-xs">
|
||||
{partner.widgetSiteUrl ? (
|
||||
<a
|
||||
href={partner.widgetSiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-turquoise hover:underline flex items-center gap-1 font-medium"
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5 shrink-0" />
|
||||
{partner.widgetSiteUrl.replace('https://', '')}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="w-3.5 h-3.5 text-shutter/60" />
|
||||
{new Date(partner.createdAt).toLocaleDateString('tr-TR')}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-medium text-xs text-shutter">
|
||||
{partner.neighborhoodSlug || 'Tümü (Karışık)'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<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" />
|
||||
widget aktif
|
||||
</span>
|
||||
{partner.isActive ? (
|
||||
<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" />
|
||||
Aktif
|
||||
</span>
|
||||
) : (
|
||||
<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-shutter border border-shutter/20 uppercase tracking-wider">
|
||||
Pasif
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||
<Link
|
||||
href={`/admin/widget-partners/${partner.id}`}
|
||||
className="inline-flex items-center justify-center p-2 text-shutter hover:text-turquoise hover:bg-turquoise/10 rounded-lg transition-colors"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -96,7 +96,7 @@ export default async function RootLayout({
|
||||
|
||||
const headersList = await headers();
|
||||
const pathname = headersList.get('x-pathname') || '';
|
||||
const hideNavbarFooter = pathname.includes('/admin') || pathname.includes('/login');
|
||||
const hideNavbarFooter = pathname.includes('/admin') || pathname.includes('/login') || pathname.includes('/widget');
|
||||
|
||||
const categories = await mockDb.getCategories();
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import { MapPin, Star, ExternalLink } from 'lucide-react'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Marmaris Local Widget'
|
||||
}
|
||||
|
||||
export default async function WidgetPage(props: {
|
||||
params: Promise<{ locale: string }>,
|
||||
searchParams: Promise<{ neighborhood?: string }>
|
||||
}) {
|
||||
const { locale } = await props.params
|
||||
const searchParams = await props.searchParams
|
||||
const neighborhoodSlug = searchParams.neighborhood
|
||||
|
||||
// Get listings (veritabanında Local Approved mekan olmadığı için şimdilik tümünü çekiyoruz)
|
||||
let listings = await mockDb.getListings()
|
||||
|
||||
if (neighborhoodSlug) {
|
||||
const neighborhood = await mockDb.getNeighborhoodBySlug(neighborhoodSlug)
|
||||
if (neighborhood) {
|
||||
listings = listings.filter(l => l.neighborhoodId === neighborhood.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle and limit to 5
|
||||
listings = listings.sort(() => 0.5 - Math.random()).slice(0, 5)
|
||||
|
||||
return (
|
||||
<div className="bg-paper min-h-screen p-4 flex flex-col font-sans">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="font-heading font-extrabold text-pine text-lg tracking-tight lowercase">
|
||||
marmaris local
|
||||
</h1>
|
||||
<p className="text-[10px] text-shutter font-medium">Yerel Seçimler</p>
|
||||
</div>
|
||||
<a
|
||||
href={`https://marmarislocal.com/${locale}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-pine/5 text-pine hover:bg-pine/10 p-2 rounded-xl transition-colors"
|
||||
title="Tümünü Gör"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 flex-1 overflow-y-auto pr-1 custom-scrollbar">
|
||||
{listings.map(listing => (
|
||||
<a
|
||||
key={listing.id}
|
||||
href={`https://marmarislocal.com/${locale}/${listing.category?.slug}/${listing.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex gap-3 p-3 bg-white border border-pine/5 rounded-2xl hover:border-turquoise hover:shadow-sm transition-all group"
|
||||
>
|
||||
<div className="w-20 h-20 shrink-0 rounded-xl overflow-hidden bg-stone">
|
||||
<img
|
||||
src={listing.images?.[0]?.url || 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=400&q=80'}
|
||||
alt={listing.nameTr}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 py-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="font-heading font-bold text-pine text-sm truncate group-hover:text-turquoise transition-colors">
|
||||
{locale === 'en' ? listing.nameEn || listing.nameTr : locale === 'ru' ? listing.nameRu || listing.nameTr : listing.nameTr}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5 mt-1 text-[10px] font-medium text-shutter">
|
||||
<span className="bg-stone-deep px-1.5 py-0.5 rounded-md">
|
||||
{locale === 'en' ? listing.category?.nameEn : locale === 'ru' ? listing.category?.nameRu : listing.category?.nameTr}
|
||||
</span>
|
||||
<span className="flex items-center gap-0.5">
|
||||
<MapPin className="w-3 h-3" />
|
||||
{locale === 'en' ? listing.neighborhood?.nameEn : locale === 'ru' ? listing.neighborhood?.nameRu : listing.neighborhood?.nameTr}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex items-center gap-1 bg-yellow-400/10 px-1.5 py-0.5 rounded-md">
|
||||
<Star className="w-3 h-3 text-yellow-500 fill-yellow-500" />
|
||||
<span className="text-[10px] font-bold text-yellow-600">{(listing.rating || 0).toFixed(1)}</span>
|
||||
</div>
|
||||
<div className="text-[10px] font-bold tracking-widest text-turquoise">
|
||||
{'₺'.repeat(listing.priceRange || 1)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-pine/10 text-center">
|
||||
<a
|
||||
href={`https://marmarislocal.com/${locale}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[10px] font-bold text-pine hover:text-turquoise transition-colors"
|
||||
>
|
||||
marmarislocal.com'da keşfet
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -645,3 +645,40 @@ export async function searchListingsAction(query: string) {
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
export async function saveWidgetPartnerAction(formData: FormData) {
|
||||
const id = formData.get('id') as string | null
|
||||
const name = formData.get('name') as string
|
||||
const url = formData.get('url') as string
|
||||
const neighborhoodSlug = formData.get('neighborhoodSlug') as string | null
|
||||
const isActive = formData.get('isActive') === 'true'
|
||||
|
||||
if (!name || !url) {
|
||||
return { error: 'Lütfen zorunlu alanları doldurun.' }
|
||||
}
|
||||
|
||||
const data = {
|
||||
name,
|
||||
url,
|
||||
neighborhoodSlug: neighborhoodSlug || null,
|
||||
isActive
|
||||
}
|
||||
|
||||
try {
|
||||
if (id && id !== 'new') {
|
||||
await mockDb.updateWidgetPartner(id, data)
|
||||
} else {
|
||||
await mockDb.createWidgetPartner(data)
|
||||
}
|
||||
revalidatePath('/admin/widget-partners')
|
||||
return { success: true, error: undefined }
|
||||
} catch (error: any) {
|
||||
return { error: error.message || 'Bir hata oluştu.' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteWidgetPartnerAction(id: string) {
|
||||
await mockDb.deleteWidgetPartner(id)
|
||||
revalidatePath('/admin/widget-partners')
|
||||
return { success: true, error: undefined }
|
||||
}
|
||||
|
||||
@@ -20,10 +20,9 @@ export async function GET(req: NextRequest) {
|
||||
})
|
||||
}
|
||||
|
||||
// Get listings in that neighborhood that are local approved
|
||||
// Get listings in that neighborhood
|
||||
const listings = await mockDb.getListings({
|
||||
neighborhoodId: neighborhood.id,
|
||||
isLocalApproved: true
|
||||
neighborhoodId: neighborhood.id
|
||||
})
|
||||
|
||||
// Format response matching widget needs
|
||||
|
||||
+66
-4
@@ -127,6 +127,16 @@ export interface Collection {
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface WidgetPartner {
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
neighborhoodSlug: string | null
|
||||
isActive: boolean
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface InstagramFeedCache {
|
||||
id: string
|
||||
listingId: string
|
||||
@@ -191,6 +201,7 @@ const globalForMockDb = globalThis as unknown as {
|
||||
instagramFeedCaches: InstagramFeedCache[]
|
||||
listingAnalyticsDaily: ListingAnalyticsDaily[]
|
||||
events: Event[]
|
||||
widgetPartners: WidgetPartner[]
|
||||
generatedItineraries: GeneratedItinerary[]
|
||||
initialized: boolean
|
||||
__version: number
|
||||
@@ -586,6 +597,8 @@ if (!globalForMockDb.initialized || globalForMockDb.__version !== MOCK_DB_VERSIO
|
||||
]
|
||||
|
||||
// Seed Events
|
||||
globalForMockDb.widgetPartners = []
|
||||
|
||||
globalForMockDb.events = [
|
||||
{
|
||||
id: 'evt-1',
|
||||
@@ -1307,14 +1320,63 @@ export const mockDb = {
|
||||
// Phase 3 Widget & Backlink
|
||||
async getWidgetPartners() {
|
||||
if (this.isMock()) {
|
||||
return globalForMockDb.listings.filter(l => l.hasWidgetInstalled && !l.deletedAt)
|
||||
return globalForMockDb.widgetPartners.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
}
|
||||
return db.listing.findMany({
|
||||
where: { hasWidgetInstalled: true, deletedAt: null },
|
||||
include: { category: true, neighborhood: true }
|
||||
return db.widgetPartner.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
},
|
||||
|
||||
async getWidgetPartner(id: string) {
|
||||
if (this.isMock()) {
|
||||
return globalForMockDb.widgetPartners.find(w => w.id === id) || null
|
||||
}
|
||||
return db.widgetPartner.findUnique({ where: { id } })
|
||||
},
|
||||
|
||||
async createWidgetPartner(data: Omit<WidgetPartner, 'id' | 'createdAt' | 'updatedAt'>) {
|
||||
if (this.isMock()) {
|
||||
const newPartner: WidgetPartner = {
|
||||
id: `wp-${Date.now()}`,
|
||||
...data,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
globalForMockDb.widgetPartners.push(newPartner)
|
||||
return newPartner
|
||||
}
|
||||
return db.widgetPartner.create({ data })
|
||||
},
|
||||
|
||||
async updateWidgetPartner(id: string, data: Partial<WidgetPartner>) {
|
||||
if (this.isMock()) {
|
||||
const idx = globalForMockDb.widgetPartners.findIndex(w => w.id === id)
|
||||
if (idx !== -1) {
|
||||
globalForMockDb.widgetPartners[idx] = {
|
||||
...globalForMockDb.widgetPartners[idx],
|
||||
...data,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
return globalForMockDb.widgetPartners[idx]
|
||||
}
|
||||
return null
|
||||
}
|
||||
return db.widgetPartner.update({ where: { id }, data })
|
||||
},
|
||||
|
||||
async deleteWidgetPartner(id: string) {
|
||||
if (this.isMock()) {
|
||||
const idx = globalForMockDb.widgetPartners.findIndex(w => w.id === id)
|
||||
if (idx !== -1) {
|
||||
globalForMockDb.widgetPartners.splice(idx, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
await db.widgetPartner.delete({ where: { id } })
|
||||
return true
|
||||
},
|
||||
|
||||
// Phase 3 Analytics daily aggregates
|
||||
async getAnalytics(listingId: string, startDate?: Date, endDate?: Date) {
|
||||
if (this.isMock()) {
|
||||
|
||||
@@ -263,3 +263,13 @@ model GeneratedItinerary {
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model WidgetPartner {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
url String
|
||||
neighborhoodSlug String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
const allListings = await prisma.listing.count()
|
||||
const localApproved = await prisma.listing.count({ where: { isLocalApproved: true } })
|
||||
console.log(`Total listings: ${allListings}, Local Approved: ${localApproved}`)
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect())
|
||||
Reference in New Issue
Block a user