From b44acc26785a9cd52ae3cf4ef5ed9cfa211a4468 Mon Sep 17 00:00:00 2001 From: AyrisAI Date: Mon, 13 Jul 2026 14:42:07 +0300 Subject: [PATCH] b --- .../[id]/WidgetPartnerForm.tsx | 227 ++++++++++++++++++ .../admin/widget-partners/[id]/page.tsx | 28 +++ app/[locale]/admin/widget-partners/page.tsx | 115 +++++---- app/[locale]/layout.tsx | 2 +- app/[locale]/widget/page.tsx | 107 +++++++++ app/actions.ts | 37 +++ app/api/widget/nearby/route.ts | 5 +- lib/mockDb.ts | 70 +++++- prisma/schema.prisma | 10 + test-db.js | 10 + 10 files changed, 551 insertions(+), 60 deletions(-) create mode 100644 app/[locale]/admin/widget-partners/[id]/WidgetPartnerForm.tsx create mode 100644 app/[locale]/admin/widget-partners/[id]/page.tsx create mode 100644 app/[locale]/widget/page.tsx create mode 100644 test-db.js diff --git a/app/[locale]/admin/widget-partners/[id]/WidgetPartnerForm.tsx b/app/[locale]/admin/widget-partners/[id]/WidgetPartnerForm.tsx new file mode 100644 index 0000000..41566c9 --- /dev/null +++ b/app/[locale]/admin/widget-partners/[id]/WidgetPartnerForm.tsx @@ -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('') + const [loading, setLoading] = useState(false) + const [copied, setCopied] = useState(false) + const formRef = useRef(null) + + const isEditing = !!partner + + const embedCode = partner + ? `` + : '' + + 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 ( +
+
+ + + +
+

+ {isEditing ? 'widget partneri düzenle' : 'yeni widget partneri'} +

+

+ Harici web sitesi bilgilerini girin ve embed kodunu alın. +

+
+
+ + {isEditing && ( +
+
+

+ + Widget Embed Kodu +

+ +
+

+ Aşağıdaki kodu kopyalayarak partnerin web sitesine yerleştirin. Bu kod, sitenizden backlink almayı sağlar. +

+
+            {embedCode}
+          
+
+ )} + +
+ + + {error && ( +
+ + {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +

Eğer seçilirse widget sadece o bölgedeki mekanları gösterir.

+
+ +
+ +
+
+ +
+ {isEditing && ( + + )} +
+ +
+
+
+
+ ) +} diff --git a/app/[locale]/admin/widget-partners/[id]/page.tsx b/app/[locale]/admin/widget-partners/[id]/page.tsx new file mode 100644 index 0000000..0a2c6b9 --- /dev/null +++ b/app/[locale]/admin/widget-partners/[id]/page.tsx @@ -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 ( +
+ +
+ ) +} diff --git a/app/[locale]/admin/widget-partners/page.tsx b/app/[locale]/admin/widget-partners/page.tsx index 3b9faed..0ebd97b 100644 --- a/app/[locale]/admin/widget-partners/page.tsx +++ b/app/[locale]/admin/widget-partners/page.tsx @@ -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 (
-
-

widget ortakları (backlinks)

-

- Ajans sinerjisi kapsamında Marmaris Local bölgesel öneri widget'ını kendi web sitelerine yerleştiren anlaşmalı işletmeler. -

+
+
+

widget ortakları (harici)

+

+ Kendi eklediğiniz veya anlaştığınız harici siteler. Bu sitelere widget embed kodunu vererek gösterim yapabilirsiniz. +

+
+ + + Yeni Ekle +
{/* Stats row */} -
+
Toplam Partner {partners.length}
- Aktif Backlinks - {partners.filter(p => p.widgetSiteUrl).length} -
-
- Pilot Bölge - yat limanı + Aktif Olanlar + {partners.filter(p => p.isActive).length}
@@ -36,18 +41,19 @@ export default async function AdminWidgetPartnersPage() { - - - - + + + + + {partners.length === 0 ? ( - ) : ( @@ -55,45 +61,50 @@ export default async function AdminWidgetPartnersPage() { return ( - + + ) diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 0826103..652ba44 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -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(); diff --git a/app/[locale]/widget/page.tsx b/app/[locale]/widget/page.tsx new file mode 100644 index 0000000..e675f6a --- /dev/null +++ b/app/[locale]/widget/page.tsx @@ -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 ( + + ) +} diff --git a/app/actions.ts b/app/actions.ts index 0000e76..f96c914 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -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 } +} diff --git a/app/api/widget/nearby/route.ts b/app/api/widget/nearby/route.ts index f4bd640..877c041 100644 --- a/app/api/widget/nearby/route.ts +++ b/app/api/widget/nearby/route.ts @@ -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 diff --git a/lib/mockDb.ts b/lib/mockDb.ts index fa0d031..2954d5e 100644 --- a/lib/mockDb.ts +++ b/lib/mockDb.ts @@ -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) { + 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) { + 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()) { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a099e12..985fe23 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 +} diff --git a/test-db.js b/test-db.js new file mode 100644 index 0000000..0d9196d --- /dev/null +++ b/test-db.js @@ -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())
MekanMahalleKurulum TarihiMüşteri Web SitesiSite AdıSite URLKayıt TarihiBölge (Opsiyonel) Durumİşlem
- Henüz widget kurulumu yapılmış partner bulunmuyor. + + Henüz eklenmiş bir widget partneri bulunmuyor.
- - {partner.nameTr} - -
{partner.category?.nameTr}
-
- {partner.neighborhood?.nameTr} + + {partner.name} + - {partner.widgetInstalledAt ? ( -
- - {new Date(partner.widgetInstalledAt).toLocaleDateString('tr-TR')} -
- ) : ( - '-' - )} + + + {partner.url.replace('https://', '')} + +
- {partner.widgetSiteUrl ? ( - - - {partner.widgetSiteUrl.replace('https://', '')} - - - ) : ( - '-' - )} +
+ + {new Date(partner.createdAt).toLocaleDateString('tr-TR')} +
+
+ {partner.neighborhoodSlug || 'Tümü (Karışık)'} - - - widget aktif - + {partner.isActive ? ( + + + Aktif + + ) : ( + + Pasif + + )} + + + +