feat: Implement Phase 2 features including blog, collections, saved listings, manifest, and api cron sync
This commit is contained in:
+478
-103
@@ -51,6 +51,11 @@ export interface Listing {
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
|
||||
// Phase 2
|
||||
menuUrl?: string | null
|
||||
isFeatured: boolean
|
||||
featuredUntil?: Date | null
|
||||
}
|
||||
|
||||
export interface BusinessSubmission {
|
||||
@@ -81,12 +86,62 @@ export interface ContactMessage {
|
||||
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
|
||||
}
|
||||
|
||||
const globalForMockDb = globalThis as unknown as {
|
||||
categories: Category[]
|
||||
neighborhoods: Neighborhood[]
|
||||
listings: Listing[]
|
||||
submissions: BusinessSubmission[]
|
||||
messages: ContactMessage[]
|
||||
blogPosts: BlogPost[]
|
||||
collections: Collection[]
|
||||
instagramFeedCaches: InstagramFeedCache[]
|
||||
initialized: boolean
|
||||
}
|
||||
|
||||
@@ -108,7 +163,7 @@ if (!globalForMockDb.initialized) {
|
||||
globalForMockDb.submissions = []
|
||||
globalForMockDb.messages = []
|
||||
|
||||
// Pre-populate with realistic Marmaris data
|
||||
// Seed Listings with Phase 2 fields
|
||||
globalForMockDb.listings = [
|
||||
{
|
||||
id: 'list-1',
|
||||
@@ -126,7 +181,7 @@ if (!globalForMockDb.initialized) {
|
||||
phone: '+90 252 412 34 56',
|
||||
whatsapp: '+90 532 123 45 67',
|
||||
website: 'https://iskelemarmaris.com',
|
||||
instagram: 'https://instagram.com/iskele_marmaris',
|
||||
instagram: 'iskele_marmaris',
|
||||
priceRange: 3,
|
||||
rating: 4.8,
|
||||
isLocalApproved: true,
|
||||
@@ -137,8 +192,10 @@ if (!globalForMockDb.initialized) {
|
||||
{ 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(),
|
||||
updatedAt: new Date()
|
||||
createdAt: new Date(Date.now() - 3600000 * 24 * 5),
|
||||
updatedAt: new Date(),
|
||||
menuUrl: 'https://iskelemarmaris.com/menu',
|
||||
isFeatured: true
|
||||
},
|
||||
{
|
||||
id: 'list-2',
|
||||
@@ -156,7 +213,7 @@ if (!globalForMockDb.initialized) {
|
||||
phone: '+90 252 412 78 90',
|
||||
whatsapp: '+90 533 987 65 43',
|
||||
website: 'https://mavibeyazmarmaris.com',
|
||||
instagram: 'https://instagram.com/mavibeyaz_marmaris',
|
||||
instagram: 'mavibeyaz_marmaris',
|
||||
priceRange: 2,
|
||||
rating: 4.5,
|
||||
isLocalApproved: true,
|
||||
@@ -166,8 +223,10 @@ if (!globalForMockDb.initialized) {
|
||||
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(),
|
||||
updatedAt: new Date()
|
||||
createdAt: new Date(Date.now() - 3600000 * 24 * 4),
|
||||
updatedAt: new Date(),
|
||||
menuUrl: 'https://mavibeyazmarmaris.com/digital-menu',
|
||||
isFeatured: false
|
||||
},
|
||||
{
|
||||
id: 'list-3',
|
||||
@@ -185,7 +244,7 @@ if (!globalForMockDb.initialized) {
|
||||
phone: '+90 252 413 11 22',
|
||||
whatsapp: null,
|
||||
website: null,
|
||||
instagram: 'https://instagram.com/dostlarkebap_marmaris',
|
||||
instagram: 'dostlarkebap_marmaris',
|
||||
priceRange: 1,
|
||||
rating: 4.7,
|
||||
isLocalApproved: false,
|
||||
@@ -195,8 +254,9 @@ if (!globalForMockDb.initialized) {
|
||||
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(),
|
||||
updatedAt: new Date()
|
||||
createdAt: new Date(Date.now() - 3600000 * 24 * 3),
|
||||
updatedAt: new Date(),
|
||||
isFeatured: false
|
||||
},
|
||||
{
|
||||
id: 'list-4',
|
||||
@@ -225,8 +285,9 @@ if (!globalForMockDb.initialized) {
|
||||
{ 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(),
|
||||
updatedAt: new Date()
|
||||
createdAt: new Date(Date.now() - 3600000 * 24 * 2),
|
||||
updatedAt: new Date(),
|
||||
isFeatured: true
|
||||
},
|
||||
{
|
||||
id: 'list-5',
|
||||
@@ -254,55 +315,57 @@ if (!globalForMockDb.initialized) {
|
||||
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(),
|
||||
updatedAt: new Date()
|
||||
createdAt: new Date(Date.now() - 3600000 * 24 * 1),
|
||||
updatedAt: new Date(),
|
||||
isFeatured: false
|
||||
},
|
||||
{
|
||||
id: 'list-6',
|
||||
slug: 'marina-dalis-merkezi',
|
||||
slug: 'marina-diving-center',
|
||||
categoryId: 'cat-3',
|
||||
neighborhoodId: 'neigh-2',
|
||||
neighborhoodId: 'neigh-1',
|
||||
city: 'marmaris',
|
||||
nameTr: 'Marina Dalış Merkezi',
|
||||
nameEn: 'Marina Diving Center',
|
||||
nameRu: 'Дайвинг-центр Марина',
|
||||
descriptionTr: 'Marmaris\'in eşsiz koylarında profesyonel eğitmenler eşliğinde tüplü dalış deneyimi. Başlangıç seviyesinden PADI sertifikasyonuna kadar hizmet.',
|
||||
descriptionEn: 'Scuba diving experience in unique bays of Marmaris accompanied by professional instructors. Service from discovery dive to PADI certification.',
|
||||
descriptionRu: 'Опыт подводного плавания в уникальных бухтах Мармариса в сопровождении профессиональных инструкторов. Услуги от ознакомительного погружения до сертификации PADI.',
|
||||
address: 'Liman Yolu No:32, İçmeler, Marmaris',
|
||||
phone: '+90 252 455 22 33',
|
||||
whatsapp: '+90 536 777 88 99',
|
||||
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 и PADI.',
|
||||
address: 'Yat Limanı Belediye İskelesi, Marmaris',
|
||||
phone: '+90 532 234 56 78',
|
||||
whatsapp: '+90 532 234 56 78',
|
||||
website: 'https://marinadivingmarmaris.com',
|
||||
instagram: 'https://instagram.com/marinadiving_marmaris',
|
||||
priceRange: 3,
|
||||
instagram: null,
|
||||
priceRange: 2,
|
||||
rating: 4.9,
|
||||
isLocalApproved: true,
|
||||
latitude: 36.8020,
|
||||
longitude: 28.2320,
|
||||
openingHours: { all: '08:30 - 19:30' },
|
||||
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(),
|
||||
updatedAt: new Date()
|
||||
createdAt: new Date(Date.now() - 3600000 * 12),
|
||||
updatedAt: new Date(),
|
||||
isFeatured: false
|
||||
},
|
||||
{
|
||||
id: 'list-7',
|
||||
slug: 'ege-ruzgari-tekne-kiralama',
|
||||
slug: 'aegean-wind-yacht-charter',
|
||||
categoryId: 'cat-3',
|
||||
neighborhoodId: 'neigh-1',
|
||||
city: 'marmaris',
|
||||
nameTr: 'Ege Rüzgarı Tekne Kiralama',
|
||||
nameEn: 'Aegean Wind Boat Rental',
|
||||
nameRu: 'Аренда Лодок Эгейский Ветер',
|
||||
descriptionTr: 'Kaptanlı veya kaptansız günlük ve haftalık özel tekne kiralama hizmeti. Marmaris koylarını kendi rotanızda özgürce keşfedin.',
|
||||
descriptionEn: 'Daily and weekly private boat rental service with or without skipper. Discover Marmaris bays freely on your own route.',
|
||||
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: 'https://instagram.com/egeruzgariboat',
|
||||
instagram: 'egeruzgariboat',
|
||||
priceRange: 3,
|
||||
rating: 4.8,
|
||||
isLocalApproved: true,
|
||||
@@ -312,8 +375,9 @@ if (!globalForMockDb.initialized) {
|
||||
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(),
|
||||
updatedAt: new Date()
|
||||
createdAt: new Date(Date.now() - 3600000 * 6),
|
||||
updatedAt: new Date(),
|
||||
isFeatured: false
|
||||
},
|
||||
{
|
||||
id: 'list-8',
|
||||
@@ -341,8 +405,102 @@ if (!globalForMockDb.initialized) {
|
||||
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
|
||||
}
|
||||
]
|
||||
|
||||
// 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()
|
||||
}
|
||||
]
|
||||
|
||||
@@ -358,7 +516,7 @@ export const mockDb = {
|
||||
if (this.isMock()) {
|
||||
return globalForMockDb.categories
|
||||
}
|
||||
return db.category.findMany({ orderBy: { nameTr: 'asc' } })
|
||||
return db.category.findMany({ orderBy: { id: 'asc' } })
|
||||
},
|
||||
|
||||
async getCategoryBySlug(slug: string) {
|
||||
@@ -368,25 +526,11 @@ export const mockDb = {
|
||||
return db.category.findUnique({ where: { slug } })
|
||||
},
|
||||
|
||||
async createCategory(data: { slug: string; nameTr: string; nameEn: string; nameRu: string }) {
|
||||
async getCategoryById(id: string) {
|
||||
if (this.isMock()) {
|
||||
const newCat = { id: `cat-${Date.now()}`, ...data }
|
||||
globalForMockDb.categories.push(newCat)
|
||||
return newCat
|
||||
return globalForMockDb.categories.find(c => c.id === id) || null
|
||||
}
|
||||
return db.category.create({ data })
|
||||
},
|
||||
|
||||
async updateCategory(id: string, data: { slug: string; nameTr: string; nameEn: string; nameRu: string }) {
|
||||
if (this.isMock()) {
|
||||
const idx = globalForMockDb.categories.findIndex(c => c.id === id)
|
||||
if (idx !== -1) {
|
||||
globalForMockDb.categories[idx] = { ...globalForMockDb.categories[idx], ...data }
|
||||
return globalForMockDb.categories[idx]
|
||||
}
|
||||
return null
|
||||
}
|
||||
return db.category.update({ where: { id }, data })
|
||||
return db.category.findUnique({ where: { id } })
|
||||
},
|
||||
|
||||
// Neighborhoods
|
||||
@@ -394,7 +538,7 @@ export const mockDb = {
|
||||
if (this.isMock()) {
|
||||
return globalForMockDb.neighborhoods
|
||||
}
|
||||
return db.neighborhood.findMany({ orderBy: { nameTr: 'asc' } })
|
||||
return db.neighborhood.findMany({ orderBy: { id: 'asc' } })
|
||||
},
|
||||
|
||||
async getNeighborhoodBySlug(slug: string) {
|
||||
@@ -404,52 +548,49 @@ export const mockDb = {
|
||||
return db.neighborhood.findUnique({ where: { slug } })
|
||||
},
|
||||
|
||||
async createNeighborhood(data: { slug: string; nameTr: string; nameEn: string; nameRu: string }) {
|
||||
async getNeighborhoodById(id: string) {
|
||||
if (this.isMock()) {
|
||||
const newNeigh = { id: `neigh-${Date.now()}`, ...data }
|
||||
globalForMockDb.neighborhoods.push(newNeigh)
|
||||
return newNeigh
|
||||
return globalForMockDb.neighborhoods.find(n => n.id === id) || null
|
||||
}
|
||||
return db.neighborhood.create({ data })
|
||||
return db.neighborhood.findUnique({ where: { id } })
|
||||
},
|
||||
|
||||
async updateNeighborhood(id: string, data: { slug: string; nameTr: string; nameEn: string; nameRu: string }) {
|
||||
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]
|
||||
}
|
||||
return null
|
||||
}
|
||||
return db.neighborhood.update({ where: { id }, data })
|
||||
},
|
||||
|
||||
// Listings
|
||||
// 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)
|
||||
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)
|
||||
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),
|
||||
@@ -463,6 +604,7 @@ export const mockDb = {
|
||||
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' } },
|
||||
@@ -476,7 +618,10 @@ export const mockDb = {
|
||||
return db.listing.findMany({
|
||||
where,
|
||||
include: { category: true, neighborhood: true, images: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
orderBy: [
|
||||
{ isFeatured: 'desc' },
|
||||
{ createdAt: 'desc' }
|
||||
]
|
||||
})
|
||||
},
|
||||
|
||||
@@ -522,18 +667,19 @@ export const mockDb = {
|
||||
...rest,
|
||||
images: gallery,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
updatedAt: new Date(),
|
||||
isFeatured: data.isFeatured || false
|
||||
}
|
||||
globalForMockDb.listings.unshift(newListing)
|
||||
globalForMockDb.listings.push(newListing)
|
||||
return newListing
|
||||
}
|
||||
|
||||
return db.listing.create({
|
||||
data: {
|
||||
...rest,
|
||||
images: images ? { create: images.map(url => ({ url })) } : undefined
|
||||
},
|
||||
include: { images: true }
|
||||
images: {
|
||||
create: (images || []).map(url => ({ url }))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -542,33 +688,35 @@ export const mockDb = {
|
||||
if (this.isMock()) {
|
||||
const idx = globalForMockDb.listings.findIndex(l => l.id === id)
|
||||
if (idx !== -1) {
|
||||
const oldListing = globalForMockDb.listings[idx]
|
||||
const updatedGallery = images
|
||||
? images.map((url, i) => ({ id: `img-${id}-${i}`, listingId: id, url }))
|
||||
: oldListing.images
|
||||
const updated = {
|
||||
...oldListing,
|
||||
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: updatedGallery,
|
||||
images: gallery,
|
||||
updatedAt: new Date()
|
||||
} as Listing
|
||||
globalForMockDb.listings[idx] = updated
|
||||
return updated
|
||||
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,
|
||||
images: images ? { create: images.map(url => ({ url })) } : undefined
|
||||
},
|
||||
include: { images: true }
|
||||
data: rest
|
||||
})
|
||||
},
|
||||
|
||||
@@ -671,5 +819,232 @@ export const mockDb = {
|
||||
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() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user