feat: implement dynamic categories, admin category CRUD, fix routing and cleanup
This commit is contained in:
+256
-13
@@ -1,4 +1,6 @@
|
||||
'use server'
|
||||
"use server"
|
||||
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import { uploadToOpeninary } from '@/lib/openinary'
|
||||
@@ -100,9 +102,9 @@ export async function approveSubmissionAction(id: string) {
|
||||
await mockDb.updateSubmissionStatus(id, 'APPROVED')
|
||||
|
||||
revalidatePath('/admin/submissions')
|
||||
revalidatePath('/restoranlar')
|
||||
revalidatePath('/apartlar')
|
||||
revalidatePath('/isletmeler')
|
||||
revalidatePath('/restaurants')
|
||||
revalidatePath('/aparts')
|
||||
revalidatePath('/businesses')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
@@ -115,12 +117,32 @@ export async function rejectSubmissionAction(id: string) {
|
||||
export async function deleteListingAction(id: string) {
|
||||
await mockDb.deleteListing(id)
|
||||
revalidatePath('/admin/listings')
|
||||
revalidatePath('/restoranlar')
|
||||
revalidatePath('/apartlar')
|
||||
revalidatePath('/isletmeler')
|
||||
revalidatePath('/restaurants')
|
||||
revalidatePath('/aparts')
|
||||
revalidatePath('/businesses')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function restoreListingAction(id: string) {
|
||||
await mockDb.restoreListing(id)
|
||||
revalidatePath('/admin/trash')
|
||||
revalidatePath('/admin/listings')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function hardDeleteListingAction(id: string) {
|
||||
try {
|
||||
await mockDb.hardDeleteListing(id)
|
||||
revalidatePath('/admin/trash')
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
if (err.message?.includes('RESTRICT') || err.message?.includes('Foreign key constraint')) {
|
||||
return { success: false, error: 'Silme başarısız: Bu mekana bağlı etkinlik (Event) veya kayıtlar var.' }
|
||||
}
|
||||
return { success: false, error: err.message || 'Bilinmeyen bir hata oluştu.' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function markMessageReadAction(id: string) {
|
||||
await mockDb.markMessageAsRead(id)
|
||||
revalidatePath('/admin/messages')
|
||||
@@ -225,9 +247,9 @@ export async function createOrUpdateListingAction(formData: FormData) {
|
||||
await mockDb.createListing(data)
|
||||
}
|
||||
revalidatePath('/admin/listings')
|
||||
revalidatePath('/restoranlar')
|
||||
revalidatePath('/apartlar')
|
||||
revalidatePath('/isletmeler')
|
||||
revalidatePath('/restaurants')
|
||||
revalidatePath('/aparts')
|
||||
revalidatePath('/businesses')
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message || 'Mekan kaydedilemedi.' }
|
||||
@@ -307,7 +329,7 @@ export async function createOrUpdateBlogPostAction(formData: FormData) {
|
||||
export async function deleteCollectionAction(id: string) {
|
||||
await mockDb.deleteCollection(id)
|
||||
revalidatePath('/admin/collections')
|
||||
revalidatePath('/seckiler')
|
||||
revalidatePath('/collections')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
@@ -356,10 +378,231 @@ export async function createOrUpdateCollectionAction(formData: FormData) {
|
||||
await mockDb.createCollection(data)
|
||||
}
|
||||
revalidatePath('/admin/collections')
|
||||
revalidatePath('/seckiler')
|
||||
revalidatePath(`/secki/${slug}`)
|
||||
revalidatePath('/collections')
|
||||
revalidatePath(`/collection/${slug}`)
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message || 'Koleksiyon kaydedilemedi.' }
|
||||
}
|
||||
}
|
||||
|
||||
// Events Actions (Phase 3)
|
||||
export async function deleteEventAction(id: string) {
|
||||
await mockDb.deleteEvent(id)
|
||||
revalidatePath('/admin/events')
|
||||
revalidatePath('/events')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function createOrUpdateEventAction(formData: FormData) {
|
||||
const id = formData.get('id') as string | null
|
||||
const slug = formData.get('slug') as string
|
||||
const listingId = formData.get('listingId') as string | null || null
|
||||
const titleTr = formData.get('titleTr') as string
|
||||
const titleEn = formData.get('titleEn') as string
|
||||
const titleRu = formData.get('titleRu') as string
|
||||
const descriptionTr = formData.get('descriptionTr') as string
|
||||
const descriptionEn = formData.get('descriptionEn') as string
|
||||
const descriptionRu = formData.get('descriptionRu') as string
|
||||
|
||||
const startDateStr = formData.get('startDate') as string
|
||||
const endDateStr = formData.get('endDate') as string | null
|
||||
const isSponsored = formData.get('isSponsored') === 'true'
|
||||
|
||||
const startDate = new Date(startDateStr)
|
||||
const endDate = endDateStr ? new Date(endDateStr) : null
|
||||
|
||||
// Handle Cover Image
|
||||
const coverImageFile = formData.get('coverImageFile') as File | null
|
||||
let coverImage = formData.get('coverImageUrl') as string | null || null
|
||||
|
||||
if (coverImageFile && coverImageFile.size > 0) {
|
||||
try {
|
||||
coverImage = await uploadToOpeninary(coverImageFile, `events/${slug}`)
|
||||
} catch (e: any) {
|
||||
console.error('Event cover upload error:', e)
|
||||
return { success: false, error: `Kapak görseli yüklenemedi: ${e.message}` }
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
slug,
|
||||
listingId,
|
||||
titleTr,
|
||||
titleEn,
|
||||
titleRu,
|
||||
descriptionTr,
|
||||
descriptionEn,
|
||||
descriptionRu,
|
||||
startDate,
|
||||
endDate,
|
||||
coverImage,
|
||||
isSponsored
|
||||
}
|
||||
|
||||
try {
|
||||
if (id && id !== 'new') {
|
||||
await mockDb.updateEvent(id, data)
|
||||
} else {
|
||||
await mockDb.createEvent(data)
|
||||
}
|
||||
revalidatePath('/admin/events')
|
||||
revalidatePath('/events')
|
||||
revalidatePath(`/event/${slug}`)
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message || 'Etkinlik kaydedilemedi.' }
|
||||
}
|
||||
}
|
||||
// Neighborhood Actions
|
||||
export async function deleteNeighborhoodAction(id: string) {
|
||||
try {
|
||||
const listings = await mockDb.getListings({ neighborhoodId: id })
|
||||
if (listings && listings.length > 0) {
|
||||
return { success: false, error: `Silme başarısız: Bu mahalleye kayıtlı ${listings.length} adet mekan var.` }
|
||||
}
|
||||
|
||||
await mockDb.deleteNeighborhood(id)
|
||||
revalidatePath('/admin/neighborhoods')
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
if (err.message?.includes('RESTRICT') || err.message?.includes('Foreign key constraint')) {
|
||||
return { success: false, error: 'Silme başarısız: Bu mahalleye bağlı gizli (silinmiş veya arşivlenmiş) mekanlar veya etkinlikler var.' }
|
||||
}
|
||||
return { success: false, error: err.message || 'Bilinmeyen bir hata oluştu.' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function createOrUpdateNeighborhoodAction(formData: FormData) {
|
||||
const id = formData.get('id') as string | null
|
||||
const slug = formData.get('slug') as string
|
||||
const nameTr = formData.get('nameTr') as string
|
||||
const nameEn = formData.get('nameEn') as string
|
||||
const nameRu = formData.get('nameRu') as string
|
||||
|
||||
if (!slug || !nameTr || !nameEn || !nameRu) {
|
||||
return { error: 'Zorunlu alanları doldurun.' }
|
||||
}
|
||||
|
||||
const data = {
|
||||
slug,
|
||||
nameTr,
|
||||
nameEn,
|
||||
nameRu,
|
||||
}
|
||||
|
||||
try {
|
||||
if (id && id !== 'new') {
|
||||
await mockDb.updateNeighborhood(id, data)
|
||||
} else {
|
||||
await mockDb.createNeighborhood(data)
|
||||
}
|
||||
revalidatePath('/admin/neighborhoods')
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message || 'Mahalle kaydedilemedi.' }
|
||||
}
|
||||
}
|
||||
|
||||
// AI Itinerary Planner Actions (Phase 3)
|
||||
import crypto from 'crypto'
|
||||
|
||||
async function generateMockItinerary(days: number, style: string, neighborhoodSlugs: string[]) {
|
||||
const allListings = await mockDb.getListings()
|
||||
const matchingListings = allListings.filter(l =>
|
||||
!l.deletedAt && l.isLocalApproved &&
|
||||
(neighborhoodSlugs.length === 0 ||
|
||||
(l.neighborhood && neighborhoodSlugs.includes(l.neighborhood.slug)))
|
||||
)
|
||||
|
||||
const pool = matchingListings.length > 0 ? matchingListings : allListings.filter(l => !l.deletedAt && l.isLocalApproved)
|
||||
|
||||
let md = `# marmaris local kişisel gezi rotası 🌴\n\n`
|
||||
md += `**gün sayısı:** ${days} gün | **gezi tarzı:** ${style === 'gastronomy' ? 'gurme' : style === 'relaxation' ? 'dinlenme' : 'macera'} | **keşif bölgeleri:** ${neighborhoodSlugs.join(', ')}\n\n`
|
||||
md += `bu rota, marmaris local topluluğu tarafından onaylanmış yerel işletmeler temel alınarak oluşturulmuştur.\n\n---\n\n`
|
||||
|
||||
for (let day = 1; day <= days; day++) {
|
||||
md += `## 🗓️ gün ${day}\n\n`
|
||||
|
||||
const dayListings = [...pool].sort(() => 0.5 - Math.random()).slice(0, 3)
|
||||
|
||||
if (dayListings.length >= 1) {
|
||||
md += `### 🌅 sabah: kahvaltı ve başlangıç\n`
|
||||
md += `güne yerel onaylı **[${dayListings[0].nameTr}](/${dayListings[0].category?.slug || 'isletme'}/${dayListings[0].slug})** işletmesinde başlayın. \n`
|
||||
md += `> **yerel ipucu:** ${dayListings[0].descriptionTr}\n\n`
|
||||
}
|
||||
|
||||
if (dayListings.length >= 2) {
|
||||
md += `### ☀️ öğle: keşif zamanı\n`
|
||||
md += `öğleden sonra **[${dayListings[1].nameTr}](/${dayListings[1].category?.slug || 'isletme'}/${dayListings[1].slug})** mekanına uğrayın ve çevreyi keşfedin.\n`
|
||||
md += `> **editör notu:** ${dayListings[1].address}\n\n`
|
||||
}
|
||||
|
||||
if (dayListings.length >= 3) {
|
||||
md += `### 🌌 akşam: gün batımı ve akşam yemeği\n`
|
||||
md += `akşamı şık bir akşam yemeğiyle taçlandırın: **[${dayListings[2].nameTr}](/${dayListings[2].category?.slug || 'isletme'}/${dayListings[2].slug})**.\n`
|
||||
md += `> **özel detaylar:** rating: ★${dayListings[2].rating} • tel: ${dayListings[2].phone || 'belirtilmemiş'}\n\n`
|
||||
}
|
||||
|
||||
md += `---\n\n`
|
||||
}
|
||||
|
||||
md += `*not: seyahatiniz boyunca yerel rehberimizdeki işletmeleri ziyaret etmeyi ve favorilerinize eklemeyi unutmayın!*`
|
||||
|
||||
return {
|
||||
content: md,
|
||||
listingIds: pool.map(l => l.id)
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateItineraryAction(days: number, style: string, neighborhoodSlugs: string[]) {
|
||||
const sorted = [...neighborhoodSlugs].sort()
|
||||
const paramsHash = crypto.createHash('md5').update(JSON.stringify({ days, style, neighborhoodSlugs: sorted })).digest('hex')
|
||||
|
||||
const existing = await mockDb.getItineraryByHash(paramsHash)
|
||||
if (existing) {
|
||||
return { success: true, id: existing.id }
|
||||
}
|
||||
|
||||
// Generate new program
|
||||
const { content, listingIds } = await generateMockItinerary(days, style, sorted)
|
||||
|
||||
const newItin = await mockDb.createItinerary({
|
||||
paramsHash,
|
||||
params: { days, style, neighborhoodSlugs: sorted },
|
||||
content,
|
||||
listingIds
|
||||
})
|
||||
|
||||
return { success: true, id: newItin.id }
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function createOrUpdateCategoryAction(formData: FormData) {
|
||||
const id = formData.get('id') as string
|
||||
const slug = formData.get('slug') as string
|
||||
const nameTr = formData.get('nameTr') as string
|
||||
const nameEn = formData.get('nameEn') as string
|
||||
const nameRu = formData.get('nameRu') as string
|
||||
|
||||
if (!slug || !nameTr || !nameEn || !nameRu) {
|
||||
throw new Error('Eksik alanlar var')
|
||||
}
|
||||
|
||||
if (id && id !== 'new') {
|
||||
await mockDb.updateCategory(id, { slug, nameTr, nameEn, nameRu })
|
||||
} else {
|
||||
await mockDb.createCategory({ slug, nameTr, nameEn, nameRu })
|
||||
}
|
||||
|
||||
revalidatePath('/admin/categories')
|
||||
redirect('/tr/admin/categories')
|
||||
}
|
||||
|
||||
export async function deleteCategoryAction(formData: FormData) {
|
||||
const id = formData.get('id') as string
|
||||
if (!id) return
|
||||
await mockDb.deleteCategory(id)
|
||||
revalidatePath('/admin/categories')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user