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 { mockDb } from '@/lib/mockDb'
|
||||||
import { Link } from '@/i18n/routing'
|
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() {
|
export default async function AdminWidgetPartnersPage() {
|
||||||
const partners = await mockDb.getWidgetPartners()
|
const partners = await mockDb.getWidgetPartners()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">widget ortakları (backlinks)</h2>
|
<div>
|
||||||
<p className="text-ink/65 text-xs font-medium mt-1">
|
<h2 className="text-3xl font-heading font-extrabold text-pine lowercase">widget ortakları (harici)</h2>
|
||||||
Ajans sinerjisi kapsamında Marmaris Local bölgesel öneri widget'ını kendi web sitelerine yerleştiren anlaşmalı işletmeler.
|
<p className="text-ink/65 text-xs font-medium mt-1">
|
||||||
</p>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Stats row */}
|
{/* 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">
|
<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-[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>
|
<span className="text-3xl font-heading font-extrabold text-pine leading-none">{partners.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-paper p-6 rounded-2xl border border-pine/8 shadow-sm">
|
<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-[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.widgetSiteUrl).length}</span>
|
<span className="text-3xl font-heading font-extrabold text-turquoise leading-none">{partners.filter(p => p.isActive).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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -36,18 +41,19 @@ export default async function AdminWidgetPartnersPage() {
|
|||||||
<table className="min-w-full divide-y divide-pine/8 text-sm">
|
<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">
|
<thead className="bg-stone-deep/40 text-shutter font-mono text-[10px] uppercase tracking-wider">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-4 text-left">Mekan</th>
|
<th className="px-6 py-4 text-left">Site Adı</th>
|
||||||
<th className="px-6 py-4 text-left">Mahalle</th>
|
<th className="px-6 py-4 text-left">Site URL</th>
|
||||||
<th className="px-6 py-4 text-left">Kurulum Tarihi</th>
|
<th className="px-6 py-4 text-left">Kayıt Tarihi</th>
|
||||||
<th className="px-6 py-4 text-left">Müşteri Web Sitesi</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-left">Durum</th>
|
||||||
|
<th className="px-6 py-4 text-right">İşlem</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-dashed divide-pine/8 text-ink/80">
|
<tbody className="divide-y divide-dashed divide-pine/8 text-ink/80">
|
||||||
{partners.length === 0 ? (
|
{partners.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="px-6 py-12 text-center text-xs text-shutter font-medium">
|
<td colSpan={6} className="px-6 py-12 text-center text-xs text-shutter font-medium">
|
||||||
Henüz widget kurulumu yapılmış partner bulunmuyor.
|
Henüz eklenmiş bir widget partneri bulunmuyor.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
@@ -55,45 +61,50 @@ export default async function AdminWidgetPartnersPage() {
|
|||||||
return (
|
return (
|
||||||
<tr key={partner.id} className="hover:bg-stone/20 transition duration-150">
|
<tr key={partner.id} className="hover:bg-stone/20 transition duration-150">
|
||||||
<td className="px-6 py-4 font-medium">
|
<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">
|
<span className="font-heading font-bold text-pine lowercase text-sm">
|
||||||
{partner.nameTr}
|
{partner.name}
|
||||||
</Link>
|
</span>
|
||||||
<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}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 font-mono text-xs">
|
<td className="px-6 py-4 font-mono text-xs">
|
||||||
{partner.widgetInstalledAt ? (
|
<a
|
||||||
<div className="flex items-center gap-1">
|
href={partner.url}
|
||||||
<Calendar className="w-3.5 h-3.5 text-shutter/60" />
|
target="_blank"
|
||||||
{new Date(partner.widgetInstalledAt).toLocaleDateString('tr-TR')}
|
rel="noopener noreferrer"
|
||||||
</div>
|
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>
|
||||||
<td className="px-6 py-4 font-mono text-xs">
|
<td className="px-6 py-4 font-mono text-xs">
|
||||||
{partner.widgetSiteUrl ? (
|
<div className="flex items-center gap-1">
|
||||||
<a
|
<Calendar className="w-3.5 h-3.5 text-shutter/60" />
|
||||||
href={partner.widgetSiteUrl}
|
{new Date(partner.createdAt).toLocaleDateString('tr-TR')}
|
||||||
target="_blank"
|
</div>
|
||||||
rel="noopener noreferrer"
|
</td>
|
||||||
className="text-turquoise hover:underline flex items-center gap-1 font-medium"
|
<td className="px-6 py-4 font-medium text-xs text-shutter">
|
||||||
>
|
{partner.neighborhoodSlug || 'Tümü (Karışık)'}
|
||||||
<Globe className="w-3.5 h-3.5 shrink-0" />
|
|
||||||
{partner.widgetSiteUrl.replace('https://', '')}
|
|
||||||
<ExternalLink className="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<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">
|
{partner.isActive ? (
|
||||||
<CheckCircle className="w-3.5 h-3.5" />
|
<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">
|
||||||
widget aktif
|
<CheckCircle className="w-3.5 h-3.5" />
|
||||||
</span>
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export default async function RootLayout({
|
|||||||
|
|
||||||
const headersList = await headers();
|
const headersList = await headers();
|
||||||
const pathname = headersList.get('x-pathname') || '';
|
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();
|
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({
|
const listings = await mockDb.getListings({
|
||||||
neighborhoodId: neighborhood.id,
|
neighborhoodId: neighborhood.id
|
||||||
isLocalApproved: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Format response matching widget needs
|
// Format response matching widget needs
|
||||||
|
|||||||
+66
-4
@@ -127,6 +127,16 @@ export interface Collection {
|
|||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WidgetPartner {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
neighborhoodSlug: string | null
|
||||||
|
isActive: boolean
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
export interface InstagramFeedCache {
|
export interface InstagramFeedCache {
|
||||||
id: string
|
id: string
|
||||||
listingId: string
|
listingId: string
|
||||||
@@ -191,6 +201,7 @@ const globalForMockDb = globalThis as unknown as {
|
|||||||
instagramFeedCaches: InstagramFeedCache[]
|
instagramFeedCaches: InstagramFeedCache[]
|
||||||
listingAnalyticsDaily: ListingAnalyticsDaily[]
|
listingAnalyticsDaily: ListingAnalyticsDaily[]
|
||||||
events: Event[]
|
events: Event[]
|
||||||
|
widgetPartners: WidgetPartner[]
|
||||||
generatedItineraries: GeneratedItinerary[]
|
generatedItineraries: GeneratedItinerary[]
|
||||||
initialized: boolean
|
initialized: boolean
|
||||||
__version: number
|
__version: number
|
||||||
@@ -586,6 +597,8 @@ if (!globalForMockDb.initialized || globalForMockDb.__version !== MOCK_DB_VERSIO
|
|||||||
]
|
]
|
||||||
|
|
||||||
// Seed Events
|
// Seed Events
|
||||||
|
globalForMockDb.widgetPartners = []
|
||||||
|
|
||||||
globalForMockDb.events = [
|
globalForMockDb.events = [
|
||||||
{
|
{
|
||||||
id: 'evt-1',
|
id: 'evt-1',
|
||||||
@@ -1307,14 +1320,63 @@ export const mockDb = {
|
|||||||
// Phase 3 Widget & Backlink
|
// Phase 3 Widget & Backlink
|
||||||
async getWidgetPartners() {
|
async getWidgetPartners() {
|
||||||
if (this.isMock()) {
|
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({
|
return db.widgetPartner.findMany({
|
||||||
where: { hasWidgetInstalled: true, deletedAt: null },
|
orderBy: { createdAt: 'desc' }
|
||||||
include: { category: true, neighborhood: true }
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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
|
// Phase 3 Analytics daily aggregates
|
||||||
async getAnalytics(listingId: string, startDate?: Date, endDate?: Date) {
|
async getAnalytics(listingId: string, startDate?: Date, endDate?: Date) {
|
||||||
if (this.isMock()) {
|
if (this.isMock()) {
|
||||||
|
|||||||
@@ -263,3 +263,13 @@ model GeneratedItinerary {
|
|||||||
|
|
||||||
createdAt DateTime @default(now())
|
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