This commit is contained in:
AyrisAI
2026-07-13 14:42:07 +03:00
parent f37d2b4c39
commit b44acc2678
10 changed files with 551 additions and 60 deletions
@@ -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>
)
}
+63 -52
View File
@@ -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>
)
+1 -1
View File
@@ -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();
+107
View File
@@ -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>
)
}
+37
View File
@@ -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 }
}
+2 -3
View File
@@ -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
View File
@@ -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()) {
+10
View File
@@ -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
View File
@@ -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())