diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 0cd0699..fdbc2ac 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -221,7 +221,7 @@ export async function POST(req: Request) { const calendarUrls = normalizeCalendarUrls(body.calendarUrls); const translationsInputRaw = Array.isArray(body.translations) ? body.translations : []; type TranslationInput = { locale: string; title: string; description: string; teaser: string | null; slug: string }; - const translationsInput = + let translationsInput = translationsInputRaw .map((item: any) => ({ locale: String(item.locale ?? '').toLowerCase(), @@ -230,18 +230,18 @@ export async function POST(req: Request) { teaser: typeof item.teaser === 'string' ? item.teaser.trim() : null, slug: String(item.slug ?? slug).trim().toLowerCase(), })) - .filter((t: any) => t.locale && t.title && t.description) || []; + .filter((t: any) => t.locale && (saveDraft || (t.title && t.description))) || []; const fallbackLocale = String(body.locale ?? 'en').toLowerCase(); const fallbackTranslationTitle = typeof body.title === 'string' ? body.title.trim() : ''; const fallbackTranslationDescription = typeof body.description === 'string' ? body.description.trim() : ''; const fallbackTranslationTeaser = typeof body.teaser === 'string' ? body.teaser.trim() : null; - if (translationsInput.length === 0 && fallbackTranslationTitle && fallbackTranslationDescription) { + if (translationsInput.length === 0 && (fallbackTranslationTitle || saveDraft) && (fallbackTranslationDescription || saveDraft)) { translationsInput.push({ locale: fallbackLocale, - title: fallbackTranslationTitle, - description: fallbackTranslationDescription, + title: fallbackTranslationTitle ?? '', + description: fallbackTranslationDescription ?? '', teaser: fallbackTranslationTeaser, slug, }); diff --git a/app/listings/[slug]/page.tsx b/app/listings/[slug]/page.tsx index efff2a1..3d1986d 100644 --- a/app/listings/[slug]/page.tsx +++ b/app/listings/[slug]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from 'next'; +import { ListingStatus } from '@prisma/client'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { cookies, headers } from 'next/headers'; @@ -6,6 +7,7 @@ import { getListingBySlug, DEFAULT_LOCALE, withResolvedListingImages } from '../ import { SAMPLE_LISTING_SLUGS } from '../../../lib/sampleListing'; import { resolveLocale, t as translate } from '../../../lib/i18n'; import { getCalendarRanges, expandBlockedDates } from '../../../lib/calendar'; +import { verifyAccessToken } from '../../../lib/jwt'; import AvailabilityCalendar from '../../components/AvailabilityCalendar'; type ListingPageProps = { @@ -42,8 +44,22 @@ export default async function ListingPage({ params }: ListingPageProps) { const cookieStore = cookies(); const locale = resolveLocale({ cookieLocale: cookieStore.get('locale')?.value, acceptLanguage: headers().get('accept-language') }); const t = (key: any, vars?: Record) => translate(locale, key as any, vars); + const sessionToken = cookieStore.get('session_token')?.value; + let viewerId: string | null = null; + if (sessionToken) { + try { + const payload = await verifyAccessToken(sessionToken); + viewerId = payload.userId; + } catch { + viewerId = null; + } + } - const translationRaw = await getListingBySlug({ slug: params.slug, locale: locale ?? DEFAULT_LOCALE }); + const translationRaw = await getListingBySlug({ + slug: params.slug, + locale: locale ?? DEFAULT_LOCALE, + includeOwnerDraftsForUserId: viewerId ?? undefined, + }); const translation = translationRaw ? withResolvedListingImages(translationRaw) : null; if (!translation) { @@ -100,6 +116,8 @@ export default async function ListingPage({ params }: ListingPageProps) { .filter(Boolean) .join(' ยท ') : t('priceNotSet'); + const isDraftOrPending = listing.status !== ListingStatus.PUBLISHED; + const isOwnerView = viewerId && listing.ownerId === viewerId; return (
@@ -108,6 +126,11 @@ export default async function ListingPage({ params }: ListingPageProps) {
+ {isDraftOrPending ? ( +
+ {isOwnerView ? t('statusLabel') : 'Status'}: {listing.status} +
+ ) : null} {isSample ? (
{t('sampleBadge')} diff --git a/app/listings/new/page.tsx b/app/listings/new/page.tsx index f3805cd..b9713cf 100644 --- a/app/listings/new/page.tsx +++ b/app/listings/new/page.tsx @@ -387,41 +387,43 @@ export default function NewListingPage() { ? t('createListingSuccess', { id: data.listing.id, status: 'DRAFT' }) : t('createListingSuccess', { id: data.listing.id, status: data.listing.status }), ); - setSlug(''); - setTranslations({ - en: { title: '', description: '', teaser: '' }, - fi: { title: '', description: '', teaser: '' }, - sv: { title: '', description: '', teaser: '' }, - }); - setMaxGuests(4); - setBedrooms(2); - setBeds(3); - setBathrooms(1); - setPriceWeekday(''); - setPriceWeekend(''); - setHasSauna(true); - setHasFireplace(true); - setHasWifi(true); - setPetsAllowed(false); - setByTheLake(false); - setHasAirConditioning(false); - setHasKitchen(true); - setHasDishwasher(false); - setHasWashingMachine(false); - setHasBarbecue(false); - setHasMicrowave(false); - setHasFreeParking(false); - setRegion(''); - setCity(''); - setStreetAddress(''); - setAddressNote(''); - setLatitude(''); - setLongitude(''); - setContactName(''); - setContactEmail(''); - setCalendarUrls(''); - setSelectedImages([]); - setCoverImageIndex(1); + if (!saveDraft) { + setSlug(''); + setTranslations({ + en: { title: '', description: '', teaser: '' }, + fi: { title: '', description: '', teaser: '' }, + sv: { title: '', description: '', teaser: '' }, + }); + setMaxGuests(4); + setBedrooms(2); + setBeds(3); + setBathrooms(1); + setPriceWeekday(''); + setPriceWeekend(''); + setHasSauna(true); + setHasFireplace(true); + setHasWifi(true); + setPetsAllowed(false); + setByTheLake(false); + setHasAirConditioning(false); + setHasKitchen(true); + setHasDishwasher(false); + setHasWashingMachine(false); + setHasBarbecue(false); + setHasMicrowave(false); + setHasFreeParking(false); + setRegion(''); + setCity(''); + setStreetAddress(''); + setAddressNote(''); + setLatitude(''); + setLongitude(''); + setContactName(''); + setContactEmail(''); + setCalendarUrls(''); + setSelectedImages([]); + setCoverImageIndex(1); + } } } catch (err) { setError('Failed to create listing'); diff --git a/lib/listings.ts b/lib/listings.ts index 0292795..08404e9 100644 --- a/lib/listings.ts +++ b/lib/listings.ts @@ -16,6 +16,7 @@ export type ListingWithTranslations = Prisma.ListingTranslationGetPayload<{ type FetchOptions = { slug: string; locale?: string; + includeOwnerDraftsForUserId?: string; }; function resolveImageUrl(img: { id: string; url: string | null; size: number | null }) { @@ -29,11 +30,22 @@ function resolveImageUrl(img: { id: string; url: string | null; size: number | n * Fetch a listing translation by slug and locale. * Falls back to any locale if the requested locale is missing. */ -export async function getListingBySlug({ slug, locale }: FetchOptions): Promise { +export async function getListingBySlug({ slug, locale, includeOwnerDraftsForUserId }: FetchOptions): Promise { const targetLocale = locale ?? DEFAULT_LOCALE; + const listingWhere: Prisma.ListingWhereInput = + includeOwnerDraftsForUserId + ? { + removedAt: null, + OR: [ + { status: ListingStatus.PUBLISHED }, + { ownerId: includeOwnerDraftsForUserId, status: { in: [ListingStatus.DRAFT, ListingStatus.PENDING] } }, + ], + } + : { status: ListingStatus.PUBLISHED, removedAt: null }; + const translation = await prisma.listingTranslation.findFirst({ - where: { slug, locale: targetLocale, listing: { status: ListingStatus.PUBLISHED, removedAt: null } }, + where: { slug, locale: targetLocale, listing: listingWhere }, include: { listing: { include: { @@ -50,7 +62,7 @@ export async function getListingBySlug({ slug, locale }: FetchOptions): Promise< // Fallback: first translation for this slug return prisma.listingTranslation.findFirst({ - where: { slug, listing: { status: ListingStatus.PUBLISHED, removedAt: null } }, + where: { slug, listing: listingWhere }, include: { listing: { include: {