Merge pull request 'Fix draft listing save/reset and allow viewing drafts' (#5) from feature/draftfix into master
Some checks are pending
CI / checks (push) Waiting to run
Some checks are pending
CI / checks (push) Waiting to run
Reviewed-on: #5
This commit is contained in:
commit
1fe2da1f66
4 changed files with 81 additions and 44 deletions
|
|
@ -221,7 +221,7 @@ export async function POST(req: Request) {
|
||||||
const calendarUrls = normalizeCalendarUrls(body.calendarUrls);
|
const calendarUrls = normalizeCalendarUrls(body.calendarUrls);
|
||||||
const translationsInputRaw = Array.isArray(body.translations) ? body.translations : [];
|
const translationsInputRaw = Array.isArray(body.translations) ? body.translations : [];
|
||||||
type TranslationInput = { locale: string; title: string; description: string; teaser: string | null; slug: string };
|
type TranslationInput = { locale: string; title: string; description: string; teaser: string | null; slug: string };
|
||||||
const translationsInput =
|
let translationsInput =
|
||||||
translationsInputRaw
|
translationsInputRaw
|
||||||
.map((item: any) => ({
|
.map((item: any) => ({
|
||||||
locale: String(item.locale ?? '').toLowerCase(),
|
locale: String(item.locale ?? '').toLowerCase(),
|
||||||
|
|
@ -230,18 +230,18 @@ export async function POST(req: Request) {
|
||||||
teaser: typeof item.teaser === 'string' ? item.teaser.trim() : null,
|
teaser: typeof item.teaser === 'string' ? item.teaser.trim() : null,
|
||||||
slug: String(item.slug ?? slug).trim().toLowerCase(),
|
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 fallbackLocale = String(body.locale ?? 'en').toLowerCase();
|
||||||
const fallbackTranslationTitle = typeof body.title === 'string' ? body.title.trim() : '';
|
const fallbackTranslationTitle = typeof body.title === 'string' ? body.title.trim() : '';
|
||||||
const fallbackTranslationDescription = typeof body.description === 'string' ? body.description.trim() : '';
|
const fallbackTranslationDescription = typeof body.description === 'string' ? body.description.trim() : '';
|
||||||
const fallbackTranslationTeaser = typeof body.teaser === 'string' ? body.teaser.trim() : null;
|
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({
|
translationsInput.push({
|
||||||
locale: fallbackLocale,
|
locale: fallbackLocale,
|
||||||
title: fallbackTranslationTitle,
|
title: fallbackTranslationTitle ?? '',
|
||||||
description: fallbackTranslationDescription,
|
description: fallbackTranslationDescription ?? '',
|
||||||
teaser: fallbackTranslationTeaser,
|
teaser: fallbackTranslationTeaser,
|
||||||
slug,
|
slug,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { ListingStatus } from '@prisma/client';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { cookies, headers } from 'next/headers';
|
import { cookies, headers } from 'next/headers';
|
||||||
|
|
@ -6,6 +7,7 @@ import { getListingBySlug, DEFAULT_LOCALE, withResolvedListingImages } from '../
|
||||||
import { SAMPLE_LISTING_SLUGS } from '../../../lib/sampleListing';
|
import { SAMPLE_LISTING_SLUGS } from '../../../lib/sampleListing';
|
||||||
import { resolveLocale, t as translate } from '../../../lib/i18n';
|
import { resolveLocale, t as translate } from '../../../lib/i18n';
|
||||||
import { getCalendarRanges, expandBlockedDates } from '../../../lib/calendar';
|
import { getCalendarRanges, expandBlockedDates } from '../../../lib/calendar';
|
||||||
|
import { verifyAccessToken } from '../../../lib/jwt';
|
||||||
import AvailabilityCalendar from '../../components/AvailabilityCalendar';
|
import AvailabilityCalendar from '../../components/AvailabilityCalendar';
|
||||||
|
|
||||||
type ListingPageProps = {
|
type ListingPageProps = {
|
||||||
|
|
@ -42,8 +44,22 @@ export default async function ListingPage({ params }: ListingPageProps) {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const locale = resolveLocale({ cookieLocale: cookieStore.get('locale')?.value, acceptLanguage: headers().get('accept-language') });
|
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 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;
|
const translation = translationRaw ? withResolvedListingImages(translationRaw) : null;
|
||||||
|
|
||||||
if (!translation) {
|
if (!translation) {
|
||||||
|
|
@ -100,6 +116,8 @@ export default async function ListingPage({ params }: ListingPageProps) {
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' · ')
|
.join(' · ')
|
||||||
: t('priceNotSet');
|
: t('priceNotSet');
|
||||||
|
const isDraftOrPending = listing.status !== ListingStatus.PUBLISHED;
|
||||||
|
const isOwnerView = viewerId && listing.ownerId === viewerId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="listing-shell">
|
<main className="listing-shell">
|
||||||
|
|
@ -108,6 +126,11 @@ export default async function ListingPage({ params }: ListingPageProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className="listing-layout">
|
<div className="listing-layout">
|
||||||
<div className="panel listing-main">
|
<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 ? (
|
{isSample ? (
|
||||||
<div className="badge warning" style={{ marginBottom: 10, display: 'inline-block' }}>
|
<div className="badge warning" style={{ marginBottom: 10, display: 'inline-block' }}>
|
||||||
{t('sampleBadge')}
|
{t('sampleBadge')}
|
||||||
|
|
|
||||||
|
|
@ -387,41 +387,43 @@ export default function NewListingPage() {
|
||||||
? t('createListingSuccess', { id: data.listing.id, status: 'DRAFT' })
|
? t('createListingSuccess', { id: data.listing.id, status: 'DRAFT' })
|
||||||
: t('createListingSuccess', { id: data.listing.id, status: data.listing.status }),
|
: t('createListingSuccess', { id: data.listing.id, status: data.listing.status }),
|
||||||
);
|
);
|
||||||
setSlug('');
|
if (!saveDraft) {
|
||||||
setTranslations({
|
setSlug('');
|
||||||
en: { title: '', description: '', teaser: '' },
|
setTranslations({
|
||||||
fi: { title: '', description: '', teaser: '' },
|
en: { title: '', description: '', teaser: '' },
|
||||||
sv: { title: '', description: '', teaser: '' },
|
fi: { title: '', description: '', teaser: '' },
|
||||||
});
|
sv: { title: '', description: '', teaser: '' },
|
||||||
setMaxGuests(4);
|
});
|
||||||
setBedrooms(2);
|
setMaxGuests(4);
|
||||||
setBeds(3);
|
setBedrooms(2);
|
||||||
setBathrooms(1);
|
setBeds(3);
|
||||||
setPriceWeekday('');
|
setBathrooms(1);
|
||||||
setPriceWeekend('');
|
setPriceWeekday('');
|
||||||
setHasSauna(true);
|
setPriceWeekend('');
|
||||||
setHasFireplace(true);
|
setHasSauna(true);
|
||||||
setHasWifi(true);
|
setHasFireplace(true);
|
||||||
setPetsAllowed(false);
|
setHasWifi(true);
|
||||||
setByTheLake(false);
|
setPetsAllowed(false);
|
||||||
setHasAirConditioning(false);
|
setByTheLake(false);
|
||||||
setHasKitchen(true);
|
setHasAirConditioning(false);
|
||||||
setHasDishwasher(false);
|
setHasKitchen(true);
|
||||||
setHasWashingMachine(false);
|
setHasDishwasher(false);
|
||||||
setHasBarbecue(false);
|
setHasWashingMachine(false);
|
||||||
setHasMicrowave(false);
|
setHasBarbecue(false);
|
||||||
setHasFreeParking(false);
|
setHasMicrowave(false);
|
||||||
setRegion('');
|
setHasFreeParking(false);
|
||||||
setCity('');
|
setRegion('');
|
||||||
setStreetAddress('');
|
setCity('');
|
||||||
setAddressNote('');
|
setStreetAddress('');
|
||||||
setLatitude('');
|
setAddressNote('');
|
||||||
setLongitude('');
|
setLatitude('');
|
||||||
setContactName('');
|
setLongitude('');
|
||||||
setContactEmail('');
|
setContactName('');
|
||||||
setCalendarUrls('');
|
setContactEmail('');
|
||||||
setSelectedImages([]);
|
setCalendarUrls('');
|
||||||
setCoverImageIndex(1);
|
setSelectedImages([]);
|
||||||
|
setCoverImageIndex(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to create listing');
|
setError('Failed to create listing');
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export type ListingWithTranslations = Prisma.ListingTranslationGetPayload<{
|
||||||
type FetchOptions = {
|
type FetchOptions = {
|
||||||
slug: string;
|
slug: string;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
includeOwnerDraftsForUserId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveImageUrl(img: { id: string; url: string | null; size: number | null }) {
|
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.
|
* Fetch a listing translation by slug and locale.
|
||||||
* Falls back to any locale if the requested locale is missing.
|
* 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 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({
|
const translation = await prisma.listingTranslation.findFirst({
|
||||||
where: { slug, locale: targetLocale, listing: { status: ListingStatus.PUBLISHED, removedAt: null } },
|
where: { slug, locale: targetLocale, listing: listingWhere },
|
||||||
include: {
|
include: {
|
||||||
listing: {
|
listing: {
|
||||||
include: {
|
include: {
|
||||||
|
|
@ -50,7 +62,7 @@ export async function getListingBySlug({ slug, locale }: FetchOptions): Promise<
|
||||||
|
|
||||||
// Fallback: first translation for this slug
|
// Fallback: first translation for this slug
|
||||||
return prisma.listingTranslation.findFirst({
|
return prisma.listingTranslation.findFirst({
|
||||||
where: { slug, listing: { status: ListingStatus.PUBLISHED, removedAt: null } },
|
where: { slug, listing: listingWhere },
|
||||||
include: {
|
include: {
|
||||||
listing: {
|
listing: {
|
||||||
include: {
|
include: {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue