Fix draft listing save/reset and allow viewing drafts
Some checks failed
CI / checks (push) Has been cancelled
CI / checks (pull_request) Has been cancelled

This commit is contained in:
Tero Halla-aho 2025-12-15 21:23:25 +02:00
parent d9a5700162
commit 0b5ca0a190
4 changed files with 81 additions and 44 deletions

View file

@ -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,
});

View file

@ -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<string, string | number>) => 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 (
<main className="listing-shell">
@ -108,6 +126,11 @@ export default async function ListingPage({ params }: ListingPageProps) {
</div>
<div className="listing-layout">
<div className="panel listing-main">
{isDraftOrPending ? (
<div className="badge warning" style={{ marginBottom: 10, display: 'inline-block' }}>
{isOwnerView ? t('statusLabel') : 'Status'}: {listing.status}
</div>
) : null}
{isSample ? (
<div className="badge warning" style={{ marginBottom: 10, display: 'inline-block' }}>
{t('sampleBadge')}

View file

@ -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');

View file

@ -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<ListingWithTranslations | null> {
export async function getListingBySlug({ slug, locale, includeOwnerDraftsForUserId }: FetchOptions): Promise<ListingWithTranslations | null> {
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: {