lomavuokraus/app/api/listings/route.ts
2025-11-24 17:15:20 +02:00

195 lines
7.7 KiB
TypeScript

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<T extends { locale: string }>(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 });
}
}