feat: implement dynamic categories, admin category CRUD, fix routing and cleanup

This commit is contained in:
AyrisAI
2026-07-13 13:51:48 +03:00
parent 2f2dafcfb9
commit 5b44c78396
54 changed files with 3619 additions and 473 deletions
+256 -13
View File
@@ -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')
}