Files
marmarislocal/lib/mockDb.ts
T

1531 lines
55 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { db } from './db'
export interface Category {
id: string
slug: string
nameTr: string
nameEn: string
nameRu: string
createdAt: Date
updatedAt: Date
}
export interface Neighborhood {
id: string
slug: string
nameTr: string
nameEn: string
nameRu: string
}
export interface Gallery {
id: string
listingId: string
url: string
}
export interface Listing {
id: string
slug: string
categoryId: string
neighborhoodId: string
city: string
nameTr: string
nameEn: string
nameRu: string
descriptionTr: string
descriptionEn: string
descriptionRu: string
address: string
phone?: string | null
whatsapp?: string | null
website?: string | null
instagram?: string | null
priceRange: number
rating?: number | null
isLocalApproved: boolean
latitude?: number | null
longitude?: number | null
openingHours?: any | null
images: Gallery[]
category?: Category
neighborhood?: Neighborhood
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
// Phase 2
menuUrl?: string | null
isFeatured: boolean
featuredUntil?: Date | null
// Phase 3
hasWidgetInstalled: boolean
widgetInstalledAt?: Date | null
widgetSiteUrl?: string | null
}
export interface BusinessSubmission {
id: string
businessName: string
categoryId: string
neighborhoodId: string
address: string
phone?: string | null
whatsapp?: string | null
description: string
contactName: string
contactEmail: string
imageUrl?: string | null
status: string
createdAt: Date
updatedAt: Date
}
export interface ContactMessage {
id: string
name: string
email: string
subject: string
message: string
isRead: boolean
createdAt: Date
updatedAt: Date
}
export interface BlogPost {
id: string
slug: string
titleTr: string
titleEn: string
titleRu: string
contentTr: string
contentEn: string
contentRu: string
coverImage?: string | null
tags: string[]
relatedListingIds: string[]
publishedAt?: Date | null
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
}
export interface Collection {
id: string
slug: string
titleTr: string
titleEn: string
titleRu: string
descriptionTr: string
descriptionEn: string
descriptionRu: string
coverImage?: string | null
listings?: Listing[]
listingIds: string[]
createdAt: Date
updatedAt: Date
}
export interface InstagramFeedCache {
id: string
listingId: string
handle: string
posts: Array<{
imageUrl: string
caption?: string
permalink?: string
postedAt: string
}>
fetchedAt: Date
}
export interface ListingAnalyticsDaily {
id: string
listingId: string
date: Date
views: number
whatsappClicks: number
phoneClicks: number
menuClicks: number
}
export interface Event {
id: string
slug: string
listingId?: string | null
titleTr: string
titleEn: string
titleRu: string
descriptionTr: string
descriptionEn: string
descriptionRu: string
startDate: Date
endDate?: Date | null
coverImage?: string | null
isSponsored: boolean
createdAt: Date
updatedAt: Date
deletedAt?: Date | null
}
export interface GeneratedItinerary {
id: string
paramsHash: string
params: any
content: string
listingIds: string[]
createdAt: Date
}
const MOCK_DB_VERSION = 4
const globalForMockDb = globalThis as unknown as {
categories: Category[]
neighborhoods: Neighborhood[]
listings: Listing[]
submissions: BusinessSubmission[]
messages: ContactMessage[]
blogPosts: BlogPost[]
collections: Collection[]
instagramFeedCaches: InstagramFeedCache[]
listingAnalyticsDaily: ListingAnalyticsDaily[]
events: Event[]
generatedItineraries: GeneratedItinerary[]
initialized: boolean
__version: number
}
if (!globalForMockDb.initialized || globalForMockDb.__version !== MOCK_DB_VERSION) {
globalForMockDb.categories = [
{ id: 'cat-1', slug: 'restoran', nameTr: 'Restoran', nameEn: 'Restaurant', nameRu: 'Ресторан', createdAt: new Date(), updatedAt: new Date() },
{ id: 'cat-2', slug: 'apart', nameTr: 'Apart', nameEn: 'Apart Hotel', nameRu: 'Апарт-отель', createdAt: new Date(), updatedAt: new Date() },
{ id: 'cat-3', slug: 'isletme', nameTr: 'İşletme & Hizmet', nameEn: 'Business & Service', nameRu: 'Бизнес и Услуги', createdAt: new Date(), updatedAt: new Date() }
]
globalForMockDb.neighborhoods = [
{ id: 'neigh-1', slug: 'yat-limani', nameTr: 'Yat Limanı', nameEn: 'Marina', nameRu: 'Марина' },
{ id: 'neigh-2', slug: 'icmeler', nameTr: 'İçmeler', nameEn: 'Icmeler', nameRu: 'Ичмелер' },
{ id: 'neigh-3', slug: 'armutalan', nameTr: 'Armutalan', nameEn: 'Armutalan', nameRu: 'Армуталан' },
{ id: 'neigh-4', slug: 'siteler', nameTr: 'Siteler', nameEn: 'Siteler', nameRu: 'Сителер' },
{ id: 'neigh-5', slug: 'turunc', nameTr: 'Turunç', nameEn: 'Turunc', nameRu: 'Турунч' }
]
globalForMockDb.submissions = []
globalForMockDb.messages = []
// Seed Listings with Phase 2 & 3 fields
globalForMockDb.listings = [
{
id: 'list-1',
slug: 'iskele-balik-ocakbasi',
categoryId: 'cat-1',
neighborhoodId: 'neigh-1',
city: 'marmaris',
nameTr: 'İskele Balık Ocakbaşı',
nameEn: 'Iskele Fish & Grill',
nameRu: 'Рыбный Гриль İskele',
descriptionTr: 'Yat Limanı\'nda taze Ege balıkları ve geleneksel meze çeşitleriyle yerel lezzet durağınız. Mükemmel körfez manzarası eşliğinde akşam yemeği.',
descriptionEn: 'Your local taste stop at the Marina with fresh Aegean fish and traditional appetizers. Dinner accompanied by excellent bay views.',
descriptionRu: 'Ваша местная гастрономическая остановка в Марине со свежей эгейской рыбой и традиционными закусками. Ужин в сопровождении великолепного вида на залив.',
address: 'Yat Limanı No:12, Marmaris',
phone: '+90 252 412 34 56',
whatsapp: '+90 532 123 45 67',
website: 'https://iskelemarmaris.com',
instagram: 'iskele_marmaris',
priceRange: 3,
rating: 4.8,
isLocalApproved: true,
latitude: 36.8524,
longitude: 28.2741,
openingHours: { all: '12:00 - 00:00' },
images: [
{ id: 'img-1-1', listingId: 'list-1', url: 'https://images.unsplash.com/photo-1519708227418-c8fd9a32b7a2?w=800&auto=format&fit=crop&q=80' },
{ id: 'img-1-2', listingId: 'list-1', url: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&auto=format&fit=crop&q=80' }
],
createdAt: new Date(Date.now() - 3600000 * 24 * 5),
updatedAt: new Date(),
menuUrl: 'https://iskelemarmaris.com/menu',
isFeatured: true,
hasWidgetInstalled: true,
widgetInstalledAt: new Date(Date.now() - 3600000 * 24 * 10),
widgetSiteUrl: 'https://iskelemarmaris.com'
},
{
id: 'list-2',
slug: 'mavi-beyaz-restoran',
categoryId: 'cat-1',
neighborhoodId: 'neigh-1',
city: 'marmaris',
nameTr: 'Mavi Beyaz Restoran',
nameEn: 'Blue White Restaurant',
nameRu: 'Ресторан Сине-Белый',
descriptionTr: 'Ege kıyılarının esintisini taşıyan, deniz ürünleri ağırlıklı menüsü ve gün batımı eşliğindeki eşsiz mezeleriyle ünlüdür.',
descriptionEn: 'Famous for its seafood-oriented menu carrying the breeze of the Aegean coasts and unique appetizers accompanied by sunset.',
descriptionRu: 'Славится своим меню из морепродуктов, несущим бриз Эгейского побережья, и уникальными закусками на фоне заката.',
address: 'Kordon Caddesi No:45, Yat Limanı, Marmaris',
phone: '+90 252 412 78 90',
whatsapp: '+90 533 987 65 43',
website: 'https://mavibeyazmarmaris.com',
instagram: 'mavibeyaz_marmaris',
priceRange: 2,
rating: 4.5,
isLocalApproved: true,
latitude: 36.8530,
longitude: 28.2725,
openingHours: { all: '11:00 - 23:30' },
images: [
{ id: 'img-2-1', listingId: 'list-2', url: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=800&auto=format&fit=crop&q=80' }
],
createdAt: new Date(Date.now() - 3600000 * 24 * 4),
updatedAt: new Date(),
menuUrl: 'https://mavibeyazmarmaris.com/digital-menu',
isFeatured: false,
hasWidgetInstalled: false,
widgetInstalledAt: null,
widgetSiteUrl: null
},
{
id: 'list-3',
slug: 'dostlar-kebap',
categoryId: 'cat-1',
neighborhoodId: 'neigh-3',
city: 'marmaris',
nameTr: 'Dostlar Kebap',
nameEn: 'Dostlar Kebab House',
nameRu: 'Кебаб Хаус Достлар',
descriptionTr: 'Marmaris Armutalan\'da yıllardır değişmeyen lezzetiyle gerçek ocakbaşı ve kebap deneyimi sunan samimi bir mahalle restoranı.',
descriptionEn: 'A cozy neighborhood restaurant offering real grill and kebab experience with its unchanged taste for years in Armutalan, Marmaris.',
descriptionRu: 'Уютный районный ресторан, предлагающий настоящий гриль и кебаб с неизменным вкусом на протяжении многих лет в Армуталане, Мармарис.',
address: 'Vatan Caddesi No:18, Armutalan, Marmaris',
phone: '+90 252 413 11 22',
whatsapp: null,
website: null,
instagram: 'dostlarkebap_marmaris',
priceRange: 1,
rating: 4.7,
isLocalApproved: false,
latitude: 36.8488,
longitude: 28.2450,
openingHours: { all: '11:00 - 22:00' },
images: [
{ id: 'img-3-1', listingId: 'list-3', url: 'https://images.unsplash.com/photo-1544025162-d76694265947?w=800&auto=format&fit=crop&q=80' }
],
createdAt: new Date(Date.now() - 3600000 * 24 * 3),
updatedAt: new Date(),
isFeatured: false,
hasWidgetInstalled: false,
widgetInstalledAt: null,
widgetSiteUrl: null
},
{
id: 'list-4',
slug: 'zeytin-bahcesi-apart',
categoryId: 'cat-2',
neighborhoodId: 'neigh-3',
city: 'marmaris',
nameTr: 'Zeytin Bahçesi Apart',
nameEn: 'Olive Garden Apart',
nameRu: 'Апарт Оливковый Сад',
descriptionTr: 'Zeytin ağaçları arasında, sakin ve huzurlu bir ortamda ailece tatil yapmak isteyenler için tasarlanmış geniş mutfaklı apart daireler.',
descriptionEn: 'Spacious apart apartments with kitchens designed for families who want to have a holiday in a quiet and peaceful environment among olive trees.',
descriptionRu: 'Просторные апартаменты с кухнями, предназначенные для семей, желающих отдохнуть в тихой и спокойной обстановке среди оливковых деревьев.',
address: 'Zeytinlik Sokak No:5, Armutalan, Marmaris',
phone: '+90 252 413 55 66',
whatsapp: '+90 535 555 66 77',
website: 'https://zeytinbahcesiapart.com',
instagram: null,
priceRange: 2,
rating: 4.6,
isLocalApproved: true,
latitude: 36.8510,
longitude: 28.2415,
openingHours: null,
images: [
{ id: 'img-4-1', listingId: 'list-4', url: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800&auto=format&fit=crop&q=80' },
{ id: 'img-4-2', listingId: 'list-4', url: 'https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?w=800&auto=format&fit=crop&q=80' }
],
createdAt: new Date(Date.now() - 3600000 * 24 * 2),
updatedAt: new Date(),
isFeatured: true,
hasWidgetInstalled: false,
widgetInstalledAt: null,
widgetSiteUrl: null
},
{
id: 'list-5',
slug: 'deniz-apart',
categoryId: 'cat-2',
neighborhoodId: 'neigh-4',
city: 'marmaris',
nameTr: 'Deniz Apart',
nameEn: 'Sea Apart Hotel',
nameRu: 'Апарт-отель Дениз',
descriptionTr: 'Denize sadece 100 metre mesafede, bütçe dostu fiyatları ve güler yüzlü yerel işletme sahibiyle Marmaris\'te sıcak konaklama.',
descriptionEn: 'Cozy accommodation in Marmaris, just 100 meters from the sea, with budget-friendly prices and a friendly local owner.',
descriptionRu: 'Уютное жилье в Мармарисе, всего в 100 метрах от моря, с доступными ценами и дружелюбным местным владельцем.',
address: 'Sahil Yolu Caddesi No:88, Siteler, Marmaris',
phone: '+90 252 417 88 99',
whatsapp: null,
website: null,
instagram: null,
priceRange: 1,
rating: 4.2,
isLocalApproved: false,
latitude: 36.8375,
longitude: 28.2580,
openingHours: null,
images: [
{ id: 'img-5-1', listingId: 'list-5', url: 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800&auto=format&fit=crop&q=80' }
],
createdAt: new Date(Date.now() - 3600000 * 24 * 1),
updatedAt: new Date(),
isFeatured: false,
hasWidgetInstalled: false,
widgetInstalledAt: null,
widgetSiteUrl: null
},
{
id: 'list-6',
slug: 'marina-diving-center',
categoryId: 'cat-3',
neighborhoodId: 'neigh-1',
city: 'marmaris',
nameTr: 'Marina Dalış Merkezi',
nameEn: 'Marina Diving Center',
nameRu: 'Дайвинг-центр Марина',
descriptionTr: 'Marmaris\'in kristal netliğindeki sularında profesyonel eğitmenlerle dalış eğitimleri ve günlük dalış turları. CMAS ve PADI sertifikalı eğitimler.',
descriptionEn: 'Diving training and daily diving tours in the crystal clear waters of Marmaris with professional instructors. CMAS and PADI certified courses.',
descriptionRu: 'Обучение дайвингу и ежедневные дайв-туры в кристально чистых водах Мармариса с профессиональными инструкторами. Курсы с сертификатом CMAS и ПАДИ.',
address: 'Yat Limanı Belediye İskelesi, Marmaris',
phone: '+90 532 234 56 78',
whatsapp: '+90 532 234 56 78',
website: 'https://marinadivingmarmaris.com',
instagram: null,
priceRange: 2,
rating: 4.9,
isLocalApproved: true,
latitude: 36.8521,
longitude: 28.2748,
openingHours: { all: '09:00 - 19:00' },
images: [
{ id: 'img-6-1', listingId: 'list-6', url: 'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=800&auto=format&fit=crop&q=80' }
],
createdAt: new Date(Date.now() - 3600000 * 12),
updatedAt: new Date(),
isFeatured: false,
hasWidgetInstalled: false,
widgetInstalledAt: null,
widgetSiteUrl: null
},
{
id: 'list-7',
slug: 'aegean-wind-yacht-charter',
categoryId: 'cat-3',
neighborhoodId: 'neigh-1',
city: 'marmaris',
nameTr: 'Ege Rüzgarı Yat Kiralama',
nameEn: 'Aegean Wind Yacht Charter',
nameRu: 'Аренда Яхт Эгейский Ветер',
descriptionTr: 'Kaptanlı veya kaptansız olarak günlük ve haftalık özel tekne kiralama. Marmaris koylarını kendi rotanızla özgürce keşfedin.',
descriptionEn: 'Daily and weekly private boat charter with or without skipper. Discover the bays of Marmaris freely with your own route.',
descriptionRu: 'Ежедневная и еженедельная аренда частных лодок со шкипером или без. Откройте для себя бухты Мармариса свободно по собственному маршруту.',
address: 'Yat Limanı G İskelesi, Marmaris',
phone: '+90 532 999 88 77',
whatsapp: '+90 532 999 88 77',
website: 'https://egeruzgariboat.com',
instagram: 'egeruzgariboat',
priceRange: 3,
rating: 4.8,
isLocalApproved: true,
latitude: 36.8528,
longitude: 28.2755,
openingHours: { all: '08:00 - 21:00' },
images: [
{ id: 'img-7-1', listingId: 'list-7', url: 'https://images.unsplash.com/photo-1567899378494-47b22a2ae96a?w=800&auto=format&fit=crop&q=80' }
],
createdAt: new Date(Date.now() - 3600000 * 6),
updatedAt: new Date(),
isFeatured: false,
hasWidgetInstalled: false,
widgetInstalledAt: null,
widgetSiteUrl: null
},
{
id: 'list-8',
slug: 'marmaris-transfer-tours',
categoryId: 'cat-3',
neighborhoodId: 'neigh-3',
city: 'marmaris',
nameTr: 'Marmaris VIP Transfer & Tur',
nameEn: 'Marmaris VIP Transfer & Tours',
nameRu: 'Marmaris VIP Трансфер и Туры',
descriptionTr: 'Dalaman Havalimanı transferleri ve Marmaris çevresindeki tarihi/doğal alanlara özel konforlu turlar. Güvenilir ve lüks taşımacılık.',
descriptionEn: 'Dalaman Airport transfers and comfortable private tours to historical/natural areas around Marmaris. Reliable and luxurious transportation.',
descriptionRu: 'Трансфер из аэропорта Даламан и комфортабельные частные туры по историческим и природным местам вокруг Мармариса. Надежный и роскошный транспорт.',
address: 'Atatürk Caddesi No:102, Armutalan, Marmaris',
phone: '+90 252 413 77 88',
whatsapp: '+90 541 333 44 55',
website: 'https://marmarisviptransfer.com',
instagram: null,
priceRange: 2,
rating: 4.6,
isLocalApproved: false,
latitude: 36.8495,
longitude: 28.2430,
openingHours: { all: '24 Hours Open' },
images: [
{ id: 'img-8-1', listingId: 'list-8', url: 'https://images.unsplash.com/photo-1549317661-bd32c8ce0db2?w=800&auto=format&fit=crop&q=80' }
],
createdAt: new Date(Date.now() - 3600000 * 2),
updatedAt: new Date(),
isFeatured: false,
hasWidgetInstalled: false,
widgetInstalledAt: null,
widgetSiteUrl: null
}
]
// Seed Blog Posts
globalForMockDb.blogPosts = [
{
id: 'post-1',
slug: 'marmariste-nerede-yenir-2026',
titleTr: 'Marmaris\'te Nerede Yenir? 2026 Lezzet Durakları',
titleEn: 'Where to Eat in Marmaris? 2026 Culinary Hotspots',
titleRu: 'Где поесть в Мармарисе? Лучшие места 2026 года',
contentTr: 'Marmaris, Ege ve Akdeniz mutfağının en taze deniz ürünlerini ve mezelerini bulabileceğiniz harika bir sahil kenti. İşte 2026 yılında ziyaret etmeniz gereken en lezzetli mekanlar...\n\n### 1. İskele Balık Ocakbaşı\nYat Limanında yer alan bu harika mekan, taze balıkları ve mezeleriyle ünlüdür.\n\n### 2. Mavi Beyaz Restoran\nSöğüt köyündeki eşsiz manzarası ve gurme Ege yemekleri ile unutulmaz bir akşam sunuyor.',
contentEn: 'Marmaris is a wonderful coastal town where you can find the freshest seafood and appetizers of Aegean and Mediterranean cuisine. Here are the most delicious places you should visit in 2026...\n\n### 1. Iskele Fish & Grill\nLocated in Yat Limani, this wonderful venue is famous for its fresh fish and appetizers.\n\n### 2. Mavi Beyaz Restaurant\nOffers an unforgettable evening with its unique view and gourmet Aegean dishes in Sogut village.',
contentRu: 'Мармарис — прекрасный прибрежный город, где вы найдете самые свежие морепродукты и закуски эгейской и средиземноморской кухни. Вот самые вкусные места, которые стоит посетить в 2026 году...\n\n### 1. Искеле Балык Оджакбаши\nЭто замечательное заведение, расположенное в Ят Лимани, славится свежей рыбой и закусками.\n\n### 2. Ресторан Мави Беяз\nПредлагает незабываемый вечер с уникальным видом и изысканными блюдами эгейской кухни в деревне Сегют.',
coverImage: 'https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=1200&auto=format&fit=crop&q=80',
tags: ['Restoran', 'Yemek', 'Rehber'],
relatedListingIds: ['list-1', 'list-2'],
publishedAt: new Date(),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: 'post-2',
slug: 'gun-batimi-icin-5-mekan',
titleTr: 'Marmaris\'te Gün Batımını İzleyebileceğiniz En İyi 5 Yer',
titleEn: 'Top 5 Places to Watch the Sunset in Marmaris',
titleRu: '5 лучших мест для наблюдения за закатом в Мармарисе',
contentTr: 'Marmaris\'in en büyüleyici anlarından biri hiç şüphesiz gün batımıdır. Gökyüzünün kızıla büründüğü bu saatlerde unutulmaz kareler yakalayabileceğiniz ve keyifle içeceğinizi yudumlayabileceğiniz en güzel yerleri derledik.\n\n* **Yat Limanı:** Şehir merkezinde en popüler seyir yeri.\n* **İçmeler Sahili:** Adaların arkasından batan güneşi izlemek şahanedir.\n* **Turunç Tepesi:** Kuşbakışı körfez manzarası sunar.',
contentEn: 'One of the most fascinating moments of Marmaris is undoubtedly the sunset. We have compiled the most beautiful places where you can capture unforgettable frames and enjoy your drink while the sky turns red.\n\n* **Marina:** The most popular viewing point in the city center.\n* **Icmeler Beach:** It is wonderful to watch the sun setting behind the islands.\n* **Turunc Hill:** Offers a panoramic view of the bay.',
contentRu: 'Один из самых захватывающих моментов в Мармарисе — это, без сомнения, закат. Мы собрали самые красивые места, где вы сможете сделать незабываемые снимки и насладиться напитком, пока небо окрашивается в красный цвет.\n\n* **Марина:** Самая популярная точка обзора в центре города.\n* **Пляж Ичмелер:** Прекрасно наблюдать за закатом солнца за островами.\n* **Холм Турунч:** Панорамный вид на залив.',
coverImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=1200&auto=format&fit=crop&q=80',
tags: ['Manzara', 'Gezi', 'Gün Batımı'],
relatedListingIds: ['list-2', 'list-7'],
publishedAt: new Date(),
createdAt: new Date(),
updatedAt: new Date()
}
]
// Seed Collections
globalForMockDb.collections = [
{
id: 'col-1',
slug: 'aile-dostu-restoranlar',
titleTr: 'Aile Dostu Restoranlar',
titleEn: 'Family Friendly Restaurants',
titleRu: 'Семейные рестораны',
descriptionTr: 'Çocuklarınızla birlikte rahatça yemek yiyebileceğiniz, oyun alanları ve özel çocuk menüleri bulunan en iyi Marmaris mekanları.',
descriptionEn: 'The best Marmaris restaurants with playgrounds and special kids\' menus where you can comfortably dine with your children.',
descriptionRu: 'Лучшие рестораны Мармариса с детскими площадками и специальным детским меню, где вы сможете комфортно пообедать с детьми.',
coverImage: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1200&auto=format&fit=crop&q=80',
listingIds: ['list-1', 'list-3'],
createdAt: new Date(),
updatedAt: new Date()
},
{
id: 'col-2',
slug: 'butce-dostu-mekanlar',
titleTr: 'Bütçe Dostu Mekanlar',
titleEn: 'Budget-Friendly Venues',
titleRu: 'Бюджетные заведения',
descriptionTr: 'Marmaris tatilinizde cebinizi yormayacak, hem kaliteli hizmet sunan hem de uygun fiyatlı lokasyonlar.',
descriptionEn: 'Budget-friendly locations in Marmaris that offer high-quality service and reasonable prices for your holiday.',
descriptionRu: 'Доступные заведения в Мармарисе, которые предлагают высококачественный сервис и умеренные цены во время вашего отдыха.',
coverImage: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=1200&auto=format&fit=crop&q=80',
listingIds: ['list-3', 'list-5'],
createdAt: new Date(),
updatedAt: new Date()
}
]
// Seed Instagram Caches
globalForMockDb.instagramFeedCaches = [
{
id: 'insta-1',
listingId: 'list-1',
handle: 'iskele_marmaris',
posts: [
{ imageUrl: 'https://images.unsplash.com/photo-1544025162-d76694265947?w=500&auto=format&fit=crop&q=80', caption: 'Mezelerimiz taze taze hazırlandı! 🐟', permalink: '#', postedAt: '2026-07-10T12:00:00Z' },
{ imageUrl: 'https://images.unsplash.com/photo-1519708227418-c8fd9a32b7a2?w=500&auto=format&fit=crop&q=80', caption: 'Bu akşam iskelede gün batımı bir başka güzel... 🌅', permalink: '#', postedAt: '2026-07-09T18:30:00Z' },
{ imageUrl: 'https://images.unsplash.com/photo-1476224203421-9ac39bcb3327?w=500&auto=format&fit=crop&q=80', caption: 'Balık keyfini kaçırmayın! 🍽️', permalink: '#', postedAt: '2026-07-08T14:15:00Z' }
],
fetchedAt: new Date()
},
{
id: 'insta-2',
listingId: 'list-2',
handle: 'mavibeyaz_marmaris',
posts: [
{ imageUrl: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=500&auto=format&fit=crop&q=80', caption: 'Kordon keyfi... 🍷', permalink: '#', postedAt: '2026-07-11T16:00:00Z' },
{ imageUrl: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=500&auto=format&fit=crop&q=80', caption: 'Taze deniz ürünleri ve meze çeşitleri Mavi Beyaz\'da!', permalink: '#', postedAt: '2026-07-08T15:30:00Z' }
],
fetchedAt: new Date()
}
]
// Seed Listing Analytics Daily
globalForMockDb.listingAnalyticsDaily = [
{ id: 'an-1', listingId: 'list-1', date: new Date(), views: 120, whatsappClicks: 14, phoneClicks: 5, menuClicks: 22 },
{ id: 'an-2', listingId: 'list-2', date: new Date(), views: 45, whatsappClicks: 2, phoneClicks: 1, menuClicks: 0 }
]
// Seed Events
globalForMockDb.events = [
{
id: 'evt-1',
slug: 'iskele-caz-gecesi',
listingId: 'list-1',
titleTr: 'İskele Caz Gecesi',
titleEn: 'Iskele Jazz Night',
titleRu: 'Джазовый вечер Искеле',
descriptionTr: 'Marmaris Marina\'da deniz esintisi eşliğinde canlı caz müziği ve özel akşam yemeği menüsü.',
descriptionEn: 'Live jazz music and special dinner menu accompanied by sea breeze at Marmaris Marina.',
descriptionRu: 'Живая джазовая музыка и специальное меню ужина в сопровождении морского бриза в Мармарис Марине.',
startDate: new Date(Date.now() + 3600000 * 24 * 3), // 3 days from now
endDate: new Date(Date.now() + 3600000 * 24 * 3 + 3600000 * 4),
coverImage: 'https://images.unsplash.com/photo-1511192336575-5a79af67a629?w=800&auto=format&fit=crop&q=80',
isSponsored: true,
createdAt: new Date(),
updatedAt: new Date()
},
{
id: 'evt-2',
slug: 'siteler-havuz-partisi',
listingId: 'list-2',
titleTr: 'Yaz Ortası Havuz Partisi',
titleEn: 'Midsummer Pool Party',
titleRu: 'Летняя вечеринка у бассейна',
descriptionTr: 'Sınırsız müzik, dj performansları ve eğlenceli havuz aktiviteleri.',
descriptionEn: 'Unlimited music, DJ performances and fun pool activities.',
descriptionRu: 'Безлимитная музыка, диджейские сеты и веселые развлечения у бассейна.',
startDate: new Date(Date.now() + 3600000 * 24 * 7), // 7 days from now
coverImage: 'https://images.unsplash.com/photo-1576013551627-0cc20b96c2a7?w=800&auto=format&fit=crop&q=80',
isSponsored: false,
createdAt: new Date(),
updatedAt: new Date()
}
]
globalForMockDb.generatedItineraries = []
globalForMockDb.__version = MOCK_DB_VERSION
globalForMockDb.initialized = true
}
export const mockDb = {
// Config helpers
isMock: () => process.env.USE_MOCK === 'true',
// Categories
async createCategory(data: Omit<Category, 'id' | 'createdAt' | 'updatedAt'>) {
if (this.isMock()) {
const newCat: Category = {
id: 'cat-' + Date.now(),
...data,
createdAt: new Date(),
updatedAt: new Date()
}
globalForMockDb.categories.push(newCat)
return newCat
}
return db.category.create({ data })
},
async updateCategory(id: string, data: Partial<Omit<Category, 'id' | 'createdAt' | 'updatedAt'>>) {
if (this.isMock()) {
const idx = globalForMockDb.categories.findIndex(c => c.id === id)
if (idx > -1) {
globalForMockDb.categories[idx] = { ...globalForMockDb.categories[idx], ...data, updatedAt: new Date() }
return globalForMockDb.categories[idx]
}
return null
}
return db.category.update({ where: { id }, data })
},
async deleteCategory(id: string) {
if (this.isMock()) {
globalForMockDb.categories = globalForMockDb.categories.filter(c => c.id !== id)
return true
}
await db.category.delete({ where: { id } })
return true
},
async getCategories() {
if (this.isMock()) {
return globalForMockDb.categories
}
return db.category.findMany({ orderBy: { id: 'asc' } })
},
async getCategoryBySlug(slug: string) {
if (this.isMock()) {
return globalForMockDb.categories.find(c => c.slug === slug) || null
}
return db.category.findUnique({ where: { slug } })
},
async getCategoryById(id: string) {
if (this.isMock()) {
return globalForMockDb.categories.find(c => c.id === id) || null
}
return db.category.findUnique({ where: { id } })
},
// Neighborhoods
async getNeighborhoods() {
if (this.isMock()) {
return globalForMockDb.neighborhoods
}
return db.neighborhood.findMany({ orderBy: { id: 'asc' } })
},
async getNeighborhoodBySlug(slug: string) {
if (this.isMock()) {
return globalForMockDb.neighborhoods.find(n => n.slug === slug) || null
}
return db.neighborhood.findUnique({ where: { slug } })
},
async getNeighborhoodById(id: string) {
if (this.isMock()) {
return globalForMockDb.neighborhoods.find(n => n.id === id) || null
}
return db.neighborhood.findUnique({ where: { id } })
},
async createNeighborhood(data: Omit<Neighborhood, 'id' | 'createdAt' | 'updatedAt'>) {
if (this.isMock()) {
const newNeigh: Neighborhood = {
id: `neigh-${Date.now()}`,
...data,
}
globalForMockDb.neighborhoods.push(newNeigh)
return newNeigh
}
return db.neighborhood.create({ data })
},
async updateNeighborhood(id: string, data: Partial<Omit<Neighborhood, 'id' | 'createdAt' | 'updatedAt'>>) {
if (this.isMock()) {
const idx = globalForMockDb.neighborhoods.findIndex(n => n.id === id)
if (idx > -1) {
globalForMockDb.neighborhoods[idx] = { ...globalForMockDb.neighborhoods[idx], ...data }
return globalForMockDb.neighborhoods[idx]
}
throw new Error('Mahalle bulunamadı')
}
return db.neighborhood.update({ where: { id }, data })
},
async deleteNeighborhood(id: string) {
if (this.isMock()) {
globalForMockDb.neighborhoods = globalForMockDb.neighborhoods.filter(n => n.id !== id)
return
}
return db.neighborhood.delete({ where: { id } })
},
// Listings CRUD
async getListings(filters?: {
categoryId?: string
neighborhoodId?: string
priceRange?: number
isLocalApproved?: boolean
search?: string
isFeatured?: boolean
}) {
if (this.isMock()) {
let result = [...globalForMockDb.listings].filter(l => !l.deletedAt)
if (filters) {
if (filters.categoryId) result = result.filter(l => l.categoryId === filters.categoryId)
if (filters.neighborhoodId) result = result.filter(l => l.neighborhoodId === filters.neighborhoodId)
if (filters.priceRange) result = result.filter(l => l.priceRange === filters.priceRange)
if (filters.isLocalApproved !== undefined) result = result.filter(l => l.isLocalApproved === filters.isLocalApproved)
if (filters.isFeatured !== undefined) result = result.filter(l => l.isFeatured === filters.isFeatured)
if (filters.search) {
const s = filters.search.toLowerCase()
result = result.filter(
l =>
l.nameTr.toLowerCase().includes(s) ||
l.nameEn.toLowerCase().includes(s) ||
l.nameRu.toLowerCase().includes(s) ||
l.address.toLowerCase().includes(s)
)
}
}
// Sort: Featured items first, then newer listings first
result.sort((a, b) => {
if (a.isFeatured && !b.isFeatured) return -1
if (!a.isFeatured && b.isFeatured) return 1
return b.createdAt.getTime() - a.createdAt.getTime()
})
return result.map(l => ({
...l,
category: globalForMockDb.categories.find(c => c.id === l.categoryId),
neighborhood: globalForMockDb.neighborhoods.find(n => n.id === l.neighborhoodId)
}))
}
const where: any = { deletedAt: null }
if (filters) {
if (filters.categoryId) where.categoryId = filters.categoryId
if (filters.neighborhoodId) where.neighborhoodId = filters.neighborhoodId
if (filters.priceRange) where.priceRange = filters.priceRange
if (filters.isLocalApproved !== undefined) where.isLocalApproved = filters.isLocalApproved
if (filters.isFeatured !== undefined) where.isFeatured = filters.isFeatured
if (filters.search) {
where.OR = [
{ nameTr: { contains: filters.search, mode: 'insensitive' } },
{ nameEn: { contains: filters.search, mode: 'insensitive' } },
{ nameRu: { contains: filters.search, mode: 'insensitive' } },
{ address: { contains: filters.search, mode: 'insensitive' } },
]
}
}
return db.listing.findMany({
where,
include: { category: true, neighborhood: true, images: true },
orderBy: [
{ isFeatured: 'desc' },
{ createdAt: 'desc' }
]
})
},
async getListingBySlug(slug: string) {
if (this.isMock()) {
const listing = globalForMockDb.listings.find(l => l.slug === slug && !l.deletedAt)
if (!listing) return null
return {
...listing,
category: globalForMockDb.categories.find(c => c.id === listing.categoryId),
neighborhood: globalForMockDb.neighborhoods.find(n => n.id === listing.neighborhoodId)
}
}
return db.listing.findUnique({
where: { slug },
include: { category: true, neighborhood: true, images: true }
})
},
async getListingById(id: string) {
if (this.isMock()) {
const listing = globalForMockDb.listings.find(l => l.id === id && !l.deletedAt)
if (!listing) return null
return {
...listing,
category: globalForMockDb.categories.find(c => c.id === listing.categoryId),
neighborhood: globalForMockDb.neighborhoods.find(n => n.id === listing.neighborhoodId)
}
}
return db.listing.findUnique({
where: { id },
include: { category: true, neighborhood: true, images: true }
})
},
async createListing(data: Omit<Listing, 'id' | 'images' | 'createdAt' | 'updatedAt'> & { images?: string[] }) {
const { images, category, neighborhood, ...rest } = data
if (this.isMock()) {
const newId = `list-${Date.now()}`
const gallery = (images || []).map((url, i) => ({ id: `img-${newId}-${i}`, listingId: newId, url }))
const newListing: Listing = {
id: newId,
...rest,
images: gallery,
createdAt: new Date(),
updatedAt: new Date(),
isFeatured: data.isFeatured || false
}
globalForMockDb.listings.push(newListing)
return newListing
}
return db.listing.create({
data: {
...rest,
images: {
create: (images || []).map(url => ({ url }))
}
}
})
},
async updateListing(id: string, data: Partial<Omit<Listing, 'id' | 'images' | 'createdAt' | 'updatedAt'>> & { images?: string[] }) {
const { images, category, neighborhood, ...rest } = data
if (this.isMock()) {
const idx = globalForMockDb.listings.findIndex(l => l.id === id)
if (idx !== -1) {
const gallery = images ? images.map((url, i) => ({ id: `img-${id}-${i}`, listingId: id, url })) : globalForMockDb.listings[idx].images
globalForMockDb.listings[idx] = {
...globalForMockDb.listings[idx],
...rest,
images: gallery,
updatedAt: new Date()
} as Listing
return globalForMockDb.listings[idx]
}
return null
}
// Update listing images transaction
if (images) {
await db.gallery.deleteMany({ where: { listingId: id } })
return db.listing.update({
where: { id },
data: {
...rest,
images: {
create: images.map(url => ({ url }))
}
}
})
}
return db.listing.update({
where: { id },
data: rest
})
},
async deleteListing(id: string) {
if (this.isMock()) {
const idx = globalForMockDb.listings.findIndex(l => l.id === id)
if (idx !== -1) {
globalForMockDb.listings[idx].deletedAt = new Date()
return true
}
return false
}
await db.listing.update({
where: { id },
data: { deletedAt: new Date() }
})
return true
},
async getDeletedListings() {
if (this.isMock()) {
const result = globalForMockDb.listings.filter(l => l.deletedAt)
return result.map(l => ({
...l,
category: globalForMockDb.categories.find(c => c.id === l.categoryId),
neighborhood: globalForMockDb.neighborhoods.find(n => n.id === l.neighborhoodId)
}))
}
return db.listing.findMany({
where: { deletedAt: { not: null } },
include: {
category: true,
neighborhood: true
},
orderBy: { deletedAt: 'desc' }
})
},
async restoreListing(id: string) {
if (this.isMock()) {
const idx = globalForMockDb.listings.findIndex(l => l.id === id)
if (idx !== -1) {
globalForMockDb.listings[idx].deletedAt = null
return true
}
return false
}
await db.listing.update({
where: { id },
data: { deletedAt: null }
})
return true
},
async hardDeleteListing(id: string) {
if (this.isMock()) {
globalForMockDb.listings = globalForMockDb.listings.filter(l => l.id !== id)
return true
}
await db.listing.delete({ where: { id } })
return true
},
// Business Submissions
async getSubmissions() {
if (this.isMock()) {
return globalForMockDb.submissions.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
}
return db.businessSubmission.findMany({ orderBy: { createdAt: 'desc' } })
},
async deleteSubmission(id: string) {
if (this.isMock()) {
const idx = globalForMockDb.submissions.findIndex(s => s.id === id)
if (idx !== -1) {
globalForMockDb.submissions.splice(idx, 1)
return true
}
return false
}
await db.businessSubmission.delete({ where: { id } })
},
async createSubmission(data: Omit<BusinessSubmission, 'id' | 'status' | 'createdAt' | 'updatedAt'>) {
if (this.isMock()) {
const newSub: BusinessSubmission = {
id: `sub-${Date.now()}`,
...data,
status: 'PENDING',
createdAt: new Date(),
updatedAt: new Date()
}
globalForMockDb.submissions.push(newSub)
return newSub
}
return db.businessSubmission.create({ data: { ...data, status: 'PENDING' } })
},
async updateSubmissionStatus(id: string, status: 'APPROVED' | 'REJECTED') {
if (this.isMock()) {
const idx = globalForMockDb.submissions.findIndex(s => s.id === id)
if (idx !== -1) {
globalForMockDb.submissions[idx].status = status
globalForMockDb.submissions[idx].updatedAt = new Date()
return globalForMockDb.submissions[idx]
}
return null
}
return db.businessSubmission.update({
where: { id },
data: { status }
})
},
async getSubmissionById(id: string) {
if (this.isMock()) {
return globalForMockDb.submissions.find(s => s.id === id) || null
}
return db.businessSubmission.findUnique({ where: { id } })
},
// Contact Messages
async getMessages() {
if (this.isMock()) {
return globalForMockDb.messages.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
}
return db.contactMessage.findMany({ orderBy: { createdAt: 'desc' } })
},
async deleteMessage(id: string) {
if (this.isMock()) {
const idx = globalForMockDb.messages.findIndex(m => m.id === id)
if (idx !== -1) {
globalForMockDb.messages.splice(idx, 1)
return true
}
return false
}
await db.contactMessage.delete({ where: { id } })
},
async createMessage(data: Omit<ContactMessage, 'id' | 'isRead' | 'createdAt' | 'updatedAt'>) {
if (this.isMock()) {
const newMsg: ContactMessage = {
id: `msg-${Date.now()}`,
...data,
isRead: false,
createdAt: new Date(),
updatedAt: new Date()
}
globalForMockDb.messages.push(newMsg)
return newMsg
}
return db.contactMessage.create({ data: { ...data, isRead: false } })
},
async markMessageAsRead(id: string) {
if (this.isMock()) {
const idx = globalForMockDb.messages.findIndex(m => m.id === id)
if (idx !== -1) {
globalForMockDb.messages[idx].isRead = true
globalForMockDb.messages[idx].updatedAt = new Date()
return globalForMockDb.messages[idx]
}
return null
}
return db.contactMessage.update({
where: { id },
data: { isRead: true }
})
},
// BlogPost CRUD (Phase 2)
async getBlogPosts(onlyPublished: boolean = false) {
if (this.isMock()) {
let posts = globalForMockDb.blogPosts.filter(p => !p.deletedAt)
if (onlyPublished) {
posts = posts.filter(p => p.publishedAt !== null)
}
return posts.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
}
const where: any = { deletedAt: null }
if (onlyPublished) {
where.publishedAt = { not: null }
}
return db.blogPost.findMany({
where,
orderBy: { createdAt: 'desc' }
})
},
async getBlogPostBySlug(slug: string) {
if (this.isMock()) {
return globalForMockDb.blogPosts.find(p => p.slug === slug && !p.deletedAt) || null
}
return db.blogPost.findUnique({ where: { slug } })
},
async getBlogPostById(id: string) {
if (this.isMock()) {
return globalForMockDb.blogPosts.find(p => p.id === id && !p.deletedAt) || null
}
return db.blogPost.findUnique({ where: { id } })
},
async createBlogPost(data: Omit<BlogPost, 'id' | 'createdAt' | 'updatedAt'>) {
if (this.isMock()) {
const newPost: BlogPost = {
id: `post-${Date.now()}`,
...data,
createdAt: new Date(),
updatedAt: new Date()
}
globalForMockDb.blogPosts.push(newPost)
return newPost
}
return db.blogPost.create({ data })
},
async updateBlogPost(id: string, data: Partial<Omit<BlogPost, 'id' | 'createdAt' | 'updatedAt'>>) {
if (this.isMock()) {
const idx = globalForMockDb.blogPosts.findIndex(p => p.id === id)
if (idx !== -1) {
globalForMockDb.blogPosts[idx] = {
...globalForMockDb.blogPosts[idx],
...data,
updatedAt: new Date()
}
return globalForMockDb.blogPosts[idx]
}
return null
}
return db.blogPost.update({ where: { id }, data })
},
async deleteBlogPost(id: string) {
if (this.isMock()) {
const idx = globalForMockDb.blogPosts.findIndex(p => p.id === id)
if (idx !== -1) {
globalForMockDb.blogPosts[idx].deletedAt = new Date()
return true
}
return false
}
await db.blogPost.update({ where: { id }, data: { deletedAt: new Date() } })
return true
},
// Collection CRUD (Phase 2)
async getCollections() {
if (this.isMock()) {
const colls = [...globalForMockDb.collections]
return colls.map(c => ({
...c,
listings: globalForMockDb.listings.filter(l => c.listingIds.includes(l.id) && !l.deletedAt)
}))
}
return db.collection.findMany({
include: { listings: { include: { images: true, category: true, neighborhood: true } } },
orderBy: { createdAt: 'desc' }
})
},
async getCollectionBySlug(slug: string) {
if (this.isMock()) {
const c = globalForMockDb.collections.find(col => col.slug === slug)
if (!c) return null
return {
...c,
listings: globalForMockDb.listings.filter(l => c.listingIds.includes(l.id) && !l.deletedAt).map(l => ({
...l,
category: globalForMockDb.categories.find(cat => cat.id === l.categoryId),
neighborhood: globalForMockDb.neighborhoods.find(n => n.id === l.neighborhoodId)
}))
}
}
return db.collection.findUnique({
where: { slug },
include: { listings: { include: { images: true, category: true, neighborhood: true } } }
})
},
async getCollectionById(id: string) {
if (this.isMock()) {
const c = globalForMockDb.collections.find(col => col.id === id)
if (!c) return null
return {
...c,
listings: globalForMockDb.listings.filter(l => c.listingIds.includes(l.id) && !l.deletedAt)
}
}
return db.collection.findUnique({
where: { id },
include: { listings: true }
})
},
async createCollection(data: Omit<Collection, 'id' | 'createdAt' | 'updatedAt' | 'listings'>) {
if (this.isMock()) {
const newCol: Collection = {
id: `col-${Date.now()}`,
...data,
createdAt: new Date(),
updatedAt: new Date()
}
globalForMockDb.collections.push(newCol)
return newCol
}
const { listingIds, ...rest } = data
return db.collection.create({
data: {
...rest,
listings: {
connect: listingIds.map(id => ({ id }))
}
}
})
},
async updateCollection(id: string, data: Partial<Omit<Collection, 'id' | 'createdAt' | 'updatedAt' | 'listings'>>) {
if (this.isMock()) {
const idx = globalForMockDb.collections.findIndex(c => c.id === id)
if (idx !== -1) {
globalForMockDb.collections[idx] = {
...globalForMockDb.collections[idx],
...data,
updatedAt: new Date()
} as Collection
return globalForMockDb.collections[idx]
}
return null
}
const { listingIds, ...rest } = data
if (listingIds) {
// For actual DB, first disconnect all and connect new
const current = await db.collection.findUnique({ where: { id }, include: { listings: true } })
const disconnectIds = current?.listings.map(l => ({ id: l.id })) || []
return db.collection.update({
where: { id },
data: {
...rest,
listings: {
disconnect: disconnectIds,
connect: listingIds.map(lid => ({ id: lid }))
}
}
})
}
return db.collection.update({ where: { id }, data: rest as any })
},
async deleteCollection(id: string) {
if (this.isMock()) {
const idx = globalForMockDb.collections.findIndex(c => c.id === id)
if (idx !== -1) {
globalForMockDb.collections.splice(idx, 1)
return true
}
return false
}
await db.collection.delete({ where: { id } })
return true
},
// Instagram Feed Cache (Phase 2)
async getInstagramFeedCacheByListingId(listingId: string) {
if (this.isMock()) {
return globalForMockDb.instagramFeedCaches.find(cache => cache.listingId === listingId) || null
}
return db.instagramFeedCache.findUnique({ where: { listingId } })
},
async updateInstagramFeedCache(listingId: string, handle: string, posts: any[]) {
if (this.isMock()) {
const idx = globalForMockDb.instagramFeedCaches.findIndex(cache => cache.listingId === listingId)
if (idx !== -1) {
globalForMockDb.instagramFeedCaches[idx].handle = handle
globalForMockDb.instagramFeedCaches[idx].posts = posts
globalForMockDb.instagramFeedCaches[idx].fetchedAt = new Date()
return globalForMockDb.instagramFeedCaches[idx]
} else {
const newCache = {
id: `insta-${Date.now()}`,
listingId,
handle,
posts,
fetchedAt: new Date()
}
globalForMockDb.instagramFeedCaches.push(newCache)
return newCache
}
}
return db.instagramFeedCache.upsert({
where: { listingId },
update: { handle, posts, fetchedAt: new Date() },
create: { listingId, handle, posts, fetchedAt: new Date() }
})
},
// Phase 3 Widget & Backlink
async getWidgetPartners() {
if (this.isMock()) {
return globalForMockDb.listings.filter(l => l.hasWidgetInstalled && !l.deletedAt)
}
return db.listing.findMany({
where: { hasWidgetInstalled: true, deletedAt: null },
include: { category: true, neighborhood: true }
})
},
// Phase 3 Analytics daily aggregates
async getAnalytics(listingId: string, startDate?: Date, endDate?: Date) {
if (this.isMock()) {
let data = globalForMockDb.listingAnalyticsDaily.filter(a => a.listingId === listingId)
if (startDate) {
data = data.filter(a => a.date >= startDate)
}
if (endDate) {
data = data.filter(a => a.date <= endDate)
}
return data
}
return db.listingAnalyticsDaily.findMany({
where: {
listingId,
date: {
gte: startDate,
lte: endDate
}
},
orderBy: { date: 'asc' }
})
},
async incrementAnalytics(listingId: string, actionType: 'views' | 'whatsapp' | 'phone' | 'menu') {
const today = new Date()
today.setHours(0, 0, 0, 0)
if (this.isMock()) {
const record = globalForMockDb.listingAnalyticsDaily.find(a =>
a.listingId === listingId &&
a.date.getTime() === today.getTime()
)
const keyMap: Record<string, keyof ListingAnalyticsDaily> = {
views: 'views',
whatsapp: 'whatsappClicks',
phone: 'phoneClicks',
menu: 'menuClicks'
}
const mappedKey = keyMap[actionType]
if (record) {
(record[mappedKey] as number) += 1
return record
} else {
const newRecord: ListingAnalyticsDaily = {
id: `an-${Date.now()}`,
listingId,
date: today,
views: actionType === 'views' ? 1 : 0,
whatsappClicks: actionType === 'whatsapp' ? 1 : 0,
phoneClicks: actionType === 'phone' ? 1 : 0,
menuClicks: actionType === 'menu' ? 1 : 0
}
globalForMockDb.listingAnalyticsDaily.push(newRecord)
return newRecord
}
}
const keyMap = {
views: 'views',
whatsapp: 'whatsappClicks',
phone: 'phoneClicks',
menu: 'menuClicks'
}
const updateKey = keyMap[actionType] as 'views' | 'whatsappClicks' | 'phoneClicks' | 'menuClicks'
return db.listingAnalyticsDaily.upsert({
where: {
listingId_date: {
listingId,
date: today
}
},
update: {
[updateKey]: { increment: 1 }
},
create: {
listingId,
date: today,
views: actionType === 'views' ? 1 : 0,
whatsappClicks: actionType === 'whatsapp' ? 1 : 0,
phoneClicks: actionType === 'phone' ? 1 : 0,
menuClicks: actionType === 'menu' ? 1 : 0
}
})
},
// Phase 3 Events
async getEvents(onlyActive = false) {
if (this.isMock()) {
let evts = globalForMockDb.events.filter(e => !e.deletedAt)
if (onlyActive) {
evts = evts.filter(e => e.startDate >= new Date())
}
evts.sort((a, b) => a.startDate.getTime() - b.startDate.getTime())
return evts.map(e => ({
...e,
listing: globalForMockDb.listings.find(l => l.id === e.listingId)
}))
}
return db.event.findMany({
where: {
deletedAt: null,
...(onlyActive ? { startDate: { gte: new Date() } } : {})
},
include: { listing: { include: { category: true, neighborhood: true } } },
orderBy: { startDate: 'asc' }
})
},
async getEventBySlug(slug: string) {
if (this.isMock()) {
const e = globalForMockDb.events.find(evt => evt.slug === slug && !evt.deletedAt)
if (!e) return null
return {
...e,
listing: globalForMockDb.listings.find(l => l.id === e.listingId)
}
}
return db.event.findUnique({
where: { slug },
include: { listing: { include: { category: true, neighborhood: true } } }
})
},
async getEventById(id: string) {
if (this.isMock()) {
const e = globalForMockDb.events.find(evt => evt.id === id && !evt.deletedAt)
if (!e) return null
return {
...e,
listing: globalForMockDb.listings.find(l => l.id === e.listingId)
}
}
return db.event.findUnique({
where: { id },
include: { listing: { include: { category: true, neighborhood: true } } }
})
},
async createEvent(data: Omit<Event, 'id' | 'createdAt' | 'updatedAt' | 'deletedAt'>) {
if (this.isMock()) {
const newEvt: Event = {
id: `evt-${Date.now()}`,
...data,
createdAt: new Date(),
updatedAt: new Date()
}
globalForMockDb.events.push(newEvt)
return newEvt
}
return db.event.create({ data })
},
async updateEvent(id: string, data: Partial<Omit<Event, 'id' | 'createdAt' | 'updatedAt' | 'deletedAt'>>) {
if (this.isMock()) {
const idx = globalForMockDb.events.findIndex(e => e.id === id)
if (idx !== -1) {
globalForMockDb.events[idx] = {
...globalForMockDb.events[idx],
...data,
updatedAt: new Date()
} as Event
return globalForMockDb.events[idx]
}
return null
}
return db.event.update({ where: { id }, data })
},
async deleteEvent(id: string) {
if (this.isMock()) {
const idx = globalForMockDb.events.findIndex(e => e.id === id)
if (idx !== -1) {
globalForMockDb.events.splice(idx, 1)
return true
}
return false
}
await db.event.delete({ where: { id } })
return true
},
// Phase 3 AI Itineraries
async getItineraryByHash(hash: string) {
if (this.isMock()) {
return globalForMockDb.generatedItineraries.find(i => i.paramsHash === hash) || null
}
return db.generatedItinerary.findUnique({ where: { paramsHash: hash } })
},
async getItineraryById(id: string) {
if (this.isMock()) {
return globalForMockDb.generatedItineraries.find(i => i.id === id) || null
}
return db.generatedItinerary.findUnique({ where: { id } })
},
async createItinerary(data: Omit<GeneratedItinerary, 'id' | 'createdAt'>) {
if (this.isMock()) {
const newItin: GeneratedItinerary = {
id: `itin-${Date.now()}`,
...data,
createdAt: new Date()
}
globalForMockDb.generatedItineraries.push(newItin)
return newItin
}
return db.generatedItinerary.create({ data })
}
}