import { NextResponse } from 'next/server'; import { ListingStatus, UserStatus, EvCharging, Prisma } from '@prisma/client'; import { prisma } from '../../../lib/prisma'; import { requireAuth } from '../../../lib/jwt'; import { resolveLocale } from '../../../lib/i18n'; function normalizeEvCharging(input?: string | null): EvCharging { const value = String(input ?? 'NONE').toUpperCase(); if (value === 'FREE') return EvCharging.FREE; if (value === 'PAID') return EvCharging.PAID; return EvCharging.NONE; } function pickTranslation(translations: T[], locale: string | null): T | null { if (!translations.length) return null; if (locale) { const exact = translations.find((t) => t.locale === locale); if (exact) return exact; } return translations[0]; } export async function GET(req: Request) { const url = new URL(req.url); const searchParams = url.searchParams; const q = searchParams.get('q')?.trim(); const city = searchParams.get('city')?.trim(); const region = searchParams.get('region')?.trim(); const evChargingParam = searchParams.get('evCharging'); const evCharging = evChargingParam ? normalizeEvCharging(evChargingParam) : null; const locale = resolveLocale({ cookieLocale: null, acceptLanguage: req.headers.get('accept-language') }); const limit = Math.min(Number(searchParams.get('limit') ?? 40), 100); const where: Prisma.ListingWhereInput = { status: ListingStatus.PUBLISHED, removedAt: null, city: city ? { contains: city, mode: 'insensitive' } : undefined, region: region ? { contains: region, mode: 'insensitive' } : undefined, evCharging: evCharging ?? undefined, translations: q ? { some: { OR: [ { title: { contains: q, mode: 'insensitive' } }, { description: { contains: q, mode: 'insensitive' } }, { teaser: { contains: q, mode: 'insensitive' } }, ], }, } : undefined, }; const listings = await prisma.listing.findMany({ where, include: { translations: { select: { id: true, locale: true, title: true, slug: true, teaser: true, description: true } }, images: { select: { id: true, url: true, altText: true, order: true, isCover: true }, orderBy: { order: 'asc' } }, }, orderBy: { createdAt: 'desc' }, take: Number.isNaN(limit) ? 40 : limit, }); const payload = listings.map((listing) => { const translation = pickTranslation(listing.translations, locale); const fallback = listing.translations[0]; return { id: listing.id, title: translation?.title ?? fallback?.title ?? 'Listing', slug: translation?.slug ?? fallback?.slug ?? '', teaser: translation?.teaser ?? translation?.description ?? fallback?.description ?? null, locale: translation?.locale ?? fallback?.locale ?? locale, country: listing.country, region: listing.region, city: listing.city, streetAddress: listing.streetAddress, addressNote: listing.addressNote, latitude: listing.latitude, longitude: listing.longitude, hasSauna: listing.hasSauna, hasFireplace: listing.hasFireplace, hasWifi: listing.hasWifi, petsAllowed: listing.petsAllowed, byTheLake: listing.byTheLake, hasAirConditioning: listing.hasAirConditioning, evCharging: listing.evCharging, maxGuests: listing.maxGuests, bedrooms: listing.bedrooms, beds: listing.beds, bathrooms: listing.bathrooms, priceHintPerNightCents: listing.priceHintPerNightCents, coverImage: (listing.images.find((img) => img.isCover) ?? listing.images[0])?.url ?? null, }; }); return NextResponse.json({ listings: payload }); } const MAX_IMAGES = 10; export async function POST(req: Request) { try { const auth = await requireAuth(req); const user = await prisma.user.findUnique({ where: { id: auth.userId } }); if (!user || !user.emailVerifiedAt || !user.approvedAt || user.status !== UserStatus.ACTIVE) { return NextResponse.json({ error: 'User not permitted to create listings' }, { status: 403 }); } const body = await req.json(); const slug = String(body.slug ?? '').trim().toLowerCase(); const locale = String(body.locale ?? 'en').toLowerCase(); const title = String(body.title ?? '').trim(); const description = String(body.description ?? '').trim(); const country = String(body.country ?? '').trim(); const region = String(body.region ?? '').trim(); const city = String(body.city ?? '').trim(); const streetAddress = String(body.streetAddress ?? '').trim(); const contactName = String(body.contactName ?? '').trim(); const contactEmail = String(body.contactEmail ?? '').trim(); if (!slug || !title || !description || !country || !region || !city || !contactEmail || !contactName) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); } const maxGuests = Number(body.maxGuests ?? 1); const bedrooms = Number(body.bedrooms ?? 1); const beds = Number(body.beds ?? 1); const bathrooms = Number(body.bathrooms ?? 1); const priceHintPerNightCents = body.priceHintPerNightCents ? Number(body.priceHintPerNightCents) : null; const images = Array.isArray(body.images) ? body.images.slice(0, MAX_IMAGES) : []; const coverImageIndex = Math.min(Math.max(Number(body.coverImageIndex ?? 1), 1), images.length || 1); const autoApprove = process.env.AUTO_APPROVE_LISTINGS === 'true' || auth.role === 'ADMIN'; const status = autoApprove ? ListingStatus.PUBLISHED : ListingStatus.PENDING; const listing = await prisma.listing.create({ data: { ownerId: user.id, status, approvedAt: autoApprove ? new Date() : null, approvedById: autoApprove && auth.role === 'ADMIN' ? user.id : null, country, region, city, streetAddress: streetAddress || null, addressNote: body.addressNote ?? null, latitude: body.latitude !== undefined && body.latitude !== null && body.latitude !== '' ? Number(body.latitude) : null, longitude: body.longitude !== undefined && body.longitude !== null && body.longitude !== '' ? Number(body.longitude) : null, maxGuests, bedrooms, beds, bathrooms, hasSauna: Boolean(body.hasSauna), hasFireplace: Boolean(body.hasFireplace), hasWifi: Boolean(body.hasWifi), petsAllowed: Boolean(body.petsAllowed), byTheLake: Boolean(body.byTheLake), hasAirConditioning: Boolean(body.hasAirConditioning), evCharging: normalizeEvCharging(body.evCharging), priceHintPerNightCents, contactName, contactEmail, contactPhone: body.contactPhone ?? null, externalUrl: body.externalUrl ?? null, published: status === ListingStatus.PUBLISHED, translations: { create: { locale, slug, title, description, teaser: body.teaser ?? null, }, }, images: images.length ? { create: images.map((img: any, idx: number) => ({ url: String(img.url ?? ''), altText: img.altText ? String(img.altText) : null, order: idx + 1, isCover: coverImageIndex === idx + 1, })), } : undefined, }, include: { translations: true, images: true }, }); return NextResponse.json({ ok: true, listing }); } catch (error: any) { console.error('Create listing error', error); const message = error?.code === 'P2002' ? 'Slug already exists for this locale' : 'Failed to create listing'; return NextResponse.json({ error: message }, { status: 400 }); } }