Files
marmarislocal/app/actions.ts
T

687 lines
22 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.
"use server"
import { redirect } from "next/navigation"
import { mockDb } from '@/lib/mockDb'
import { uploadToOpeninary } from '@/lib/openinary'
import { revalidatePath } from 'next/cache'
export async function submitBusinessAction(formData: FormData) {
const businessName = formData.get('businessName') as string
const categoryId = formData.get('categoryId') as string
const neighborhoodId = formData.get('neighborhoodId') as string
const address = formData.get('address') as string
const phone = formData.get('phone') as string
const whatsapp = formData.get('whatsapp') as string
const description = formData.get('description') as string
const contactName = formData.get('contactName') as string
const contactEmail = formData.get('contactEmail') as string
const imageFile = formData.get('imageFile') as File | null
if (!businessName || !categoryId || !neighborhoodId || !address || !description || !contactName || !contactEmail) {
return { error: 'Lütfen tüm zorunlu alanları doldurun.' }
}
let imageUrl = null
if (imageFile && imageFile.size > 0) {
try {
imageUrl = await uploadToOpeninary(imageFile, 'submissions')
} catch (e: any) {
console.error('Openinary upload error:', e)
return { error: `Görsel yüklenemedi: ${e.message}` }
}
}
await mockDb.createSubmission({
businessName,
categoryId,
neighborhoodId,
address,
phone,
whatsapp,
description,
contactName,
contactEmail,
imageUrl
})
revalidatePath('/admin/submissions')
return { success: true, error: undefined }
}
export async function submitContactMessageAction(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const subject = formData.get('subject') as string
const message = formData.get('message') as string
if (!name || !email || !subject || !message) {
return { error: 'Lütfen tüm alanları doldurun.' }
}
await mockDb.createMessage({
name,
email,
subject,
message
})
revalidatePath('/admin/messages')
return { success: true, error: undefined }
}
export async function approveSubmissionAction(id: string) {
const submission = await mockDb.getSubmissionById(id)
if (!submission) return { error: 'Başvuru bulunamadı' }
const slug = submission.businessName.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
await mockDb.createListing({
slug,
categoryId: submission.categoryId,
neighborhoodId: submission.neighborhoodId,
city: 'marmaris',
nameTr: submission.businessName,
nameEn: submission.businessName,
nameRu: submission.businessName,
descriptionTr: submission.description,
descriptionEn: submission.description,
descriptionRu: submission.description,
address: submission.address,
phone: submission.phone,
whatsapp: submission.whatsapp,
priceRange: 2,
rating: 5.0,
isLocalApproved: false,
isFeatured: false,
hasWidgetInstalled: false,
images: submission.imageUrl ? [submission.imageUrl] : []
})
await mockDb.updateSubmissionStatus(id, 'APPROVED')
revalidatePath('/admin/submissions')
revalidatePath('/restaurants')
revalidatePath('/aparts')
revalidatePath('/businesses')
return { success: true, error: undefined }
}
export async function rejectSubmissionAction(id: string) {
await mockDb.updateSubmissionStatus(id, 'REJECTED')
revalidatePath('/admin/submissions')
return { success: true, error: undefined }
}
export async function deleteListingAction(id: string) {
await mockDb.deleteListing(id)
revalidatePath('/admin/listings')
revalidatePath('/restaurants')
revalidatePath('/aparts')
revalidatePath('/businesses')
return { success: true, error: undefined }
}
export async function restoreListingAction(id: string) {
await mockDb.restoreListing(id)
revalidatePath('/admin/trash')
revalidatePath('/admin/listings')
return { success: true, error: undefined }
}
export async function hardDeleteListingAction(id: string) {
try {
await mockDb.hardDeleteListing(id)
revalidatePath('/admin/trash')
return { success: true, error: undefined }
} 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')
return { success: true, error: undefined }
}
export async function createOrUpdateListingAction(formData: FormData) {
const id = formData.get('id') as string | null
const slug = formData.get('slug') as string
const categoryId = formData.get('categoryId') as string
const neighborhoodId = formData.get('neighborhoodId') as string
const nameTr = formData.get('nameTr') as string
const nameEn = formData.get('nameEn') as string
const nameRu = formData.get('nameRu') as string
const descriptionTr = formData.get('descriptionTr') as string
const descriptionEn = formData.get('descriptionEn') as string
const descriptionRu = formData.get('descriptionRu') as string
const address = formData.get('address') as string
const phone = formData.get('phone') as string || null
const whatsapp = formData.get('whatsapp') as string || null
const website = formData.get('website') as string || null
const instagram = formData.get('instagram') as string || null
const priceRange = parseInt(formData.get('priceRange') as string)
const rating = formData.get('rating') ? parseFloat(formData.get('rating') as string) : null
const isLocalApproved = formData.get('isLocalApproved') === 'true'
const isFeatured = formData.get('isFeatured') === 'true'
const latitude = formData.get('latitude') ? parseFloat(formData.get('latitude') as string) : null
const longitude = formData.get('longitude') ? parseFloat(formData.get('longitude') as string) : null
const openingHoursStr = formData.get('openingHours') as string
const openingHours = openingHoursStr ? { all: openingHoursStr } : null
// Handle images
const images: string[] = []
const imageFile1 = formData.get('imageFile1') as File | null
const imageUrl1 = formData.get('imageUrl1') as string | null
const imageFile2 = formData.get('imageFile2') as File | null
const imageUrl2 = formData.get('imageUrl2') as string | null
// Process first image
if (imageFile1 && imageFile1.size > 0) {
try {
const url = await uploadToOpeninary(imageFile1, `listings/${slug}`)
images.push(url)
} catch (e: any) {
console.error('Image 1 upload error:', e)
return { success: false, error: `1. Görsel yüklenemedi: ${e.message}` }
}
} else if (imageUrl1) {
images.push(imageUrl1)
}
// Process second image
if (imageFile2 && imageFile2.size > 0) {
try {
const url = await uploadToOpeninary(imageFile2, `listings/${slug}`)
images.push(url)
} catch (e: any) {
console.error('Image 2 upload error:', e)
return { success: false, error: `2. Görsel yüklenemedi: ${e.message}` }
}
} else if (imageUrl2) {
images.push(imageUrl2)
}
const data = {
slug,
categoryId,
neighborhoodId,
city: 'marmaris',
nameTr,
nameEn,
nameRu,
descriptionTr,
descriptionEn,
descriptionRu,
address,
phone,
whatsapp,
website,
instagram,
priceRange,
rating,
isLocalApproved,
isFeatured,
latitude,
longitude,
openingHours,
images,
hasWidgetInstalled: false
}
try {
if (id && id !== 'new') {
await mockDb.updateListing(id, data)
} else {
await mockDb.createListing(data)
}
revalidatePath('/admin/listings')
revalidatePath('/restaurants')
revalidatePath('/aparts')
revalidatePath('/businesses')
return { success: true, error: undefined }
} catch (err: any) {
return { success: false, error: err.message || 'Mekan kaydedilemedi.' }
}
}
// Blog Actions
export async function deleteBlogPostAction(id: string) {
await mockDb.deleteBlogPost(id)
revalidatePath('/admin/blog')
revalidatePath('/blog')
return { success: true, error: undefined }
}
export async function createOrUpdateBlogPostAction(formData: FormData) {
const id = formData.get('id') as string | null
const slug = formData.get('slug') as string
const titleTr = formData.get('titleTr') as string
const titleEn = formData.get('titleEn') as string
const titleRu = formData.get('titleRu') as string
const contentTr = formData.get('contentTr') as string
const contentEn = formData.get('contentEn') as string
const contentRu = formData.get('contentRu') as string
const tagsStr = formData.get('tags') as string || ''
const tags = tagsStr.split(',').map(t => t.trim()).filter(Boolean)
const relatedListingIdsStr = formData.get('relatedListingIds') as string || ''
const relatedListingIds = relatedListingIdsStr.split(',').map(i => i.trim()).filter(Boolean)
const isPublished = formData.get('isPublished') === 'true'
const publishedAt = isPublished ? new Date() : 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, `blog/${slug}`)
} catch (e: any) {
console.error('Blog cover upload error:', e)
return { success: false, error: `Kapak görseli yüklenemedi: ${e.message}` }
}
}
const data = {
slug,
titleTr,
titleEn,
titleRu,
contentTr,
contentEn,
contentRu,
coverImage,
tags,
relatedListingIds,
publishedAt
}
try {
if (id && id !== 'new') {
await mockDb.updateBlogPost(id, data)
} else {
await mockDb.createBlogPost(data)
}
revalidatePath('/admin/blog')
revalidatePath('/blog')
revalidatePath(`/blog/${slug}`)
return { success: true, error: undefined }
} catch (err: any) {
return { success: false, error: err.message || 'Yazı kaydedilemedi.' }
}
}
// Collections Actions
export async function deleteCollectionAction(id: string) {
await mockDb.deleteCollection(id)
revalidatePath('/admin/collections')
revalidatePath('/collections')
return { success: true, error: undefined }
}
export async function createOrUpdateCollectionAction(formData: FormData) {
const id = formData.get('id') as string | null
const slug = formData.get('slug') as string
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 listingIdsStr = formData.get('listingIds') as string || ''
const listingIds = listingIdsStr.split(',').map(i => i.trim()).filter(Boolean)
// 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, `collections/${slug}`)
} catch (e: any) {
console.error('Collection cover upload error:', e)
return { success: false, error: `Kapak görseli yüklenemedi: ${e.message}` }
}
}
const data = {
slug,
titleTr,
titleEn,
titleRu,
descriptionTr,
descriptionEn,
descriptionRu,
coverImage,
listingIds
}
try {
if (id && id !== 'new') {
await mockDb.updateCollection(id, data)
} else {
await mockDb.createCollection(data)
}
revalidatePath('/admin/collections')
revalidatePath('/collections')
revalidatePath(`/collection/${slug}`)
return { success: true, error: undefined }
} 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, error: undefined }
}
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, error: undefined }
} 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, error: undefined }
} 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, error: undefined }
} 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')
}
export async function deleteSubmissionAction(id: string) {
await mockDb.deleteSubmission(id)
revalidatePath('/admin/submissions')
return { success: true, error: undefined }
}
export async function deleteMessageAction(id: string) {
await mockDb.deleteMessage(id)
revalidatePath('/admin/messages')
return { success: true, error: undefined }
}
export async function searchListingsAction(query: string) {
if (!query || query.length < 2) return []
const allListings = await mockDb.getListings()
const q = query.toLowerCase()
const results = allListings.filter(l =>
!l.deletedAt && (
l.nameTr.toLowerCase().includes(q) ||
l.nameEn?.toLowerCase().includes(q) ||
l.nameRu?.toLowerCase().includes(q)
)
).slice(0, 5) // Return max 5 suggestions
return results.map(l => ({
id: l.id,
nameTr: l.nameTr,
nameEn: l.nameEn,
nameRu: l.nameRu,
slug: l.slug,
categorySlug: l.category?.slug || 'restoran'
}))
}
export async function saveWidgetPartnerAction(formData: FormData) {
const id = formData.get('id') as string | null
const name = formData.get('name') as string
const url = formData.get('url') as string
const neighborhoodSlug = formData.get('neighborhoodSlug') as string | null
const isActive = formData.get('isActive') === 'true'
const isHidden = formData.get('isHidden') === 'true'
if (!name || !url) {
return { error: 'Lütfen zorunlu alanları doldurun.' }
}
const data = {
name,
url,
neighborhoodSlug: neighborhoodSlug || null,
isActive,
isHidden
}
try {
if (id && id !== 'new') {
await mockDb.updateWidgetPartner(id, data)
} else {
await mockDb.createWidgetPartner(data)
}
revalidatePath('/admin/widget-partners')
return { success: true, error: undefined }
} catch (error: any) {
return { error: error.message || 'Bir hata oluştu.' }
}
}
export async function deleteWidgetPartnerAction(id: string) {
await mockDb.deleteWidgetPartner(id)
revalidatePath('/admin/widget-partners')
return { success: true, error: undefined }
}