Add weekday/weekend pricing and new amenities
This commit is contained in:
parent
8bd0224597
commit
bee691ebd8
9 changed files with 211 additions and 46 deletions
|
|
@ -70,3 +70,4 @@
|
|||
- Site navbar now shows the new logo above the lomavuokraus.fi brand text on every page.
|
||||
- Language selector in the navbar aligned with other buttons and given higher-contrast styling.
|
||||
- Security hardening: npm audit now passes cleanly after upgrading Prisma patch release and pinning `glob@10.5.0` via overrides to eliminate the glob CLI injection advisory in eslint tooling.
|
||||
- Listings now capture separate weekday/weekend prices and new amenities (microwave, free parking) across schema, API, UI, and seeds.
|
||||
|
|
|
|||
|
|
@ -50,6 +50,13 @@ function normalizeCalendarUrls(input: unknown): string[] {
|
|||
return [];
|
||||
}
|
||||
|
||||
function parsePrice(value: unknown): number | null {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
const num = Number(value);
|
||||
if (Number.isNaN(num)) return null;
|
||||
return Math.round(num);
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const url = new URL(req.url);
|
||||
const searchParams = url.searchParams;
|
||||
|
|
@ -78,6 +85,8 @@ export async function GET(req: Request) {
|
|||
if (amenityFilters.includes('dishwasher')) amenityWhere.hasDishwasher = true;
|
||||
if (amenityFilters.includes('washer')) amenityWhere.hasWashingMachine = true;
|
||||
if (amenityFilters.includes('barbecue')) amenityWhere.hasBarbecue = true;
|
||||
if (amenityFilters.includes('microwave')) amenityWhere.hasMicrowave = true;
|
||||
if (amenityFilters.includes('parking')) amenityWhere.hasFreeParking = true;
|
||||
|
||||
const where: Prisma.ListingWhereInput = {
|
||||
status: ListingStatus.PUBLISHED,
|
||||
|
|
@ -158,12 +167,15 @@ export async function GET(req: Request) {
|
|||
hasDishwasher: listing.hasDishwasher,
|
||||
hasWashingMachine: listing.hasWashingMachine,
|
||||
hasBarbecue: listing.hasBarbecue,
|
||||
hasMicrowave: listing.hasMicrowave,
|
||||
hasFreeParking: listing.hasFreeParking,
|
||||
evCharging: listing.evCharging,
|
||||
maxGuests: listing.maxGuests,
|
||||
bedrooms: listing.bedrooms,
|
||||
beds: listing.beds,
|
||||
bathrooms: listing.bathrooms,
|
||||
priceHintPerNightEuros: listing.priceHintPerNightEuros,
|
||||
priceWeekdayEuros: listing.priceWeekdayEuros,
|
||||
priceWeekendEuros: listing.priceWeekendEuros,
|
||||
coverImage: resolveImageUrl(listing.images.find((img) => img.isCover) ?? listing.images[0] ?? { id: '', url: null, size: null }),
|
||||
isSample,
|
||||
hasCalendar: Boolean(listing.calendarUrls?.length),
|
||||
|
|
@ -199,7 +211,8 @@ export async function POST(req: Request) {
|
|||
const bedrooms = Number(body.bedrooms ?? 1);
|
||||
const beds = Number(body.beds ?? 1);
|
||||
const bathrooms = Number(body.bathrooms ?? 1);
|
||||
const priceHintPerNightEuros = body.priceHintPerNightEuros !== undefined && body.priceHintPerNightEuros !== null && body.priceHintPerNightEuros !== '' ? Math.round(Number(body.priceHintPerNightEuros)) : null;
|
||||
const priceWeekdayEuros = parsePrice(body.priceWeekdayEuros);
|
||||
const priceWeekendEuros = parsePrice(body.priceWeekendEuros);
|
||||
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 };
|
||||
|
|
@ -323,8 +336,11 @@ export async function POST(req: Request) {
|
|||
hasDishwasher: Boolean(body.hasDishwasher),
|
||||
hasWashingMachine: Boolean(body.hasWashingMachine),
|
||||
hasBarbecue: Boolean(body.hasBarbecue),
|
||||
hasMicrowave: Boolean(body.hasMicrowave),
|
||||
hasFreeParking: Boolean(body.hasFreeParking),
|
||||
evCharging: normalizeEvCharging(body.evCharging),
|
||||
priceHintPerNightEuros,
|
||||
priceWeekdayEuros,
|
||||
priceWeekendEuros,
|
||||
calendarUrls,
|
||||
contactName,
|
||||
contactEmail,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ const amenityIcons: Record<string, string> = {
|
|||
dishwasher: '🧼',
|
||||
washer: '🧺',
|
||||
barbecue: '🍖',
|
||||
microwave: '🍲',
|
||||
parking: '🅿️',
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: ListingPageProps): Promise<Metadata> {
|
||||
|
|
@ -70,11 +72,19 @@ export default async function ListingPage({ params }: ListingPageProps) {
|
|||
listing.hasDishwasher ? { icon: amenityIcons.dishwasher, label: t('amenityDishwasher') } : null,
|
||||
listing.hasWashingMachine ? { icon: amenityIcons.washer, label: t('amenityWashingMachine') } : null,
|
||||
listing.hasBarbecue ? { icon: amenityIcons.barbecue, label: t('amenityBarbecue') } : null,
|
||||
listing.hasMicrowave ? { icon: amenityIcons.microwave, label: t('amenityMicrowave') } : null,
|
||||
listing.hasFreeParking ? { icon: amenityIcons.parking, label: t('amenityFreeParking') } : null,
|
||||
].filter(Boolean) as { icon: string; label: string }[];
|
||||
const addressLine = `${listing.streetAddress ? `${listing.streetAddress}, ` : ''}${listing.city}, ${listing.region}, ${listing.country}`;
|
||||
const capacityLine = `${t('capacityGuests', { count: listing.maxGuests })} · ${t('capacityBedrooms', { count: listing.bedrooms })} · ${t('capacityBeds', { count: listing.beds })} · ${t('capacityBathrooms', { count: listing.bathrooms })}`;
|
||||
const contactLine = `${listing.contactName} · ${listing.contactEmail}${listing.contactPhone ? ` · ${listing.contactPhone}` : ''}`;
|
||||
const coverImage = listing.images.find((img) => img.isCover) ?? listing.images[0] ?? null;
|
||||
const priceLine =
|
||||
listing.priceWeekdayEuros || listing.priceWeekendEuros
|
||||
? [listing.priceWeekdayEuros ? t('priceWeekdayShort', { price: listing.priceWeekdayEuros }) : null, listing.priceWeekendEuros ? t('priceWeekendShort', { price: listing.priceWeekendEuros }) : null]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
: t('priceNotSet');
|
||||
|
||||
return (
|
||||
<main className="listing-shell">
|
||||
|
|
@ -172,6 +182,13 @@ export default async function ListingPage({ params }: ListingPageProps) {
|
|||
<div>{capacityLine}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fact-row">
|
||||
<span aria-hidden className="amenity-icon">💶</span>
|
||||
<div>
|
||||
<div style={{ color: '#cbd5e1', fontSize: 12 }}>{t('listingPrices')}</div>
|
||||
<div>{priceLine}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fact-row">
|
||||
<span aria-hidden className="amenity-icon">✉️</span>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ export default function NewListingPage() {
|
|||
const [bedrooms, setBedrooms] = useState(2);
|
||||
const [beds, setBeds] = useState(3);
|
||||
const [bathrooms, setBathrooms] = useState(1);
|
||||
const [price, setPrice] = useState<number | ''>('');
|
||||
const [priceWeekday, setPriceWeekday] = useState<number | ''>('');
|
||||
const [priceWeekend, setPriceWeekend] = useState<number | ''>('');
|
||||
const [hasSauna, setHasSauna] = useState(true);
|
||||
const [hasFireplace, setHasFireplace] = useState(true);
|
||||
const [hasWifi, setHasWifi] = useState(true);
|
||||
|
|
@ -50,6 +51,8 @@ export default function NewListingPage() {
|
|||
const [hasDishwasher, setHasDishwasher] = useState(false);
|
||||
const [hasWashingMachine, setHasWashingMachine] = useState(false);
|
||||
const [hasBarbecue, setHasBarbecue] = useState(false);
|
||||
const [hasMicrowave, setHasMicrowave] = useState(false);
|
||||
const [hasFreeParking, setHasFreeParking] = useState(false);
|
||||
const [evCharging, setEvCharging] = useState<'NONE' | 'FREE' | 'PAID'>('NONE');
|
||||
const [calendarUrls, setCalendarUrls] = useState('');
|
||||
const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]);
|
||||
|
|
@ -129,6 +132,8 @@ export default function NewListingPage() {
|
|||
{ key: 'dishwasher', label: t('amenityDishwasher'), icon: '🧼', checked: hasDishwasher, toggle: setHasDishwasher },
|
||||
{ key: 'washer', label: t('amenityWashingMachine'), icon: '🧺', checked: hasWashingMachine, toggle: setHasWashingMachine },
|
||||
{ key: 'barbecue', label: t('amenityBarbecue'), icon: '🍖', checked: hasBarbecue, toggle: setHasBarbecue },
|
||||
{ key: 'microwave', label: t('amenityMicrowave'), icon: '🍲', checked: hasMicrowave, toggle: setHasMicrowave },
|
||||
{ key: 'parking', label: t('amenityFreeParking'), icon: '🅿️', checked: hasFreeParking, toggle: setHasFreeParking },
|
||||
];
|
||||
|
||||
function updateTranslation(locale: Locale, field: keyof LocaleFields, value: string) {
|
||||
|
|
@ -342,7 +347,8 @@ export default function NewListingPage() {
|
|||
bedrooms,
|
||||
beds,
|
||||
bathrooms,
|
||||
priceHintPerNightEuros: price === '' ? null : Math.round(Number(price)),
|
||||
priceWeekdayEuros: priceWeekday === '' ? null : Math.round(Number(priceWeekday)),
|
||||
priceWeekendEuros: priceWeekend === '' ? null : Math.round(Number(priceWeekend)),
|
||||
hasSauna,
|
||||
hasFireplace,
|
||||
hasWifi,
|
||||
|
|
@ -353,6 +359,8 @@ export default function NewListingPage() {
|
|||
hasDishwasher,
|
||||
hasWashingMachine,
|
||||
hasBarbecue,
|
||||
hasMicrowave,
|
||||
hasFreeParking,
|
||||
evCharging,
|
||||
coverImageIndex,
|
||||
images: parseImages(),
|
||||
|
|
@ -370,6 +378,24 @@ export default function NewListingPage() {
|
|||
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('');
|
||||
|
|
@ -594,18 +620,32 @@ export default function NewListingPage() {
|
|||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
|
||||
<label>
|
||||
{t('priceHintLabel')}
|
||||
{t('priceWeekdayLabel')}
|
||||
<input
|
||||
type="number"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value === '' ? '' : Number(e.target.value))}
|
||||
value={priceWeekday}
|
||||
onChange={(e) => setPriceWeekday(e.target.value === '' ? '' : Number(e.target.value))}
|
||||
min={0}
|
||||
step="10"
|
||||
placeholder="e.g. 120"
|
||||
placeholder="120"
|
||||
/>
|
||||
<div style={{ color: '#cbd5e1', fontSize: 12 }}>{t('priceHintHelp')}</div>
|
||||
</label>
|
||||
<label>
|
||||
{t('priceWeekendLabel')}
|
||||
<input
|
||||
type="number"
|
||||
value={priceWeekend}
|
||||
onChange={(e) => setPriceWeekend(e.target.value === '' ? '' : Number(e.target.value))}
|
||||
min={0}
|
||||
step="10"
|
||||
placeholder="140"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ color: '#cbd5e1', fontSize: 12 }}>{t('priceHintHelp')}</div>
|
||||
<label style={{ gridColumn: '1 / -1' }}>
|
||||
{t('calendarUrlsLabel')}
|
||||
<textarea
|
||||
|
|
@ -616,7 +656,6 @@ export default function NewListingPage() {
|
|||
/>
|
||||
<div style={{ color: '#cbd5e1', fontSize: 12 }}>{t('calendarUrlsHelp')}</div>
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 8, gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))' }}>
|
||||
<label>
|
||||
{t('latitudeLabel')}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,15 @@ type ListingResult = {
|
|||
hasDishwasher: boolean;
|
||||
hasWashingMachine: boolean;
|
||||
hasBarbecue: boolean;
|
||||
hasMicrowave: boolean;
|
||||
hasFreeParking: boolean;
|
||||
evCharging: 'NONE' | 'FREE' | 'PAID';
|
||||
maxGuests: number;
|
||||
bedrooms: number;
|
||||
beds: number;
|
||||
bathrooms: number;
|
||||
priceHintPerNightEuros: number | null;
|
||||
priceWeekdayEuros: number | null;
|
||||
priceWeekendEuros: number | null;
|
||||
coverImage: string | null;
|
||||
isSample: boolean;
|
||||
hasCalendar: boolean;
|
||||
|
|
@ -84,6 +87,8 @@ const amenityIcons: Record<string, string> = {
|
|||
dishwasher: '🧼',
|
||||
washer: '🧺',
|
||||
barbecue: '🍖',
|
||||
microwave: '🍲',
|
||||
parking: '🅿️',
|
||||
};
|
||||
|
||||
function ListingsMap({
|
||||
|
|
@ -216,6 +221,8 @@ export default function ListingsIndexPage() {
|
|||
{ key: 'dishwasher', label: t('amenityDishwasher'), icon: amenityIcons.dishwasher },
|
||||
{ key: 'washer', label: t('amenityWashingMachine'), icon: amenityIcons.washer },
|
||||
{ key: 'barbecue', label: t('amenityBarbecue'), icon: amenityIcons.barbecue },
|
||||
{ key: 'microwave', label: t('amenityMicrowave'), icon: amenityIcons.microwave },
|
||||
{ key: 'parking', label: t('amenityFreeParking'), icon: amenityIcons.parking },
|
||||
];
|
||||
|
||||
async function fetchListings() {
|
||||
|
|
@ -467,6 +474,8 @@ export default function ListingsIndexPage() {
|
|||
{l.city}, {l.region}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', fontSize: 13 }}>
|
||||
{l.priceWeekdayEuros ? <span className="badge">{t('priceWeekdayShort', { price: l.priceWeekdayEuros })}</span> : null}
|
||||
{l.priceWeekendEuros ? <span className="badge">{t('priceWeekendShort', { price: l.priceWeekendEuros })}</span> : null}
|
||||
<span className="badge">{t('capacityGuests', { count: l.maxGuests })}</span>
|
||||
<span className="badge">{t('capacityBedrooms', { count: l.bedrooms })}</span>
|
||||
{l.hasCalendar ? <span className="badge secondary">{t('calendarConnected')}</span> : null}
|
||||
|
|
@ -480,6 +489,8 @@ export default function ListingsIndexPage() {
|
|||
{l.hasDishwasher ? <span className="badge">{t('amenityDishwasher')}</span> : null}
|
||||
{l.hasWashingMachine ? <span className="badge">{t('amenityWashingMachine')}</span> : null}
|
||||
{l.hasBarbecue ? <span className="badge">{t('amenityBarbecue')}</span> : null}
|
||||
{l.hasMicrowave ? <span className="badge">{t('amenityMicrowave')}</span> : null}
|
||||
{l.hasFreeParking ? <span className="badge">{t('amenityFreeParking')}</span> : null}
|
||||
{l.hasSauna ? <span className="badge">{t('amenitySauna')}</span> : null}
|
||||
{l.hasWifi ? <span className="badge">{t('amenityWifi')}</span> : null}
|
||||
</div>
|
||||
|
|
|
|||
31
lib/i18n.ts
31
lib/i18n.ts
|
|
@ -125,6 +125,7 @@ const baseMessages = {
|
|||
listingLocation: 'Location',
|
||||
listingAddress: 'Address',
|
||||
listingCapacity: 'Capacity',
|
||||
listingPrices: 'Pricing',
|
||||
listingAmenities: 'Amenities',
|
||||
listingNoAmenities: 'No amenities listed yet.',
|
||||
listingContact: 'Contact',
|
||||
|
|
@ -186,8 +187,12 @@ const baseMessages = {
|
|||
bedroomsLabel: 'Bedrooms',
|
||||
bedsLabel: 'Beds',
|
||||
bathroomsLabel: 'Bathrooms',
|
||||
priceHintLabel: 'Price ballpark (€ / night)',
|
||||
priceHintHelp: 'Rough nightly price in euros (not a binding offer).',
|
||||
priceWeekdayLabel: 'Weeknight price (€ / night)',
|
||||
priceWeekendLabel: 'Weekend price (€ / night)',
|
||||
priceHintHelp: 'Set separate weeknight and weekend prices in euros (optional, not a binding offer).',
|
||||
priceWeekdayShort: '{price}€ weekday',
|
||||
priceWeekendShort: '{price}€ weekend',
|
||||
priceNotSet: 'Not provided',
|
||||
calendarUrlsLabel: 'Availability calendars (iCal URLs, one per line)',
|
||||
calendarUrlsHelp: 'Paste iCal links from other platforms. We will merge them to show availability.',
|
||||
imagesLabel: 'Images',
|
||||
|
|
@ -214,6 +219,8 @@ const baseMessages = {
|
|||
amenityDishwasher: 'Dishwasher',
|
||||
amenityWashingMachine: 'Washing machine',
|
||||
amenityBarbecue: 'Barbecue grill',
|
||||
amenityMicrowave: 'Microwave',
|
||||
amenityFreeParking: 'Free parking',
|
||||
amenityEvFree: 'EV charging (free)',
|
||||
amenityEvPaid: 'EV charging (paid)',
|
||||
evChargingLabel: 'EV charging',
|
||||
|
|
@ -428,6 +435,7 @@ const baseMessages = {
|
|||
listingLocation: 'Sijainti',
|
||||
listingAddress: 'Osoite',
|
||||
listingCapacity: 'Tilat',
|
||||
listingPrices: 'Hinta',
|
||||
listingAmenities: 'Varustelu',
|
||||
listingNoAmenities: 'Varustelua ei ole listattu.',
|
||||
listingContact: 'Yhteystiedot',
|
||||
|
|
@ -464,8 +472,12 @@ const baseMessages = {
|
|||
bedroomsLabel: 'Makuuhuoneita',
|
||||
bedsLabel: 'Vuoteita',
|
||||
bathroomsLabel: 'Kylpyhuoneita',
|
||||
priceHintLabel: 'Hinta-arvio (€ / yö)',
|
||||
priceHintHelp: 'Suuntaa-antava hinta euroina per yö (ei sitova).',
|
||||
priceWeekdayLabel: 'Arkiyön hinta (€ / yö)',
|
||||
priceWeekendLabel: 'Viikonlopun hinta (€ / yö)',
|
||||
priceHintHelp: 'Aseta erilliset hinnat arki- ja viikonloppuyöille euroissa (valinnainen, ei sitova).',
|
||||
priceWeekdayShort: '{price}€ arki',
|
||||
priceWeekendShort: '{price}€ viikonloppu',
|
||||
priceNotSet: 'Ei ilmoitettu',
|
||||
calendarUrlsLabel: 'Saatavuuskalenterit (iCal-osoitteet, yksi per rivi)',
|
||||
calendarUrlsHelp: 'Liitä iCal-linkit muilta alustoilta. Yhdistämme ne saatavuuden näyttämiseen.',
|
||||
imagesLabel: 'Kuvat',
|
||||
|
|
@ -492,6 +504,8 @@ const baseMessages = {
|
|||
amenityDishwasher: 'Astianpesukone',
|
||||
amenityWashingMachine: 'Pyykinpesukone',
|
||||
amenityBarbecue: 'Grilli',
|
||||
amenityMicrowave: 'Mikroaaltouuni',
|
||||
amenityFreeParking: 'Maksuton pysäköinti',
|
||||
amenityEvFree: 'Sähköauton lataus (ilmainen)',
|
||||
amenityEvPaid: 'Sähköauton lataus (maksullinen)',
|
||||
evChargingLabel: 'Sähköauton lataus',
|
||||
|
|
@ -606,6 +620,15 @@ const svMessages: Record<keyof typeof baseMessages.en, string> = {
|
|||
slugTaken: 'Sluggen används redan',
|
||||
slugCheckError: 'Kunde inte kontrollera sluggen nu',
|
||||
teaserHelp: 'Kort ingress som syns i korten',
|
||||
priceWeekdayLabel: 'Vardagspris (€ / natt)',
|
||||
priceWeekendLabel: 'Helgpris (€ / natt)',
|
||||
priceHintHelp: 'Ange separata priser för vardag och helg i euro per natt (frivilligt).',
|
||||
priceWeekdayShort: '{price}€ vardag',
|
||||
priceWeekendShort: '{price}€ helg',
|
||||
priceNotSet: 'Ej angivet',
|
||||
listingPrices: 'Priser',
|
||||
amenityMicrowave: 'Mikrovågsugn',
|
||||
amenityFreeParking: 'Gratis parkering',
|
||||
};
|
||||
|
||||
export const messages = { ...baseMessages, sv: svMessages } as const;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
-- Split single price hint into weekday/weekend prices and add new amenities
|
||||
ALTER TABLE "Listing"
|
||||
ADD COLUMN "priceWeekdayEuros" INTEGER,
|
||||
ADD COLUMN "priceWeekendEuros" INTEGER,
|
||||
ADD COLUMN "hasMicrowave" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "hasFreeParking" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
UPDATE "Listing"
|
||||
SET
|
||||
"priceWeekdayEuros" = "priceHintPerNightEuros",
|
||||
"priceWeekendEuros" = "priceHintPerNightEuros"
|
||||
WHERE "priceWeekdayEuros" IS NULL
|
||||
AND "priceWeekendEuros" IS NULL;
|
||||
|
||||
ALTER TABLE "Listing" DROP COLUMN "priceHintPerNightEuros";
|
||||
|
|
@ -92,9 +92,12 @@ model Listing {
|
|||
hasDishwasher Boolean @default(false)
|
||||
hasWashingMachine Boolean @default(false)
|
||||
hasBarbecue Boolean @default(false)
|
||||
hasMicrowave Boolean @default(false)
|
||||
hasFreeParking Boolean @default(false)
|
||||
evCharging EvCharging @default(NONE)
|
||||
calendarUrls String[] @db.Text @default([])
|
||||
priceHintPerNightEuros Int?
|
||||
priceWeekdayEuros Int?
|
||||
priceWeekendEuros Int?
|
||||
contactName String
|
||||
contactEmail String
|
||||
contactPhone String?
|
||||
|
|
|
|||
|
|
@ -157,10 +157,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: true,
|
||||
hasFreeParking: true,
|
||||
petsAllowed: false,
|
||||
byTheLake: true,
|
||||
evCharging: 'FREE',
|
||||
priceHintPerNightEuros: 145,
|
||||
priceWeekdayEuros: 145,
|
||||
priceWeekendEuros: 165,
|
||||
cover: {
|
||||
file: 'saimaa-lakeside-cabin-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -201,10 +204,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: true,
|
||||
hasFreeParking: false,
|
||||
petsAllowed: false,
|
||||
byTheLake: false,
|
||||
evCharging: 'PAID',
|
||||
priceHintPerNightEuros: 165,
|
||||
priceWeekdayEuros: 165,
|
||||
priceWeekendEuros: 185,
|
||||
cover: {
|
||||
file: 'helsinki-design-loft-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1505693415763-3bd1620f58c3?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -243,10 +249,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: true,
|
||||
hasFreeParking: true,
|
||||
petsAllowed: true,
|
||||
byTheLake: false,
|
||||
evCharging: 'NONE',
|
||||
priceHintPerNightEuros: 110,
|
||||
priceWeekdayEuros: 110,
|
||||
priceWeekendEuros: 125,
|
||||
cover: {
|
||||
file: 'turku-riverside-apartment-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -284,10 +293,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: false,
|
||||
hasFreeParking: true,
|
||||
petsAllowed: false,
|
||||
byTheLake: true,
|
||||
evCharging: 'FREE',
|
||||
priceHintPerNightEuros: 189,
|
||||
priceWeekdayEuros: 189,
|
||||
priceWeekendEuros: 215,
|
||||
cover: {
|
||||
file: 'rovaniemi-aurora-cabin-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -325,10 +337,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: true,
|
||||
hasFreeParking: false,
|
||||
petsAllowed: false,
|
||||
byTheLake: false,
|
||||
evCharging: 'NONE',
|
||||
priceHintPerNightEuros: 95,
|
||||
priceWeekdayEuros: 95,
|
||||
priceWeekendEuros: 110,
|
||||
cover: {
|
||||
file: 'tampere-sauna-studio-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -364,10 +379,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: true,
|
||||
hasFreeParking: true,
|
||||
petsAllowed: true,
|
||||
byTheLake: true,
|
||||
evCharging: 'PAID',
|
||||
priceHintPerNightEuros: 245,
|
||||
priceWeekdayEuros: 245,
|
||||
priceWeekendEuros: 275,
|
||||
cover: {
|
||||
file: 'vaasa-seaside-villa-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -403,10 +421,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: true,
|
||||
hasFreeParking: true,
|
||||
petsAllowed: false,
|
||||
byTheLake: true,
|
||||
evCharging: 'FREE',
|
||||
priceHintPerNightEuros: 129,
|
||||
priceWeekdayEuros: 129,
|
||||
priceWeekendEuros: 149,
|
||||
cover: {
|
||||
file: 'kuopio-lakeside-apartment-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1470246973918-29a93221c455?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -442,10 +463,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: true,
|
||||
hasFreeParking: false,
|
||||
petsAllowed: false,
|
||||
byTheLake: false,
|
||||
evCharging: 'NONE',
|
||||
priceHintPerNightEuros: 99,
|
||||
priceWeekdayEuros: 99,
|
||||
priceWeekendEuros: 115,
|
||||
cover: {
|
||||
file: 'porvoo-river-loft-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -481,10 +505,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: true,
|
||||
hasFreeParking: true,
|
||||
petsAllowed: false,
|
||||
byTheLake: false,
|
||||
evCharging: 'FREE',
|
||||
priceHintPerNightEuros: 105,
|
||||
priceWeekdayEuros: 105,
|
||||
priceWeekendEuros: 120,
|
||||
cover: {
|
||||
file: 'oulu-tech-apartment-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -520,10 +547,13 @@ async function main() {
|
|||
hasDishwasher: false,
|
||||
hasWashingMachine: false,
|
||||
hasBarbecue: false,
|
||||
hasMicrowave: true,
|
||||
hasFreeParking: true,
|
||||
petsAllowed: false,
|
||||
byTheLake: true,
|
||||
evCharging: 'PAID',
|
||||
priceHintPerNightEuros: 115,
|
||||
priceWeekdayEuros: 115,
|
||||
priceWeekendEuros: 130,
|
||||
cover: {
|
||||
file: 'mariehamn-harbor-flat-cover.jpg',
|
||||
url: 'https://images.unsplash.com/photo-1470246973918-29a93221c455?auto=format&fit=crop&w=1600&q=80',
|
||||
|
|
@ -541,16 +571,20 @@ async function main() {
|
|||
|
||||
// Fill in any missing amenities/prices with reasonable defaults
|
||||
const randBool = (p = 0.5) => Math.random() < p;
|
||||
listings = listings.map((item) => ({
|
||||
listings = listings.map((item) => {
|
||||
const weekdayPrice = item.priceWeekdayEuros ?? Math.round(Math.random() * (220 - 90) + 90);
|
||||
return {
|
||||
...item,
|
||||
priceHintPerNightEuros:
|
||||
item.priceHintPerNightEuros ??
|
||||
Math.round(Math.random() * (220 - 90) + 90),
|
||||
priceWeekdayEuros: weekdayPrice,
|
||||
priceWeekendEuros: item.priceWeekendEuros ?? (weekdayPrice ? weekdayPrice + 15 : null),
|
||||
hasKitchen: item.hasKitchen ?? randBool(0.9),
|
||||
hasDishwasher: item.hasDishwasher ?? randBool(0.6),
|
||||
hasWashingMachine: item.hasWashingMachine ?? randBool(0.6),
|
||||
hasBarbecue: item.hasBarbecue ?? randBool(0.5),
|
||||
}));
|
||||
hasMicrowave: item.hasMicrowave ?? randBool(0.7),
|
||||
hasFreeParking: item.hasFreeParking ?? randBool(0.6),
|
||||
};
|
||||
});
|
||||
|
||||
for (const item of listings) {
|
||||
const existing = await prisma.listingTranslation.findFirst({ where: { slug: item.slug }, select: { listingId: true } });
|
||||
|
|
@ -582,10 +616,13 @@ async function main() {
|
|||
hasDishwasher: item.hasDishwasher ?? false,
|
||||
hasWashingMachine: item.hasWashingMachine ?? false,
|
||||
hasBarbecue: item.hasBarbecue ?? false,
|
||||
hasMicrowave: item.hasMicrowave ?? false,
|
||||
hasFreeParking: item.hasFreeParking ?? false,
|
||||
petsAllowed: item.petsAllowed,
|
||||
byTheLake: item.byTheLake,
|
||||
evCharging: item.evCharging,
|
||||
priceHintPerNightEuros: item.priceHintPerNightEuros,
|
||||
priceWeekdayEuros: item.priceWeekdayEuros,
|
||||
priceWeekendEuros: item.priceWeekendEuros,
|
||||
contactName: 'Sample Host',
|
||||
contactEmail: SAMPLE_EMAIL,
|
||||
contactPhone: owner.phone,
|
||||
|
|
@ -629,10 +666,13 @@ async function main() {
|
|||
hasDishwasher: item.hasDishwasher ?? false,
|
||||
hasWashingMachine: item.hasWashingMachine ?? false,
|
||||
hasBarbecue: item.hasBarbecue ?? false,
|
||||
hasMicrowave: item.hasMicrowave ?? false,
|
||||
hasFreeParking: item.hasFreeParking ?? false,
|
||||
petsAllowed: item.petsAllowed,
|
||||
byTheLake: item.byTheLake,
|
||||
evCharging: item.evCharging,
|
||||
priceHintPerNightEuros: item.priceHintPerNightEuros,
|
||||
priceWeekdayEuros: item.priceWeekdayEuros,
|
||||
priceWeekendEuros: item.priceWeekendEuros,
|
||||
contactName: 'Sample Host',
|
||||
contactEmail: SAMPLE_EMAIL,
|
||||
contactPhone: owner.phone,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue