From 5b44c78396181b0a607e3f22578e0e402750c49b Mon Sep 17 00:00:00 2001 From: AyrisAI Date: Mon, 13 Jul 2026 13:51:48 +0300 Subject: [PATCH] feat: implement dynamic categories, admin category CRUD, fix routing and cleanup --- .../[category]/[slug]/DetailTracker.tsx | 42 ++ app/[locale]/[category]/[slug]/page.tsx | 84 ++- .../{restoranlar => [category]}/page.tsx | 26 +- app/[locale]/{hakkinda => about}/page.tsx | 2 +- .../BusinessForm.tsx | 0 .../{isletme-ekle => add-business}/page.tsx | 0 app/[locale]/admin/categories/[id]/page.tsx | 99 ++++ app/[locale]/admin/categories/page.tsx | 27 +- app/[locale]/admin/collections/page.tsx | 2 +- .../admin/events/[id]/EventFormClient.tsx | 285 ++++++++++ app/[locale]/admin/events/[id]/page.tsx | 37 ++ app/[locale]/admin/events/page.tsx | 117 ++++ app/[locale]/admin/layout.tsx | 5 +- .../admin/listings/[id]/analytics/page.tsx | 166 ++++++ app/[locale]/admin/listings/page.tsx | 10 +- .../admin/neighborhoods/DeleteButton.tsx | 23 + .../admin/neighborhoods/[id]/page.tsx | 99 ++++ app/[locale]/admin/neighborhoods/page.tsx | 71 ++- .../admin/trash/TrashActionButtons.tsx | 43 ++ app/[locale]/admin/trash/page.tsx | 87 +++ app/[locale]/admin/widget-partners/page.tsx | 108 ++++ app/[locale]/apartlar/page.tsx | 156 ------ .../{secki => collection}/[slug]/page.tsx | 2 +- .../{seckiler => collections}/page.tsx | 4 +- .../{iletisim => contact}/ContactForm.tsx | 0 app/[locale]/{iletisim => contact}/page.tsx | 0 app/[locale]/events/page.tsx | 119 ++++ app/[locale]/isletmeler/page.tsx | 156 ------ app/[locale]/layout.tsx | 5 +- .../{mahalle => neighborhood}/[slug]/page.tsx | 0 app/[locale]/page.tsx | 14 +- .../{kaydedilenler => saved}/SavedClient.tsx | 0 .../{kaydedilenler => saved}/page.tsx | 0 app/actions.ts | 269 ++++++++- app/api/events/route.ts | 26 + app/api/widget/nearby/route.ts | 61 ++ app/feed.xml/route.ts | 2 +- app/sitemap.ts | 14 +- components/DeleteButton.tsx | 24 + components/Footer.tsx | 46 +- components/LiveFilterForm.tsx | 55 ++ components/Navbar.tsx | 55 +- docs/prd-3.md | 158 ++++++ lib/mockDb.ts | 478 +++++++++++++++- messages/en.json | 31 +- messages/ru.json | 31 +- messages/tr.json | 31 +- next.config.ts | 1 + package-lock.json | 519 ++++++++++++++++++ package.json | 6 +- prisma/schema.prisma | 53 ++ prisma/seed.ts | 154 ++++++ public/test-widget.html | 65 +++ public/widget.js | 224 ++++++++ 54 files changed, 3619 insertions(+), 473 deletions(-) create mode 100644 app/[locale]/[category]/[slug]/DetailTracker.tsx rename app/[locale]/{restoranlar => [category]}/page.tsx (90%) rename app/[locale]/{hakkinda => about}/page.tsx (99%) rename app/[locale]/{isletme-ekle => add-business}/BusinessForm.tsx (100%) rename app/[locale]/{isletme-ekle => add-business}/page.tsx (100%) create mode 100644 app/[locale]/admin/categories/[id]/page.tsx create mode 100644 app/[locale]/admin/events/[id]/EventFormClient.tsx create mode 100644 app/[locale]/admin/events/[id]/page.tsx create mode 100644 app/[locale]/admin/events/page.tsx create mode 100644 app/[locale]/admin/listings/[id]/analytics/page.tsx create mode 100644 app/[locale]/admin/neighborhoods/DeleteButton.tsx create mode 100644 app/[locale]/admin/neighborhoods/[id]/page.tsx create mode 100644 app/[locale]/admin/trash/TrashActionButtons.tsx create mode 100644 app/[locale]/admin/trash/page.tsx create mode 100644 app/[locale]/admin/widget-partners/page.tsx delete mode 100644 app/[locale]/apartlar/page.tsx rename app/[locale]/{secki => collection}/[slug]/page.tsx (99%) rename app/[locale]/{seckiler => collections}/page.tsx (97%) rename app/[locale]/{iletisim => contact}/ContactForm.tsx (100%) rename app/[locale]/{iletisim => contact}/page.tsx (100%) create mode 100644 app/[locale]/events/page.tsx delete mode 100644 app/[locale]/isletmeler/page.tsx rename app/[locale]/{mahalle => neighborhood}/[slug]/page.tsx (100%) rename app/[locale]/{kaydedilenler => saved}/SavedClient.tsx (100%) rename app/[locale]/{kaydedilenler => saved}/page.tsx (100%) create mode 100644 app/api/events/route.ts create mode 100644 app/api/widget/nearby/route.ts create mode 100644 components/DeleteButton.tsx create mode 100644 components/LiveFilterForm.tsx create mode 100644 docs/prd-3.md create mode 100644 prisma/seed.ts create mode 100644 public/test-widget.html create mode 100644 public/widget.js diff --git a/app/[locale]/[category]/[slug]/DetailTracker.tsx b/app/[locale]/[category]/[slug]/DetailTracker.tsx new file mode 100644 index 0000000..3cd6355 --- /dev/null +++ b/app/[locale]/[category]/[slug]/DetailTracker.tsx @@ -0,0 +1,42 @@ +'use client' + +import { useEffect } from 'react' + +export default function DetailTracker({ listingId }: { listingId: string }) { + useEffect(() => { + // Send fire-and-forget page view tracking event on load + fetch('/api/events', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ listingId, actionType: 'views' }) + }).catch(err => console.error('Tracking views error:', err)) + + const trackClick = (action: 'phone' | 'whatsapp' | 'menu') => { + fetch('/api/events', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ listingId, actionType: action }) + }).catch(err => console.error(`Tracking ${action} error:`, err)) + } + + const telBtn = document.getElementById('listing-contact-phone') + const waBtn = document.getElementById('listing-contact-whatsapp') + const menuBtn = document.getElementById('listing-contact-menu') + + const handleTel = () => trackClick('phone') + const handleWa = () => trackClick('whatsapp') + const handleMenu = () => trackClick('menu') + + if (telBtn) telBtn.addEventListener('click', handleTel) + if (waBtn) waBtn.addEventListener('click', handleWa) + if (menuBtn) menuBtn.addEventListener('click', handleMenu) + + return () => { + if (telBtn) telBtn.removeEventListener('click', handleTel) + if (waBtn) waBtn.removeEventListener('click', handleWa) + if (menuBtn) menuBtn.removeEventListener('click', handleMenu) + } + }, [listingId]) + + return null +} diff --git a/app/[locale]/[category]/[slug]/page.tsx b/app/[locale]/[category]/[slug]/page.tsx index 8efa494..8f1f006 100644 --- a/app/[locale]/[category]/[slug]/page.tsx +++ b/app/[locale]/[category]/[slug]/page.tsx @@ -6,16 +6,19 @@ import Image from 'next/image' import { Link } from '@/i18n/routing' import { Phone, Globe, MapPin, Clock, Star, MessageSquare, Share2 } from 'lucide-react' import SaveButton from './SaveButton' +import DetailTracker from './DetailTracker' interface DetailPageProps { params: Promise<{ locale: string; category: string; slug: string }> } +export const dynamic = 'force-dynamic' + export async function generateMetadata({ params }: DetailPageProps) { const { slug } = await params const listing = await mockDb.getListingBySlug(slug) if (!listing) return {} - + return { title: `${listing.nameTr} — Marmaris Local`, description: listing.descriptionTr @@ -27,7 +30,7 @@ export default async function ListingDetailPage({ params }: DetailPageProps) { setRequestLocale(locale) const t = await getTranslations('detail') - + const listing = await mockDb.getListingBySlug(slug) if (!listing) { notFound() @@ -56,19 +59,21 @@ export default async function ListingDetailPage({ params }: DetailPageProps) { locale === 'ru' ? listing.nameRu : locale === 'en' - ? listing.nameEn - : listing.nameTr + ? listing.nameEn + : listing.nameTr const description = locale === 'ru' ? listing.descriptionRu : locale === 'en' - ? listing.descriptionEn - : listing.descriptionTr + ? listing.descriptionEn + : listing.descriptionTr const priceSymbols = '₺'.repeat(listing.priceRange) const categorySlug = listing.category?.slug || 'isletme' - + const rawId = listing.id; // örn: "gm-ChIJY_2Q8tbJvxQRs7xwaSne0is" + const hasGooglePlaceId = rawId.startsWith('gm-'); + const placeId = hasGooglePlaceId ? rawId.replace('gm-', '') : null; // Format WhatsApp Link const getWhatsAppLink = (number: string) => { const cleanNum = number.replace(/\D/g, '') @@ -83,9 +88,10 @@ export default async function ListingDetailPage({ params }: DetailPageProps) { return (
+
- + {/* Breadcrumb */}
marmaris local @@ -100,7 +106,7 @@ export default async function ListingDetailPage({ params }: DetailPageProps) { {/* Hero Details Block */}
- + {/* Left: Gallery Panel */}
@@ -137,7 +143,7 @@ export default async function ListingDetailPage({ params }: DetailPageProps) { {locale === 'ru' ? listing.category?.nameRu : locale === 'en' ? listing.category?.nameEn : listing.category?.nameTr} - + {listing.isLocalApproved && ( ★ {t('approved')} @@ -165,16 +171,50 @@ export default async function ListingDetailPage({ params }: DetailPageProps) { {/* Description */}
- {description} + {description ? description : {t('no_description', { defaultValue: 'Bu işletme için henüz bir açıklama eklenmemiştir.' })}}
+ {/* Contact Info (Only if at least one exists) */} + {(listing.phone || listing.website || listing.instagram) && ( + + )} + {/* Contact & Actions Grid */}

{t('contact')}

- +
{listing.phone && ( @@ -185,6 +225,7 @@ export default async function ListingDetailPage({ params }: DetailPageProps) { {listing.whatsapp && ( {/* Map Frame */} - {listing.latitude && listing.longitude && ( + {((listing.latitude && listing.longitude) || hasGooglePlaceId) && (
{t('location')}
-
+