Add ski pass amenity and simplify EV charging availability

This commit is contained in:
Tero Halla-aho 2025-12-06 23:17:56 +02:00
parent 6cc5efc9e5
commit 27fc8ee2d1
9 changed files with 108 additions and 66 deletions

View file

@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { ListingStatus, UserStatus, EvCharging, Prisma } from '@prisma/client'; import { ListingStatus, UserStatus, Prisma } from '@prisma/client';
import { prisma } from '../../../lib/prisma'; import { prisma } from '../../../lib/prisma';
import { requireAuth } from '../../../lib/jwt'; import { requireAuth } from '../../../lib/jwt';
import { resolveLocale } from '../../../lib/i18n'; import { resolveLocale } from '../../../lib/i18n';
@ -10,13 +10,6 @@ const MAX_IMAGES = 6;
const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB per image const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB per image
const SAMPLE_EMAIL = 'host@lomavuokraus.fi'; const SAMPLE_EMAIL = 'host@lomavuokraus.fi';
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 resolveImageUrl(img: { id: string; url: string | null; size: number | null }) { function resolveImageUrl(img: { id: string; url: string | null; size: number | null }) {
if (img.size && img.size > 0) { if (img.size && img.size > 0) {
return `/api/images/${img.id}`; return `/api/images/${img.id}`;
@ -64,7 +57,7 @@ export async function GET(req: Request) {
const city = searchParams.get('city')?.trim(); const city = searchParams.get('city')?.trim();
const region = searchParams.get('region')?.trim(); const region = searchParams.get('region')?.trim();
const evChargingParam = searchParams.get('evCharging'); const evChargingParam = searchParams.get('evCharging');
const evCharging = evChargingParam ? normalizeEvCharging(evChargingParam) : null; const evCharging = evChargingParam === 'true' ? true : evChargingParam === 'false' ? false : null;
const startDateParam = searchParams.get('availableStart'); const startDateParam = searchParams.get('availableStart');
const endDateParam = searchParams.get('availableEnd'); const endDateParam = searchParams.get('availableEnd');
const startDate = startDateParam ? new Date(startDateParam) : null; const startDate = startDateParam ? new Date(startDateParam) : null;
@ -87,13 +80,14 @@ export async function GET(req: Request) {
if (amenityFilters.includes('barbecue')) amenityWhere.hasBarbecue = true; if (amenityFilters.includes('barbecue')) amenityWhere.hasBarbecue = true;
if (amenityFilters.includes('microwave')) amenityWhere.hasMicrowave = true; if (amenityFilters.includes('microwave')) amenityWhere.hasMicrowave = true;
if (amenityFilters.includes('parking')) amenityWhere.hasFreeParking = true; if (amenityFilters.includes('parking')) amenityWhere.hasFreeParking = true;
if (amenityFilters.includes('skipass')) amenityWhere.hasSkiPass = true;
const where: Prisma.ListingWhereInput = { const where: Prisma.ListingWhereInput = {
status: ListingStatus.PUBLISHED, status: ListingStatus.PUBLISHED,
removedAt: null, removedAt: null,
city: city ? { contains: city, mode: 'insensitive' } : undefined, city: city ? { contains: city, mode: 'insensitive' } : undefined,
region: region ? { contains: region, mode: 'insensitive' } : undefined, region: region ? { contains: region, mode: 'insensitive' } : undefined,
evCharging: evCharging ?? undefined, evChargingAvailable: evCharging ?? undefined,
...amenityWhere, ...amenityWhere,
translations: q translations: q
? { ? {
@ -327,6 +321,8 @@ export async function POST(req: Request) {
const autoApprove = !saveDraft && (process.env.AUTO_APPROVE_LISTINGS === 'true' || auth.role === 'ADMIN'); const autoApprove = !saveDraft && (process.env.AUTO_APPROVE_LISTINGS === 'true' || auth.role === 'ADMIN');
const status = saveDraft ? ListingStatus.DRAFT : autoApprove ? ListingStatus.PUBLISHED : ListingStatus.PENDING; const status = saveDraft ? ListingStatus.DRAFT : autoApprove ? ListingStatus.PUBLISHED : ListingStatus.PENDING;
const isSample = (contactEmail || '').toLowerCase() === SAMPLE_EMAIL; const isSample = (contactEmail || '').toLowerCase() === SAMPLE_EMAIL;
const evChargingAvailable =
body.evChargingAvailable === undefined || body.evChargingAvailable === null ? null : Boolean(body.evChargingAvailable);
const listing = await prisma.listing.create({ const listing = await prisma.listing.create({
data: { data: {
@ -357,7 +353,8 @@ export async function POST(req: Request) {
hasBarbecue: Boolean(body.hasBarbecue), hasBarbecue: Boolean(body.hasBarbecue),
hasMicrowave: Boolean(body.hasMicrowave), hasMicrowave: Boolean(body.hasMicrowave),
hasFreeParking: Boolean(body.hasFreeParking), hasFreeParking: Boolean(body.hasFreeParking),
evCharging: normalizeEvCharging(body.evCharging), hasSkiPass: Boolean(body.hasSkiPass),
evChargingAvailable,
priceWeekdayEuros, priceWeekdayEuros,
priceWeekendEuros, priceWeekendEuros,
calendarUrls, calendarUrls,

View file

@ -26,6 +26,7 @@ const amenityIcons: Record<string, string> = {
barbecue: '🍖', barbecue: '🍖',
microwave: '🍲', microwave: '🍲',
parking: '🅿️', parking: '🅿️',
ski: '⛷️',
}; };
export async function generateMetadata({ params }: ListingPageProps): Promise<Metadata> { export async function generateMetadata({ params }: ListingPageProps): Promise<Metadata> {
@ -66,8 +67,8 @@ export default async function ListingPage({ params }: ListingPageProps) {
listing.petsAllowed ? { icon: amenityIcons.pets, label: t('amenityPets') } : null, listing.petsAllowed ? { icon: amenityIcons.pets, label: t('amenityPets') } : null,
listing.byTheLake ? { icon: amenityIcons.lake, label: t('amenityLake') } : null, listing.byTheLake ? { icon: amenityIcons.lake, label: t('amenityLake') } : null,
listing.hasAirConditioning ? { icon: amenityIcons.ac, label: t('amenityAirConditioning') } : null, listing.hasAirConditioning ? { icon: amenityIcons.ac, label: t('amenityAirConditioning') } : null,
listing.evCharging === 'FREE' ? { icon: amenityIcons.ev, label: t('amenityEvFree') } : null, listing.evChargingAvailable ? { icon: amenityIcons.ev, label: t('amenityEvNearby') } : null,
listing.evCharging === 'PAID' ? { icon: amenityIcons.ev, label: t('amenityEvPaid') } : null, listing.hasSkiPass ? { icon: amenityIcons.ski, label: t('amenitySkiPass') } : null,
listing.hasKitchen ? { icon: amenityIcons.kitchen, label: t('amenityKitchen') } : null, listing.hasKitchen ? { icon: amenityIcons.kitchen, label: t('amenityKitchen') } : null,
listing.hasDishwasher ? { icon: amenityIcons.dishwasher, label: t('amenityDishwasher') } : null, listing.hasDishwasher ? { icon: amenityIcons.dishwasher, label: t('amenityDishwasher') } : null,
listing.hasWashingMachine ? { icon: amenityIcons.washer, label: t('amenityWashingMachine') } : null, listing.hasWashingMachine ? { icon: amenityIcons.washer, label: t('amenityWashingMachine') } : null,

View file

@ -53,7 +53,7 @@ export default function NewListingPage() {
const [hasBarbecue, setHasBarbecue] = useState(false); const [hasBarbecue, setHasBarbecue] = useState(false);
const [hasMicrowave, setHasMicrowave] = useState(false); const [hasMicrowave, setHasMicrowave] = useState(false);
const [hasFreeParking, setHasFreeParking] = useState(false); const [hasFreeParking, setHasFreeParking] = useState(false);
const [evCharging, setEvCharging] = useState<'NONE' | 'FREE' | 'PAID'>('NONE'); const [evChargingAvailable, setEvChargingAvailable] = useState<boolean>(false);
const [calendarUrls, setCalendarUrls] = useState(''); const [calendarUrls, setCalendarUrls] = useState('');
const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]); const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]);
const [coverImageIndex, setCoverImageIndex] = useState(1); const [coverImageIndex, setCoverImageIndex] = useState(1);
@ -135,6 +135,7 @@ export default function NewListingPage() {
{ key: 'barbecue', label: t('amenityBarbecue'), icon: '🍖', checked: hasBarbecue, toggle: setHasBarbecue }, { key: 'barbecue', label: t('amenityBarbecue'), icon: '🍖', checked: hasBarbecue, toggle: setHasBarbecue },
{ key: 'microwave', label: t('amenityMicrowave'), icon: '🍲', checked: hasMicrowave, toggle: setHasMicrowave }, { key: 'microwave', label: t('amenityMicrowave'), icon: '🍲', checked: hasMicrowave, toggle: setHasMicrowave },
{ key: 'parking', label: t('amenityFreeParking'), icon: '🅿️', checked: hasFreeParking, toggle: setHasFreeParking }, { key: 'parking', label: t('amenityFreeParking'), icon: '🅿️', checked: hasFreeParking, toggle: setHasFreeParking },
{ key: 'ski', label: t('amenitySkiPass'), icon: '⛷️', checked: hasSkiPass, toggle: setHasSkiPass },
]; ];
function updateTranslation(locale: Locale, field: keyof LocaleFields, value: string) { function updateTranslation(locale: Locale, field: keyof LocaleFields, value: string) {
@ -369,7 +370,7 @@ export default function NewListingPage() {
hasBarbecue, hasBarbecue,
hasMicrowave, hasMicrowave,
hasFreeParking, hasFreeParking,
evCharging, evChargingAvailable,
coverImageIndex, coverImageIndex,
images: parseImages(), images: parseImages(),
calendarUrls, calendarUrls,
@ -704,17 +705,17 @@ export default function NewListingPage() {
))} ))}
<div className="amenity-ev"> <div className="amenity-ev">
<div className="amenity-ev-label">{t('evChargingLabel')}</div> <div className="amenity-ev-label">{t('evChargingLabel')}</div>
<div style={{ color: '#cbd5e1', fontSize: 12, marginBottom: 6 }}>{t('evChargingExplain')}</div>
<div className="ev-toggle-group"> <div className="ev-toggle-group">
{[ {[
{ value: 'NONE', label: t('evChargingNone'), icon: '🚗' }, { value: true, label: t('evChargingYes'), icon: '⚡' },
{ value: 'FREE', label: t('evChargingFree'), icon: '⚡' }, { value: false, label: t('evChargingNo'), icon: '🚗' },
{ value: 'PAID', label: t('evChargingPaid'), icon: '💳' },
].map((opt) => ( ].map((opt) => (
<button <button
key={opt.value} key={String(opt.value)}
type="button" type="button"
className={`ev-toggle ${evCharging === opt.value ? 'active' : ''}`} className={`ev-toggle ${evChargingAvailable === opt.value ? 'active' : ''}`}
onClick={() => setEvCharging(opt.value as typeof evCharging)} onClick={() => setEvChargingAvailable(opt.value)}
> >
<span aria-hidden className="amenity-emoji"> <span aria-hidden className="amenity-emoji">
{opt.icon} {opt.icon}

View file

@ -30,7 +30,7 @@ type ListingResult = {
hasBarbecue: boolean; hasBarbecue: boolean;
hasMicrowave: boolean; hasMicrowave: boolean;
hasFreeParking: boolean; hasFreeParking: boolean;
evCharging: 'NONE' | 'FREE' | 'PAID'; evChargingAvailable: boolean;
maxGuests: number; maxGuests: number;
bedrooms: number; bedrooms: number;
beds: number; beds: number;
@ -89,6 +89,7 @@ const amenityIcons: Record<string, string> = {
barbecue: '🍖', barbecue: '🍖',
microwave: '🍲', microwave: '🍲',
parking: '🅿️', parking: '🅿️',
ski: '⛷️',
}; };
function ListingsMap({ function ListingsMap({
@ -181,7 +182,7 @@ export default function ListingsIndexPage() {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [city, setCity] = useState(''); const [city, setCity] = useState('');
const [region, setRegion] = useState(''); const [region, setRegion] = useState('');
const [evCharging, setEvCharging] = useState<'ALL' | 'FREE' | 'PAID' | 'NONE'>('ALL'); const [evCharging, setEvCharging] = useState<'ALL' | 'YES' | 'NO'>('ALL');
const [listings, setListings] = useState<ListingResult[]>([]); const [listings, setListings] = useState<ListingResult[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -207,7 +208,7 @@ export default function ListingsIndexPage() {
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (evCharging === 'ALL') return filteredByAddress; if (evCharging === 'ALL') return filteredByAddress;
return filteredByAddress.filter((l) => l.evCharging === evCharging); return filteredByAddress.filter((l) => (evCharging === 'YES' ? l.evChargingAvailable : !l.evChargingAvailable));
}, [filteredByAddress, evCharging]); }, [filteredByAddress, evCharging]);
const amenityOptions = [ const amenityOptions = [
@ -223,6 +224,7 @@ export default function ListingsIndexPage() {
{ key: 'barbecue', label: t('amenityBarbecue'), icon: amenityIcons.barbecue }, { key: 'barbecue', label: t('amenityBarbecue'), icon: amenityIcons.barbecue },
{ key: 'microwave', label: t('amenityMicrowave'), icon: amenityIcons.microwave }, { key: 'microwave', label: t('amenityMicrowave'), icon: amenityIcons.microwave },
{ key: 'parking', label: t('amenityFreeParking'), icon: amenityIcons.parking }, { key: 'parking', label: t('amenityFreeParking'), icon: amenityIcons.parking },
{ key: 'skipass', label: t('amenitySkiPass'), icon: amenityIcons.ski },
]; ];
async function fetchListings() { async function fetchListings() {
@ -233,7 +235,7 @@ export default function ListingsIndexPage() {
if (query) params.set('q', query); if (query) params.set('q', query);
if (city) params.set('city', city); if (city) params.set('city', city);
if (region) params.set('region', region); if (region) params.set('region', region);
if (evCharging !== 'ALL') params.set('evCharging', evCharging); if (evCharging !== 'ALL') params.set('evCharging', evCharging === 'YES' ? 'true' : 'false');
if (startDate) params.set('availableStart', startDate); if (startDate) params.set('availableStart', startDate);
if (endDate) params.set('availableEnd', endDate); if (endDate) params.set('availableEnd', endDate);
amenities.forEach((a) => params.append('amenity', a)); amenities.forEach((a) => params.append('amenity', a));
@ -322,9 +324,8 @@ export default function ListingsIndexPage() {
{t('evChargingLabel')} {t('evChargingLabel')}
<select value={evCharging} onChange={(e) => setEvCharging(e.target.value as any)}> <select value={evCharging} onChange={(e) => setEvCharging(e.target.value as any)}>
<option value="ALL">{t('evChargingAny')}</option> <option value="ALL">{t('evChargingAny')}</option>
<option value="FREE">{t('evChargingFree')}</option> <option value="YES">{t('evChargingYes')}</option>
<option value="PAID">{t('evChargingPaid')}</option> <option value="NO">{t('evChargingNo')}</option>
<option value="NONE">{t('evChargingNone')}</option>
</select> </select>
</label> </label>
</div> </div>
@ -482,8 +483,8 @@ export default function ListingsIndexPage() {
{startDate && endDate && l.availableForDates ? ( {startDate && endDate && l.availableForDates ? (
<span className="badge">{t('availableForDates')}</span> <span className="badge">{t('availableForDates')}</span>
) : null} ) : null}
{l.evCharging === 'FREE' ? <span className="badge">{t('amenityEvFree')}</span> : null} {l.evChargingAvailable ? <span className="badge">{t('amenityEvAvailable')}</span> : null}
{l.evCharging === 'PAID' ? <span className="badge">{t('amenityEvPaid')}</span> : null} {l.hasSkiPass ? <span className="badge">{t('amenitySkiPass')}</span> : null}
{l.hasAirConditioning ? <span className="badge">{t('amenityAirConditioning')}</span> : null} {l.hasAirConditioning ? <span className="badge">{t('amenityAirConditioning')}</span> : null}
{l.hasKitchen ? <span className="badge">{t('amenityKitchen')}</span> : null} {l.hasKitchen ? <span className="badge">{t('amenityKitchen')}</span> : null}
{l.hasDishwasher ? <span className="badge">{t('amenityDishwasher')}</span> : null} {l.hasDishwasher ? <span className="badge">{t('amenityDishwasher')}</span> : null}

View file

@ -224,12 +224,13 @@ const baseMessages = {
amenityBarbecue: 'Barbecue grill', amenityBarbecue: 'Barbecue grill',
amenityMicrowave: 'Microwave', amenityMicrowave: 'Microwave',
amenityFreeParking: 'Free parking', amenityFreeParking: 'Free parking',
amenityEvFree: 'EV charging (free)', amenityEvAvailable: 'EV charging nearby',
amenityEvPaid: 'EV charging (paid)', amenitySkiPass: 'Ski pass included',
evChargingLabel: 'EV charging', evChargingLabel: 'EV charging nearby',
evChargingNone: 'Not available', evChargingYes: 'Charging nearby',
evChargingFree: 'Free for guests', evChargingNo: 'No charging nearby',
evChargingPaid: 'Paid on-site', evChargingAny: 'Any',
evChargingExplain: 'Is there EV charging available on-site or nearby?',
capacityGuests: '{count} guests', capacityGuests: '{count} guests',
capacityBedrooms: '{count} bedrooms', capacityBedrooms: '{count} bedrooms',
capacityBeds: '{count} beds', capacityBeds: '{count} beds',
@ -512,12 +513,13 @@ const baseMessages = {
amenityBarbecue: 'Grilli', amenityBarbecue: 'Grilli',
amenityMicrowave: 'Mikroaaltouuni', amenityMicrowave: 'Mikroaaltouuni',
amenityFreeParking: 'Maksuton pysäköinti', amenityFreeParking: 'Maksuton pysäköinti',
amenityEvFree: 'Sähköauton lataus (ilmainen)', amenityEvAvailable: 'Sähköauton lataus lähellä',
amenityEvPaid: 'Sähköauton lataus (maksullinen)', amenitySkiPass: 'Hissilippu sisältyy',
evChargingLabel: 'Sähköauton lataus', evChargingLabel: 'Sähköauton lataus lähellä',
evChargingNone: 'Ei saatavilla', evChargingYes: 'Latausta lähellä',
evChargingFree: 'Ilmainen asiakkaille', evChargingNo: 'Ei latausta lähellä',
evChargingPaid: 'Maksullinen', evChargingAny: 'Kaikki',
evChargingExplain: 'Onko kohteessa tai lähistöllä sähköauton latausmahdollisuus?',
capacityGuests: '{count} vierasta', capacityGuests: '{count} vierasta',
capacityBedrooms: '{count} makuuhuonetta', capacityBedrooms: '{count} makuuhuonetta',
capacityBeds: '{count} vuodetta', capacityBeds: '{count} vuodetta',
@ -636,8 +638,15 @@ const svMessages: Record<keyof typeof baseMessages.en, string> = {
priceWeekendShort: '{price}€ helg', priceWeekendShort: '{price}€ helg',
priceNotSet: 'Ej angivet', priceNotSet: 'Ej angivet',
listingPrices: 'Priser', listingPrices: 'Priser',
amenityEvAvailable: 'EV-laddning i närheten',
amenitySkiPass: 'Liftkort ingår',
amenityMicrowave: 'Mikrovågsugn', amenityMicrowave: 'Mikrovågsugn',
amenityFreeParking: 'Gratis parkering', amenityFreeParking: 'Gratis parkering',
evChargingLabel: 'EV-laddning i närheten',
evChargingYes: 'Laddning i närheten',
evChargingNo: 'Ingen laddning i närheten',
evChargingAny: 'Alla',
evChargingExplain: 'Finns det EV-laddning på plats eller i närheten?',
}; };
export const messages = { ...baseMessages, sv: svMessages } as const; export const messages = { ...baseMessages, sv: svMessages } as const;

View file

@ -0,0 +1,31 @@
-- Add ski pass amenity and convert EV charging to boolean availability
ALTER TABLE "Listing"
ADD COLUMN IF NOT EXISTS "hasSkiPass" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS "evChargingAvailable" BOOLEAN NOT NULL DEFAULT false;
-- Backfill evChargingAvailable from legacy enum if present
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Listing' AND column_name = 'evCharging'
) THEN
UPDATE "Listing"
SET "evChargingAvailable" = CASE WHEN "evCharging" IS NULL OR "evCharging" = 'NONE' THEN false ELSE true END;
END IF;
END $$;
-- Drop legacy enum column/type if present
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Listing' AND column_name = 'evCharging'
) THEN
ALTER TABLE "Listing" DROP COLUMN "evCharging";
END IF;
IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'EvCharging') THEN
DROP TYPE "EvCharging";
END IF;
END $$;

View file

@ -1,10 +1,6 @@
-- Add missing address and EV charging fields to listings -- Add address fields and EV charging enum column (legacy)
DO $$
BEGIN
CREATE TYPE "EvCharging" AS ENUM ('NONE', 'FREE', 'PAID');
EXCEPTION
WHEN duplicate_object THEN NULL;
END$$;
ALTER TABLE "Listing" ADD COLUMN IF NOT EXISTS "streetAddress" TEXT; ALTER TABLE "Listing" ADD COLUMN IF NOT EXISTS "streetAddress" TEXT;
ALTER TABLE "Listing" ADD COLUMN IF NOT EXISTS "addressNote" TEXT;
ALTER TABLE "Listing" ADD COLUMN IF NOT EXISTS "latitude" DOUBLE PRECISION;
ALTER TABLE "Listing" ADD COLUMN IF NOT EXISTS "longitude" DOUBLE PRECISION;
ALTER TABLE "Listing" ADD COLUMN IF NOT EXISTS "evCharging" "EvCharging" NOT NULL DEFAULT 'NONE'; ALTER TABLE "Listing" ADD COLUMN IF NOT EXISTS "evCharging" "EvCharging" NOT NULL DEFAULT 'NONE';

View file

@ -29,12 +29,6 @@ enum ListingStatus {
REMOVED REMOVED
} }
enum EvCharging {
NONE
FREE
PAID
}
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
email String @unique email String @unique
@ -94,7 +88,8 @@ model Listing {
hasBarbecue Boolean @default(false) hasBarbecue Boolean @default(false)
hasMicrowave Boolean @default(false) hasMicrowave Boolean @default(false)
hasFreeParking Boolean @default(false) hasFreeParking Boolean @default(false)
evCharging EvCharging @default(NONE) hasSkiPass Boolean @default(false)
evChargingAvailable Boolean @default(false)
calendarUrls String[] @db.Text @default([]) calendarUrls String[] @db.Text @default([])
priceWeekdayEuros Int? priceWeekdayEuros Int?
priceWeekendEuros Int? priceWeekendEuros Int?

View file

@ -161,7 +161,8 @@ async function main() {
hasFreeParking: true, hasFreeParking: true,
petsAllowed: false, petsAllowed: false,
byTheLake: true, byTheLake: true,
evCharging: 'FREE', evChargingAvailable: true,
hasSkiPass: false,
priceWeekdayEuros: 145, priceWeekdayEuros: 145,
priceWeekendEuros: 165, priceWeekendEuros: 165,
cover: { cover: {
@ -208,7 +209,8 @@ async function main() {
hasFreeParking: false, hasFreeParking: false,
petsAllowed: false, petsAllowed: false,
byTheLake: false, byTheLake: false,
evCharging: 'PAID', evChargingAvailable: true,
hasSkiPass: false,
priceWeekdayEuros: 165, priceWeekdayEuros: 165,
priceWeekendEuros: 185, priceWeekendEuros: 185,
cover: { cover: {
@ -253,7 +255,8 @@ async function main() {
hasFreeParking: true, hasFreeParking: true,
petsAllowed: true, petsAllowed: true,
byTheLake: false, byTheLake: false,
evCharging: 'NONE', evChargingAvailable: false,
hasSkiPass: false,
priceWeekdayEuros: 110, priceWeekdayEuros: 110,
priceWeekendEuros: 125, priceWeekendEuros: 125,
cover: { cover: {
@ -297,7 +300,8 @@ async function main() {
hasFreeParking: true, hasFreeParking: true,
petsAllowed: false, petsAllowed: false,
byTheLake: true, byTheLake: true,
evCharging: 'FREE', evChargingAvailable: true,
hasSkiPass: true,
priceWeekdayEuros: 189, priceWeekdayEuros: 189,
priceWeekendEuros: 215, priceWeekendEuros: 215,
cover: { cover: {
@ -341,7 +345,8 @@ async function main() {
hasFreeParking: false, hasFreeParking: false,
petsAllowed: false, petsAllowed: false,
byTheLake: false, byTheLake: false,
evCharging: 'NONE', evChargingAvailable: false,
hasSkiPass: false,
priceWeekdayEuros: 95, priceWeekdayEuros: 95,
priceWeekendEuros: 110, priceWeekendEuros: 110,
cover: { cover: {
@ -383,7 +388,8 @@ async function main() {
hasFreeParking: true, hasFreeParking: true,
petsAllowed: true, petsAllowed: true,
byTheLake: true, byTheLake: true,
evCharging: 'PAID', evChargingAvailable: true,
hasSkiPass: true,
priceWeekdayEuros: 245, priceWeekdayEuros: 245,
priceWeekendEuros: 275, priceWeekendEuros: 275,
cover: { cover: {
@ -425,7 +431,8 @@ async function main() {
hasFreeParking: true, hasFreeParking: true,
petsAllowed: false, petsAllowed: false,
byTheLake: true, byTheLake: true,
evCharging: 'FREE', evChargingAvailable: true,
hasSkiPass: true,
priceWeekdayEuros: 129, priceWeekdayEuros: 129,
priceWeekendEuros: 149, priceWeekendEuros: 149,
cover: { cover: {
@ -467,7 +474,8 @@ async function main() {
hasFreeParking: false, hasFreeParking: false,
petsAllowed: false, petsAllowed: false,
byTheLake: false, byTheLake: false,
evCharging: 'NONE', evChargingAvailable: false,
hasSkiPass: false,
priceWeekdayEuros: 99, priceWeekdayEuros: 99,
priceWeekendEuros: 115, priceWeekendEuros: 115,
cover: { cover: {
@ -551,7 +559,8 @@ async function main() {
hasFreeParking: true, hasFreeParking: true,
petsAllowed: false, petsAllowed: false,
byTheLake: true, byTheLake: true,
evCharging: 'PAID', evChargingAvailable: true,
hasSkiPass: true,
priceWeekdayEuros: 115, priceWeekdayEuros: 115,
priceWeekendEuros: 130, priceWeekendEuros: 130,
cover: { cover: {
@ -583,6 +592,8 @@ async function main() {
hasBarbecue: item.hasBarbecue ?? randBool(0.5), hasBarbecue: item.hasBarbecue ?? randBool(0.5),
hasMicrowave: item.hasMicrowave ?? randBool(0.7), hasMicrowave: item.hasMicrowave ?? randBool(0.7),
hasFreeParking: item.hasFreeParking ?? randBool(0.6), hasFreeParking: item.hasFreeParking ?? randBool(0.6),
evChargingAvailable: item.evChargingAvailable ?? randBool(0.4),
hasSkiPass: item.hasSkiPass ?? randBool(0.2),
}; };
}); });