Add ski pass amenity and simplify EV charging availability
This commit is contained in:
parent
6cc5efc9e5
commit
27fc8ee2d1
9 changed files with 108 additions and 66 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
33
lib/i18n.ts
33
lib/i18n.ts
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 $$;
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue