Add on-site EV charging amenity
This commit is contained in:
parent
6674f95856
commit
cb92a17f1d
13 changed files with 115 additions and 16 deletions
|
|
@ -86,4 +86,4 @@
|
||||||
- Netdata installed on k3s node (`node1.lomavuokraus.fi:8443`) and DB host (`db1.lomavuokraus.fi:8443`) behind self-signed TLS + basic auth; DB Netdata includes Postgres metrics via dedicated `netdata` role.
|
- Netdata installed on k3s node (`node1.lomavuokraus.fi:8443`) and DB host (`db1.lomavuokraus.fi:8443`) behind self-signed TLS + basic auth; DB Netdata includes Postgres metrics via dedicated `netdata` role.
|
||||||
- Footer now includes a minimal cookie usage statement (essential cookies only; site requires acceptance).
|
- Footer now includes a minimal cookie usage statement (essential cookies only; site requires acceptance).
|
||||||
- Forgejo deployment scaffolding added: Docker Compose + runner config guidance and Apache vhost for git.halla-aho.net, plus CI workflow placeholder under `.forgejo/workflows/`.
|
- Forgejo deployment scaffolding added: Docker Compose + runner config guidance and Apache vhost for git.halla-aho.net, plus CI workflow placeholder under `.forgejo/workflows/`.
|
||||||
- Amenities: added wheelchair accessibility flag and clarified EV charging amenity wording to mean on-site charging.
|
- Amenities: added separate EV charging flags (on-site vs nearby) plus wheelchair accessibility, including browse filters and admin approvals view badges.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,16 @@ import { useEffect, useState } from 'react';
|
||||||
import { useI18n } from '../../components/I18nProvider';
|
import { useI18n } from '../../components/I18nProvider';
|
||||||
|
|
||||||
type PendingUser = { id: string; email: string; status: string; emailVerifiedAt: string | null; approvedAt: string | null; role: string };
|
type PendingUser = { id: string; email: string; status: string; emailVerifiedAt: string | null; approvedAt: string | null; role: string };
|
||||||
type PendingListing = { id: string; status: string; createdAt: string; owner: { email: string }; translations: { title: string; slug: string; locale: string }[] };
|
type PendingListing = {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
createdAt: string;
|
||||||
|
owner: { email: string };
|
||||||
|
translations: { title: string; slug: string; locale: string }[];
|
||||||
|
evChargingAvailable: boolean;
|
||||||
|
evChargingOnSite: boolean;
|
||||||
|
wheelchairAccessible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export default function PendingAdminPage() {
|
export default function PendingAdminPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -150,6 +159,11 @@ export default function PendingAdminPage() {
|
||||||
<div>
|
<div>
|
||||||
<strong>{l.translations[0]?.title ?? 'Listing'}</strong> — owner: {l.owner.email}
|
<strong>{l.translations[0]?.title ?? 'Listing'}</strong> — owner: {l.owner.email}
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ marginTop: 6, display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||||
|
{l.evChargingOnSite ? <span className="badge">{t('amenityEvOnSite')}</span> : null}
|
||||||
|
{l.evChargingAvailable && !l.evChargingOnSite ? <span className="badge">{t('amenityEvNearby')}</span> : null}
|
||||||
|
{l.wheelchairAccessible ? <span className="badge">{t('amenityWheelchairAccessible')}</span> : null}
|
||||||
|
</div>
|
||||||
<div style={{ fontSize: 12, color: '#666' }}>
|
<div style={{ fontSize: 12, color: '#666' }}>
|
||||||
{t('slugsLabel')}: {l.translations.map((t) => `${t.slug} (${t.locale})`).join(', ')}
|
{t('slugsLabel')}: {l.translations.map((t) => `${t.slug} (${t.locale})`).join(', ')}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,16 @@ export async function GET(req: Request) {
|
||||||
wantsListings
|
wantsListings
|
||||||
? prisma.listing.findMany({
|
? prisma.listing.findMany({
|
||||||
where: { status: ListingStatus.PENDING, removedAt: null },
|
where: { status: ListingStatus.PENDING, removedAt: null },
|
||||||
select: { id: true, status: true, createdAt: true, owner: { select: { email: true } }, translations: { select: { title: true, slug: true, locale: true } } },
|
select: {
|
||||||
|
id: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
evChargingAvailable: true,
|
||||||
|
evChargingOnSite: true,
|
||||||
|
wheelchairAccessible: true,
|
||||||
|
owner: { select: { email: true } },
|
||||||
|
translations: { select: { title: true, slug: true, locale: true } },
|
||||||
|
},
|
||||||
orderBy: { createdAt: 'asc' },
|
orderBy: { createdAt: 'asc' },
|
||||||
take: 50,
|
take: 50,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,13 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const incomingEvChargingOnSite =
|
||||||
|
body.evChargingOnSite === undefined ? existing.evChargingOnSite : Boolean(body.evChargingOnSite);
|
||||||
|
const incomingEvChargingAvailable =
|
||||||
|
body.evChargingAvailable === undefined ? existing.evChargingAvailable : Boolean(body.evChargingAvailable);
|
||||||
|
const evChargingAvailable = incomingEvChargingOnSite ? true : incomingEvChargingAvailable;
|
||||||
|
const evChargingOnSite = evChargingAvailable ? incomingEvChargingOnSite : false;
|
||||||
|
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
status,
|
status,
|
||||||
approvedAt: status === ListingStatus.PUBLISHED ? existing.approvedAt ?? new Date() : null,
|
approvedAt: status === ListingStatus.PUBLISHED ? existing.approvedAt ?? new Date() : null,
|
||||||
|
|
@ -211,7 +218,8 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
||||||
hasMicrowave: body.hasMicrowave === undefined ? existing.hasMicrowave : Boolean(body.hasMicrowave),
|
hasMicrowave: body.hasMicrowave === undefined ? existing.hasMicrowave : Boolean(body.hasMicrowave),
|
||||||
hasFreeParking: body.hasFreeParking === undefined ? existing.hasFreeParking : Boolean(body.hasFreeParking),
|
hasFreeParking: body.hasFreeParking === undefined ? existing.hasFreeParking : Boolean(body.hasFreeParking),
|
||||||
hasSkiPass: body.hasSkiPass === undefined ? existing.hasSkiPass : Boolean(body.hasSkiPass),
|
hasSkiPass: body.hasSkiPass === undefined ? existing.hasSkiPass : Boolean(body.hasSkiPass),
|
||||||
evChargingAvailable: body.evChargingAvailable === undefined ? existing.evChargingAvailable : Boolean(body.evChargingAvailable),
|
evChargingAvailable,
|
||||||
|
evChargingOnSite,
|
||||||
wheelchairAccessible:
|
wheelchairAccessible:
|
||||||
body.wheelchairAccessible === undefined ? existing.wheelchairAccessible : Boolean(body.wheelchairAccessible),
|
body.wheelchairAccessible === undefined ? existing.wheelchairAccessible : Boolean(body.wheelchairAccessible),
|
||||||
priceWeekdayEuros,
|
priceWeekdayEuros,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ export async function GET(req: Request) {
|
||||||
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 === 'true' ? true : evChargingParam === 'false' ? false : null;
|
const evCharging = evChargingParam === 'true' ? true : evChargingParam === 'false' ? false : null;
|
||||||
|
const evChargingOnSiteParam = searchParams.get('evChargingOnSite');
|
||||||
|
const evChargingOnSite = evChargingOnSiteParam === 'true' ? true : evChargingOnSiteParam === '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;
|
||||||
|
|
@ -82,6 +84,7 @@ export async function GET(req: Request) {
|
||||||
if (amenityFilters.includes('parking')) amenityWhere.hasFreeParking = true;
|
if (amenityFilters.includes('parking')) amenityWhere.hasFreeParking = true;
|
||||||
if (amenityFilters.includes('skipass')) amenityWhere.hasSkiPass = true;
|
if (amenityFilters.includes('skipass')) amenityWhere.hasSkiPass = true;
|
||||||
if (amenityFilters.includes('accessible')) amenityWhere.wheelchairAccessible = true;
|
if (amenityFilters.includes('accessible')) amenityWhere.wheelchairAccessible = true;
|
||||||
|
if (amenityFilters.includes('ev-onsite')) amenityWhere.evChargingOnSite = true;
|
||||||
|
|
||||||
const where: Prisma.ListingWhereInput = {
|
const where: Prisma.ListingWhereInput = {
|
||||||
status: ListingStatus.PUBLISHED,
|
status: ListingStatus.PUBLISHED,
|
||||||
|
|
@ -89,6 +92,7 @@ export async function GET(req: Request) {
|
||||||
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,
|
||||||
evChargingAvailable: evCharging ?? undefined,
|
evChargingAvailable: evCharging ?? undefined,
|
||||||
|
evChargingOnSite: evChargingOnSite ?? undefined,
|
||||||
...amenityWhere,
|
...amenityWhere,
|
||||||
translations: q
|
translations: q
|
||||||
? {
|
? {
|
||||||
|
|
@ -175,6 +179,7 @@ export async function GET(req: Request) {
|
||||||
hasFreeParking: listing.hasFreeParking,
|
hasFreeParking: listing.hasFreeParking,
|
||||||
hasSkiPass: listing.hasSkiPass,
|
hasSkiPass: listing.hasSkiPass,
|
||||||
evChargingAvailable: listing.evChargingAvailable,
|
evChargingAvailable: listing.evChargingAvailable,
|
||||||
|
evChargingOnSite: listing.evChargingOnSite,
|
||||||
wheelchairAccessible: listing.wheelchairAccessible,
|
wheelchairAccessible: listing.wheelchairAccessible,
|
||||||
maxGuests: listing.maxGuests,
|
maxGuests: listing.maxGuests,
|
||||||
bedrooms: listing.bedrooms,
|
bedrooms: listing.bedrooms,
|
||||||
|
|
@ -333,7 +338,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 = Boolean(body.evChargingAvailable);
|
const evChargingOnSite = Boolean(body.evChargingOnSite);
|
||||||
|
const evChargingAvailable = Boolean(body.evChargingAvailable) || evChargingOnSite;
|
||||||
const wheelchairAccessible = Boolean(body.wheelchairAccessible);
|
const wheelchairAccessible = Boolean(body.wheelchairAccessible);
|
||||||
|
|
||||||
const listing = await prisma.listing.create({
|
const listing = await prisma.listing.create({
|
||||||
|
|
@ -367,6 +373,7 @@ export async function POST(req: Request) {
|
||||||
hasFreeParking: Boolean(body.hasFreeParking),
|
hasFreeParking: Boolean(body.hasFreeParking),
|
||||||
hasSkiPass: Boolean(body.hasSkiPass),
|
hasSkiPass: Boolean(body.hasSkiPass),
|
||||||
evChargingAvailable,
|
evChargingAvailable,
|
||||||
|
evChargingOnSite,
|
||||||
wheelchairAccessible,
|
wheelchairAccessible,
|
||||||
priceWeekdayEuros,
|
priceWeekdayEuros,
|
||||||
priceWeekendEuros,
|
priceWeekendEuros,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const amenityIcons: Record<string, string> = {
|
||||||
lake: '🌊',
|
lake: '🌊',
|
||||||
ac: '❄️',
|
ac: '❄️',
|
||||||
ev: '⚡',
|
ev: '⚡',
|
||||||
|
evOnSite: '🔌',
|
||||||
kitchen: '🍽️',
|
kitchen: '🍽️',
|
||||||
dishwasher: '🧼',
|
dishwasher: '🧼',
|
||||||
washer: '🧺',
|
washer: '🧺',
|
||||||
|
|
@ -92,7 +93,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.evChargingAvailable ? { icon: amenityIcons.ev, label: t('amenityEvAvailable') } : null,
|
listing.evChargingOnSite ? { icon: amenityIcons.evOnSite, label: t('amenityEvOnSite') } : null,
|
||||||
|
listing.evChargingAvailable && !listing.evChargingOnSite ? { icon: amenityIcons.ev, label: t('amenityEvNearby') } : null,
|
||||||
listing.wheelchairAccessible ? { icon: amenityIcons.accessible, label: t('amenityWheelchairAccessible') } : null,
|
listing.wheelchairAccessible ? { icon: amenityIcons.accessible, label: t('amenityWheelchairAccessible') } : null,
|
||||||
listing.hasSkiPass ? { icon: amenityIcons.ski, label: t('amenitySkiPass') } : 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,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export default function EditListingPage({ params }: { params: { id: string } })
|
||||||
const [hasFreeParking, setHasFreeParking] = useState(false);
|
const [hasFreeParking, setHasFreeParking] = useState(false);
|
||||||
const [hasSkiPass, setHasSkiPass] = useState(false);
|
const [hasSkiPass, setHasSkiPass] = useState(false);
|
||||||
const [evChargingAvailable, setEvChargingAvailable] = useState<boolean>(false);
|
const [evChargingAvailable, setEvChargingAvailable] = useState<boolean>(false);
|
||||||
|
const [evChargingOnSite, setEvChargingOnSite] = useState(false);
|
||||||
const [wheelchairAccessible, setWheelchairAccessible] = useState(false);
|
const [wheelchairAccessible, setWheelchairAccessible] = useState(false);
|
||||||
const [calendarUrls, setCalendarUrls] = useState('');
|
const [calendarUrls, setCalendarUrls] = useState('');
|
||||||
const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]);
|
const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]);
|
||||||
|
|
@ -139,6 +140,7 @@ export default function EditListingPage({ params }: { params: { id: string } })
|
||||||
setHasFreeParking(listing.hasFreeParking);
|
setHasFreeParking(listing.hasFreeParking);
|
||||||
setHasSkiPass(listing.hasSkiPass);
|
setHasSkiPass(listing.hasSkiPass);
|
||||||
setEvChargingAvailable(listing.evChargingAvailable);
|
setEvChargingAvailable(listing.evChargingAvailable);
|
||||||
|
setEvChargingOnSite(Boolean(listing.evChargingOnSite));
|
||||||
setWheelchairAccessible(Boolean(listing.wheelchairAccessible));
|
setWheelchairAccessible(Boolean(listing.wheelchairAccessible));
|
||||||
setCalendarUrls((listing.calendarUrls || []).join('\n'));
|
setCalendarUrls((listing.calendarUrls || []).join('\n'));
|
||||||
if (listing.images?.length) {
|
if (listing.images?.length) {
|
||||||
|
|
@ -257,6 +259,20 @@ export default function EditListingPage({ params }: { params: { id: string } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleEvChargingNearby(next: boolean) {
|
||||||
|
setEvChargingAvailable(next);
|
||||||
|
if (!next) {
|
||||||
|
setEvChargingOnSite(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEvChargingOnSite(next: boolean) {
|
||||||
|
setEvChargingOnSite(next);
|
||||||
|
if (next) {
|
||||||
|
setEvChargingAvailable(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const amenityOptions = [
|
const amenityOptions = [
|
||||||
{ key: 'sauna', label: t('amenitySauna'), icon: '🧖', checked: hasSauna, toggle: setHasSauna },
|
{ key: 'sauna', label: t('amenitySauna'), icon: '🧖', checked: hasSauna, toggle: setHasSauna },
|
||||||
{ key: 'fireplace', label: t('amenityFireplace'), icon: '🔥', checked: hasFireplace, toggle: setHasFireplace },
|
{ key: 'fireplace', label: t('amenityFireplace'), icon: '🔥', checked: hasFireplace, toggle: setHasFireplace },
|
||||||
|
|
@ -271,7 +287,8 @@ export default function EditListingPage({ params }: { params: { id: string } })
|
||||||
{ 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 },
|
{ key: 'ski', label: t('amenitySkiPass'), icon: '⛷️', checked: hasSkiPass, toggle: setHasSkiPass },
|
||||||
{ key: 'ev', label: t('amenityEvAvailable'), icon: '⚡', checked: evChargingAvailable, toggle: setEvChargingAvailable },
|
{ key: 'ev', label: t('amenityEvNearby'), icon: '⚡', checked: evChargingAvailable, toggle: toggleEvChargingNearby },
|
||||||
|
{ key: 'ev-onsite', label: t('amenityEvOnSite'), icon: '🔌', checked: evChargingOnSite, toggle: toggleEvChargingOnSite },
|
||||||
{ key: 'accessible', label: t('amenityWheelchairAccessible'), icon: '♿', checked: wheelchairAccessible, toggle: setWheelchairAccessible },
|
{ key: 'accessible', label: t('amenityWheelchairAccessible'), icon: '♿', checked: wheelchairAccessible, toggle: setWheelchairAccessible },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -444,6 +461,7 @@ export default function EditListingPage({ params }: { params: { id: string } })
|
||||||
hasFreeParking,
|
hasFreeParking,
|
||||||
hasSkiPass,
|
hasSkiPass,
|
||||||
evChargingAvailable,
|
evChargingAvailable,
|
||||||
|
evChargingOnSite,
|
||||||
wheelchairAccessible,
|
wheelchairAccessible,
|
||||||
coverImageIndex,
|
coverImageIndex,
|
||||||
images: selectedImages.length ? parseImages() : undefined,
|
images: selectedImages.length ? parseImages() : undefined,
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ export default function NewListingPage() {
|
||||||
const [hasFreeParking, setHasFreeParking] = useState(false);
|
const [hasFreeParking, setHasFreeParking] = useState(false);
|
||||||
const [hasSkiPass, setHasSkiPass] = useState(false);
|
const [hasSkiPass, setHasSkiPass] = useState(false);
|
||||||
const [evChargingAvailable, setEvChargingAvailable] = useState<boolean>(false);
|
const [evChargingAvailable, setEvChargingAvailable] = useState<boolean>(false);
|
||||||
|
const [evChargingOnSite, setEvChargingOnSite] = useState(false);
|
||||||
const [wheelchairAccessible, setWheelchairAccessible] = useState(false);
|
const [wheelchairAccessible, setWheelchairAccessible] = useState(false);
|
||||||
const [calendarUrls, setCalendarUrls] = useState('');
|
const [calendarUrls, setCalendarUrls] = useState('');
|
||||||
const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]);
|
const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]);
|
||||||
|
|
@ -124,6 +125,20 @@ export default function NewListingPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleEvChargingNearby(next: boolean) {
|
||||||
|
setEvChargingAvailable(next);
|
||||||
|
if (!next) {
|
||||||
|
setEvChargingOnSite(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEvChargingOnSite(next: boolean) {
|
||||||
|
setEvChargingOnSite(next);
|
||||||
|
if (next) {
|
||||||
|
setEvChargingAvailable(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const amenityOptions = [
|
const amenityOptions = [
|
||||||
{ key: 'sauna', label: t('amenitySauna'), icon: '🧖', checked: hasSauna, toggle: setHasSauna },
|
{ key: 'sauna', label: t('amenitySauna'), icon: '🧖', checked: hasSauna, toggle: setHasSauna },
|
||||||
{ key: 'fireplace', label: t('amenityFireplace'), icon: '🔥', checked: hasFireplace, toggle: setHasFireplace },
|
{ key: 'fireplace', label: t('amenityFireplace'), icon: '🔥', checked: hasFireplace, toggle: setHasFireplace },
|
||||||
|
|
@ -138,7 +153,8 @@ export default function NewListingPage() {
|
||||||
{ 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 },
|
{ key: 'ski', label: t('amenitySkiPass'), icon: '⛷️', checked: hasSkiPass, toggle: setHasSkiPass },
|
||||||
{ key: 'ev', label: t('amenityEvAvailable'), icon: '⚡', checked: evChargingAvailable, toggle: setEvChargingAvailable },
|
{ key: 'ev', label: t('amenityEvNearby'), icon: '⚡', checked: evChargingAvailable, toggle: toggleEvChargingNearby },
|
||||||
|
{ key: 'ev-onsite', label: t('amenityEvOnSite'), icon: '🔌', checked: evChargingOnSite, toggle: toggleEvChargingOnSite },
|
||||||
{ key: 'accessible', label: t('amenityWheelchairAccessible'), icon: '♿', checked: wheelchairAccessible, toggle: setWheelchairAccessible },
|
{ key: 'accessible', label: t('amenityWheelchairAccessible'), icon: '♿', checked: wheelchairAccessible, toggle: setWheelchairAccessible },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -376,6 +392,7 @@ export default function NewListingPage() {
|
||||||
hasFreeParking,
|
hasFreeParking,
|
||||||
hasSkiPass,
|
hasSkiPass,
|
||||||
evChargingAvailable,
|
evChargingAvailable,
|
||||||
|
evChargingOnSite,
|
||||||
wheelchairAccessible,
|
wheelchairAccessible,
|
||||||
coverImageIndex,
|
coverImageIndex,
|
||||||
images: parseImages(),
|
images: parseImages(),
|
||||||
|
|
@ -418,6 +435,7 @@ export default function NewListingPage() {
|
||||||
setHasFreeParking(false);
|
setHasFreeParking(false);
|
||||||
setHasSkiPass(false);
|
setHasSkiPass(false);
|
||||||
setEvChargingAvailable(false);
|
setEvChargingAvailable(false);
|
||||||
|
setEvChargingOnSite(false);
|
||||||
setWheelchairAccessible(false);
|
setWheelchairAccessible(false);
|
||||||
setRegion('');
|
setRegion('');
|
||||||
setCity('');
|
setCity('');
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ type ListingResult = {
|
||||||
hasMicrowave: boolean;
|
hasMicrowave: boolean;
|
||||||
hasFreeParking: boolean;
|
hasFreeParking: boolean;
|
||||||
evChargingAvailable: boolean;
|
evChargingAvailable: boolean;
|
||||||
|
evChargingOnSite: boolean;
|
||||||
wheelchairAccessible: boolean;
|
wheelchairAccessible: boolean;
|
||||||
hasSkiPass: boolean;
|
hasSkiPass: boolean;
|
||||||
maxGuests: number;
|
maxGuests: number;
|
||||||
|
|
@ -94,6 +95,7 @@ const amenityIcons: Record<string, string> = {
|
||||||
accessible: '♿',
|
accessible: '♿',
|
||||||
ski: '⛷️',
|
ski: '⛷️',
|
||||||
ev: '⚡',
|
ev: '⚡',
|
||||||
|
evOnSite: '🔌',
|
||||||
};
|
};
|
||||||
|
|
||||||
function ListingsMap({
|
function ListingsMap({
|
||||||
|
|
@ -226,7 +228,8 @@ export default function ListingsIndexPage() {
|
||||||
{ key: 'parking', label: t('amenityFreeParking'), icon: amenityIcons.parking },
|
{ key: 'parking', label: t('amenityFreeParking'), icon: amenityIcons.parking },
|
||||||
{ key: 'accessible', label: t('amenityWheelchairAccessible'), icon: amenityIcons.accessible },
|
{ key: 'accessible', label: t('amenityWheelchairAccessible'), icon: amenityIcons.accessible },
|
||||||
{ key: 'skipass', label: t('amenitySkiPass'), icon: amenityIcons.ski },
|
{ key: 'skipass', label: t('amenitySkiPass'), icon: amenityIcons.ski },
|
||||||
{ key: 'ev', label: t('amenityEvAvailable'), icon: amenityIcons.ev },
|
{ key: 'ev', label: t('amenityEvNearby'), icon: amenityIcons.ev },
|
||||||
|
{ key: 'ev-onsite', label: t('amenityEvOnSite'), icon: amenityIcons.evOnSite },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function fetchListings() {
|
async function fetchListings() {
|
||||||
|
|
@ -479,7 +482,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.evChargingAvailable ? <span className="badge">{t('amenityEvAvailable')}</span> : null}
|
{l.evChargingOnSite ? <span className="badge">{t('amenityEvOnSite')}</span> : null}
|
||||||
|
{l.evChargingAvailable && !l.evChargingOnSite ? <span className="badge">{t('amenityEvNearby')}</span> : null}
|
||||||
{l.wheelchairAccessible ? <span className="badge">{t('amenityWheelchairAccessible')}</span> : null}
|
{l.wheelchairAccessible ? <span className="badge">{t('amenityWheelchairAccessible')}</span> : null}
|
||||||
{l.hasSkiPass ? <span className="badge">{t('amenitySkiPass')}</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}
|
||||||
|
|
|
||||||
12
lib/i18n.ts
12
lib/i18n.ts
|
|
@ -251,7 +251,9 @@ const baseMessages = {
|
||||||
amenityBarbecue: 'Barbecue grill',
|
amenityBarbecue: 'Barbecue grill',
|
||||||
amenityMicrowave: 'Microwave',
|
amenityMicrowave: 'Microwave',
|
||||||
amenityFreeParking: 'Free parking',
|
amenityFreeParking: 'Free parking',
|
||||||
amenityEvAvailable: 'EV charging',
|
amenityEvAvailable: 'EV charging nearby',
|
||||||
|
amenityEvNearby: 'EV charging nearby',
|
||||||
|
amenityEvOnSite: 'EV charging on-site',
|
||||||
amenityWheelchairAccessible: 'Wheelchair accessible',
|
amenityWheelchairAccessible: 'Wheelchair accessible',
|
||||||
amenitySkiPass: 'Ski pass included',
|
amenitySkiPass: 'Ski pass included',
|
||||||
evChargingLabel: 'EV charging',
|
evChargingLabel: 'EV charging',
|
||||||
|
|
@ -568,7 +570,9 @@ const baseMessages = {
|
||||||
amenityBarbecue: 'Grilli',
|
amenityBarbecue: 'Grilli',
|
||||||
amenityMicrowave: 'Mikroaaltouuni',
|
amenityMicrowave: 'Mikroaaltouuni',
|
||||||
amenityFreeParking: 'Maksuton pysäköinti',
|
amenityFreeParking: 'Maksuton pysäköinti',
|
||||||
amenityEvAvailable: 'Sähköauton lataus',
|
amenityEvAvailable: 'Sähköauton lataus lähellä',
|
||||||
|
amenityEvNearby: 'Sähköauton lataus lähellä',
|
||||||
|
amenityEvOnSite: 'Sähköauton lataus kohteessa',
|
||||||
amenityWheelchairAccessible: 'Esteetön / pyörätuolilla',
|
amenityWheelchairAccessible: 'Esteetön / pyörätuolilla',
|
||||||
amenitySkiPass: 'Hissilippu sisältyy',
|
amenitySkiPass: 'Hissilippu sisältyy',
|
||||||
evChargingLabel: 'Sähköauton lataus',
|
evChargingLabel: 'Sähköauton lataus',
|
||||||
|
|
@ -720,7 +724,9 @@ const svMessages: Record<keyof typeof baseMessages.en, string> = {
|
||||||
priceNotSet: 'Ej angivet',
|
priceNotSet: 'Ej angivet',
|
||||||
listingPrices: 'Priser',
|
listingPrices: 'Priser',
|
||||||
capacityUnknown: 'Kapacitet ej angiven',
|
capacityUnknown: 'Kapacitet ej angiven',
|
||||||
amenityEvAvailable: 'EV-laddning',
|
amenityEvAvailable: 'EV-laddning i närheten',
|
||||||
|
amenityEvNearby: 'EV-laddning i närheten',
|
||||||
|
amenityEvOnSite: 'EV-laddning på plats',
|
||||||
amenityWheelchairAccessible: 'Rullstolsanpassat',
|
amenityWheelchairAccessible: 'Rullstolsanpassat',
|
||||||
amenitySkiPass: 'Liftkort ingår',
|
amenitySkiPass: 'Liftkort ingår',
|
||||||
amenityMicrowave: 'Mikrovågsugn',
|
amenityMicrowave: 'Mikrovågsugn',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Add on-site EV charging amenity (in addition to nearby charging)
|
||||||
|
ALTER TABLE "Listing" ADD COLUMN IF NOT EXISTS "evChargingOnSite" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
|
@ -90,6 +90,7 @@ model Listing {
|
||||||
hasFreeParking Boolean @default(false)
|
hasFreeParking Boolean @default(false)
|
||||||
hasSkiPass Boolean @default(false)
|
hasSkiPass Boolean @default(false)
|
||||||
evChargingAvailable Boolean @default(false)
|
evChargingAvailable Boolean @default(false)
|
||||||
|
evChargingOnSite Boolean @default(false)
|
||||||
wheelchairAccessible Boolean @default(false)
|
wheelchairAccessible Boolean @default(false)
|
||||||
calendarUrls String[] @db.Text @default([])
|
calendarUrls String[] @db.Text @default([])
|
||||||
priceWeekdayEuros Int?
|
priceWeekdayEuros Int?
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,7 @@ async function main() {
|
||||||
petsAllowed: false,
|
petsAllowed: false,
|
||||||
byTheLake: true,
|
byTheLake: true,
|
||||||
evChargingAvailable: true,
|
evChargingAvailable: true,
|
||||||
|
evChargingOnSite: true,
|
||||||
hasSkiPass: true,
|
hasSkiPass: true,
|
||||||
priceWeekdayEuros: 189,
|
priceWeekdayEuros: 189,
|
||||||
priceWeekendEuros: 215,
|
priceWeekendEuros: 215,
|
||||||
|
|
@ -389,6 +390,7 @@ async function main() {
|
||||||
petsAllowed: true,
|
petsAllowed: true,
|
||||||
byTheLake: true,
|
byTheLake: true,
|
||||||
evChargingAvailable: true,
|
evChargingAvailable: true,
|
||||||
|
evChargingOnSite: true,
|
||||||
hasSkiPass: true,
|
hasSkiPass: true,
|
||||||
priceWeekdayEuros: 245,
|
priceWeekdayEuros: 245,
|
||||||
priceWeekendEuros: 275,
|
priceWeekendEuros: 275,
|
||||||
|
|
@ -432,6 +434,7 @@ async function main() {
|
||||||
petsAllowed: false,
|
petsAllowed: false,
|
||||||
byTheLake: true,
|
byTheLake: true,
|
||||||
evChargingAvailable: true,
|
evChargingAvailable: true,
|
||||||
|
evChargingOnSite: true,
|
||||||
hasSkiPass: true,
|
hasSkiPass: true,
|
||||||
priceWeekdayEuros: 129,
|
priceWeekdayEuros: 129,
|
||||||
priceWeekendEuros: 149,
|
priceWeekendEuros: 149,
|
||||||
|
|
@ -518,6 +521,7 @@ async function main() {
|
||||||
petsAllowed: false,
|
petsAllowed: false,
|
||||||
byTheLake: false,
|
byTheLake: false,
|
||||||
evChargingAvailable: true,
|
evChargingAvailable: true,
|
||||||
|
evChargingOnSite: true,
|
||||||
priceWeekdayEuros: 105,
|
priceWeekdayEuros: 105,
|
||||||
priceWeekendEuros: 120,
|
priceWeekendEuros: 120,
|
||||||
cover: {
|
cover: {
|
||||||
|
|
@ -582,6 +586,8 @@ async function main() {
|
||||||
const randBool = (p = 0.5) => Math.random() < p;
|
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);
|
const weekdayPrice = item.priceWeekdayEuros ?? Math.round(Math.random() * (220 - 90) + 90);
|
||||||
|
const evChargingOnSite = item.evChargingOnSite ?? randBool(0.15);
|
||||||
|
const evChargingAvailable = item.evChargingAvailable ?? evChargingOnSite ?? randBool(0.4);
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
priceWeekdayEuros: weekdayPrice,
|
priceWeekdayEuros: weekdayPrice,
|
||||||
|
|
@ -592,7 +598,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),
|
evChargingOnSite,
|
||||||
|
evChargingAvailable,
|
||||||
wheelchairAccessible: item.wheelchairAccessible ?? randBool(0.25),
|
wheelchairAccessible: item.wheelchairAccessible ?? randBool(0.25),
|
||||||
hasSkiPass: item.hasSkiPass ?? randBool(0.2),
|
hasSkiPass: item.hasSkiPass ?? randBool(0.2),
|
||||||
};
|
};
|
||||||
|
|
@ -632,7 +639,8 @@ async function main() {
|
||||||
hasFreeParking: item.hasFreeParking ?? false,
|
hasFreeParking: item.hasFreeParking ?? false,
|
||||||
petsAllowed: item.petsAllowed,
|
petsAllowed: item.petsAllowed,
|
||||||
byTheLake: item.byTheLake,
|
byTheLake: item.byTheLake,
|
||||||
evChargingAvailable: item.evChargingAvailable ?? false,
|
evChargingAvailable: item.evChargingAvailable ?? item.evChargingOnSite ?? false,
|
||||||
|
evChargingOnSite: item.evChargingOnSite ?? false,
|
||||||
wheelchairAccessible: item.wheelchairAccessible ?? false,
|
wheelchairAccessible: item.wheelchairAccessible ?? false,
|
||||||
priceWeekdayEuros: item.priceWeekdayEuros,
|
priceWeekdayEuros: item.priceWeekdayEuros,
|
||||||
priceWeekendEuros: item.priceWeekendEuros,
|
priceWeekendEuros: item.priceWeekendEuros,
|
||||||
|
|
@ -683,7 +691,8 @@ async function main() {
|
||||||
hasFreeParking: item.hasFreeParking ?? false,
|
hasFreeParking: item.hasFreeParking ?? false,
|
||||||
petsAllowed: item.petsAllowed,
|
petsAllowed: item.petsAllowed,
|
||||||
byTheLake: item.byTheLake,
|
byTheLake: item.byTheLake,
|
||||||
evChargingAvailable: item.evChargingAvailable ?? false,
|
evChargingAvailable: item.evChargingAvailable ?? item.evChargingOnSite ?? false,
|
||||||
|
evChargingOnSite: item.evChargingOnSite ?? false,
|
||||||
wheelchairAccessible: item.wheelchairAccessible ?? false,
|
wheelchairAccessible: item.wheelchairAccessible ?? false,
|
||||||
priceWeekdayEuros: item.priceWeekdayEuros,
|
priceWeekdayEuros: item.priceWeekdayEuros,
|
||||||
priceWeekendEuros: item.priceWeekendEuros,
|
priceWeekendEuros: item.priceWeekendEuros,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue