From 6674f958568fe0bcaba3869f651e8e96feea7625 Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Wed, 17 Dec 2025 13:26:49 +0200 Subject: [PATCH] Add wheelchair accessibility amenity --- PROGRESS.md | 1 + app/api/listings/[id]/route.ts | 2 ++ app/api/listings/route.ts | 4 +++ app/listings/[slug]/page.tsx | 4 ++- app/listings/edit/[id]/page.tsx | 5 +++ app/listings/new/page.tsx | 7 ++++ app/listings/page.tsx | 4 +++ lib/i18n.ts | 33 ++++++++++--------- .../migration.sql | 3 ++ prisma/schema.prisma | 1 + prisma/seed.js | 9 +++-- 11 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 prisma/migrations/20251217_accessibility_amenity/migration.sql diff --git a/PROGRESS.md b/PROGRESS.md index d0d8f1d..a4dc936 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -86,3 +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. - 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/`. +- Amenities: added wheelchair accessibility flag and clarified EV charging amenity wording to mean on-site charging. diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index 45554b5..77b0963 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -212,6 +212,8 @@ export async function PUT(req: Request, { params }: { params: { id: string } }) hasFreeParking: body.hasFreeParking === undefined ? existing.hasFreeParking : Boolean(body.hasFreeParking), hasSkiPass: body.hasSkiPass === undefined ? existing.hasSkiPass : Boolean(body.hasSkiPass), evChargingAvailable: body.evChargingAvailable === undefined ? existing.evChargingAvailable : Boolean(body.evChargingAvailable), + wheelchairAccessible: + body.wheelchairAccessible === undefined ? existing.wheelchairAccessible : Boolean(body.wheelchairAccessible), priceWeekdayEuros, priceWeekendEuros, calendarUrls, diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index cf38991..2daf91d 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -81,6 +81,7 @@ export async function GET(req: Request) { if (amenityFilters.includes('microwave')) amenityWhere.hasMicrowave = true; if (amenityFilters.includes('parking')) amenityWhere.hasFreeParking = true; if (amenityFilters.includes('skipass')) amenityWhere.hasSkiPass = true; + if (amenityFilters.includes('accessible')) amenityWhere.wheelchairAccessible = true; const where: Prisma.ListingWhereInput = { status: ListingStatus.PUBLISHED, @@ -174,6 +175,7 @@ export async function GET(req: Request) { hasFreeParking: listing.hasFreeParking, hasSkiPass: listing.hasSkiPass, evChargingAvailable: listing.evChargingAvailable, + wheelchairAccessible: listing.wheelchairAccessible, maxGuests: listing.maxGuests, bedrooms: listing.bedrooms, beds: listing.beds, @@ -332,6 +334,7 @@ export async function POST(req: Request) { const status = saveDraft ? ListingStatus.DRAFT : autoApprove ? ListingStatus.PUBLISHED : ListingStatus.PENDING; const isSample = (contactEmail || '').toLowerCase() === SAMPLE_EMAIL; const evChargingAvailable = Boolean(body.evChargingAvailable); + const wheelchairAccessible = Boolean(body.wheelchairAccessible); const listing = await prisma.listing.create({ data: { @@ -364,6 +367,7 @@ export async function POST(req: Request) { hasFreeParking: Boolean(body.hasFreeParking), hasSkiPass: Boolean(body.hasSkiPass), evChargingAvailable, + wheelchairAccessible, priceWeekdayEuros, priceWeekendEuros, calendarUrls, diff --git a/app/listings/[slug]/page.tsx b/app/listings/[slug]/page.tsx index 3d1986d..b928b04 100644 --- a/app/listings/[slug]/page.tsx +++ b/app/listings/[slug]/page.tsx @@ -28,6 +28,7 @@ const amenityIcons: Record = { barbecue: '🍖', microwave: '🍲', parking: '🅿️', + accessible: '♿', ski: '⛷️', }; @@ -91,7 +92,8 @@ export default async function ListingPage({ params }: ListingPageProps) { listing.petsAllowed ? { icon: amenityIcons.pets, label: t('amenityPets') } : null, listing.byTheLake ? { icon: amenityIcons.lake, label: t('amenityLake') } : null, listing.hasAirConditioning ? { icon: amenityIcons.ac, label: t('amenityAirConditioning') } : null, - listing.evChargingAvailable ? { icon: amenityIcons.ev, label: t('amenityEvNearby') } : null, + listing.evChargingAvailable ? { icon: amenityIcons.ev, label: t('amenityEvAvailable') } : null, + listing.wheelchairAccessible ? { icon: amenityIcons.accessible, label: t('amenityWheelchairAccessible') } : null, listing.hasSkiPass ? { icon: amenityIcons.ski, label: t('amenitySkiPass') } : null, listing.hasKitchen ? { icon: amenityIcons.kitchen, label: t('amenityKitchen') } : null, listing.hasDishwasher ? { icon: amenityIcons.dishwasher, label: t('amenityDishwasher') } : null, diff --git a/app/listings/edit/[id]/page.tsx b/app/listings/edit/[id]/page.tsx index 6ade8cd..528e27b 100644 --- a/app/listings/edit/[id]/page.tsx +++ b/app/listings/edit/[id]/page.tsx @@ -59,6 +59,7 @@ export default function EditListingPage({ params }: { params: { id: string } }) const [hasFreeParking, setHasFreeParking] = useState(false); const [hasSkiPass, setHasSkiPass] = useState(false); const [evChargingAvailable, setEvChargingAvailable] = useState(false); + const [wheelchairAccessible, setWheelchairAccessible] = useState(false); const [calendarUrls, setCalendarUrls] = useState(''); const [selectedImages, setSelectedImages] = useState([]); const [coverImageIndex, setCoverImageIndex] = useState(1); @@ -138,6 +139,7 @@ export default function EditListingPage({ params }: { params: { id: string } }) setHasFreeParking(listing.hasFreeParking); setHasSkiPass(listing.hasSkiPass); setEvChargingAvailable(listing.evChargingAvailable); + setWheelchairAccessible(Boolean(listing.wheelchairAccessible)); setCalendarUrls((listing.calendarUrls || []).join('\n')); if (listing.images?.length) { const coverIdx = listing.images.find((img: any) => img.isCover)?.order ?? 1; @@ -270,6 +272,7 @@ export default function EditListingPage({ params }: { params: { id: string } }) { key: 'parking', label: t('amenityFreeParking'), icon: '🅿️', checked: hasFreeParking, toggle: setHasFreeParking }, { key: 'ski', label: t('amenitySkiPass'), icon: '⛷️', checked: hasSkiPass, toggle: setHasSkiPass }, { key: 'ev', label: t('amenityEvAvailable'), icon: '⚡', checked: evChargingAvailable, toggle: setEvChargingAvailable }, + { key: 'accessible', label: t('amenityWheelchairAccessible'), icon: '♿', checked: wheelchairAccessible, toggle: setWheelchairAccessible }, ]; async function checkSlugAvailability() { @@ -439,7 +442,9 @@ export default function EditListingPage({ params }: { params: { id: string } }) hasBarbecue, hasMicrowave, hasFreeParking, + hasSkiPass, evChargingAvailable, + wheelchairAccessible, coverImageIndex, images: selectedImages.length ? parseImages() : undefined, calendarUrls, diff --git a/app/listings/new/page.tsx b/app/listings/new/page.tsx index b9713cf..4323fb8 100644 --- a/app/listings/new/page.tsx +++ b/app/listings/new/page.tsx @@ -55,6 +55,7 @@ export default function NewListingPage() { const [hasFreeParking, setHasFreeParking] = useState(false); const [hasSkiPass, setHasSkiPass] = useState(false); const [evChargingAvailable, setEvChargingAvailable] = useState(false); + const [wheelchairAccessible, setWheelchairAccessible] = useState(false); const [calendarUrls, setCalendarUrls] = useState(''); const [selectedImages, setSelectedImages] = useState([]); const [coverImageIndex, setCoverImageIndex] = useState(1); @@ -138,6 +139,7 @@ export default function NewListingPage() { { key: 'parking', label: t('amenityFreeParking'), icon: '🅿️', checked: hasFreeParking, toggle: setHasFreeParking }, { key: 'ski', label: t('amenitySkiPass'), icon: '⛷️', checked: hasSkiPass, toggle: setHasSkiPass }, { key: 'ev', label: t('amenityEvAvailable'), icon: '⚡', checked: evChargingAvailable, toggle: setEvChargingAvailable }, + { key: 'accessible', label: t('amenityWheelchairAccessible'), icon: '♿', checked: wheelchairAccessible, toggle: setWheelchairAccessible }, ]; function updateTranslation(locale: Locale, field: keyof LocaleFields, value: string) { @@ -372,7 +374,9 @@ export default function NewListingPage() { hasBarbecue, hasMicrowave, hasFreeParking, + hasSkiPass, evChargingAvailable, + wheelchairAccessible, coverImageIndex, images: parseImages(), calendarUrls, @@ -412,6 +416,9 @@ export default function NewListingPage() { setHasBarbecue(false); setHasMicrowave(false); setHasFreeParking(false); + setHasSkiPass(false); + setEvChargingAvailable(false); + setWheelchairAccessible(false); setRegion(''); setCity(''); setStreetAddress(''); diff --git a/app/listings/page.tsx b/app/listings/page.tsx index 7bd0c50..f5bec3d 100644 --- a/app/listings/page.tsx +++ b/app/listings/page.tsx @@ -31,6 +31,7 @@ type ListingResult = { hasMicrowave: boolean; hasFreeParking: boolean; evChargingAvailable: boolean; + wheelchairAccessible: boolean; hasSkiPass: boolean; maxGuests: number; bedrooms: number; @@ -90,6 +91,7 @@ const amenityIcons: Record = { barbecue: '🍖', microwave: '🍲', parking: '🅿️', + accessible: '♿', ski: '⛷️', ev: '⚡', }; @@ -222,6 +224,7 @@ export default function ListingsIndexPage() { { key: 'barbecue', label: t('amenityBarbecue'), icon: amenityIcons.barbecue }, { key: 'microwave', label: t('amenityMicrowave'), icon: amenityIcons.microwave }, { key: 'parking', label: t('amenityFreeParking'), icon: amenityIcons.parking }, + { key: 'accessible', label: t('amenityWheelchairAccessible'), icon: amenityIcons.accessible }, { key: 'skipass', label: t('amenitySkiPass'), icon: amenityIcons.ski }, { key: 'ev', label: t('amenityEvAvailable'), icon: amenityIcons.ev }, ]; @@ -477,6 +480,7 @@ export default function ListingsIndexPage() { {t('availableForDates')} ) : null} {l.evChargingAvailable ? {t('amenityEvAvailable')} : null} + {l.wheelchairAccessible ? {t('amenityWheelchairAccessible')} : null} {l.hasSkiPass ? {t('amenitySkiPass')} : null} {l.hasAirConditioning ? {t('amenityAirConditioning')} : null} {l.hasKitchen ? {t('amenityKitchen')} : null} diff --git a/lib/i18n.ts b/lib/i18n.ts index d67b7aa..3c65b75 100644 --- a/lib/i18n.ts +++ b/lib/i18n.ts @@ -251,13 +251,14 @@ const baseMessages = { amenityBarbecue: 'Barbecue grill', amenityMicrowave: 'Microwave', amenityFreeParking: 'Free parking', - amenityEvAvailable: 'EV charging nearby', + amenityEvAvailable: 'EV charging', + amenityWheelchairAccessible: 'Wheelchair accessible', amenitySkiPass: 'Ski pass included', - evChargingLabel: 'EV charging nearby', - evChargingYes: 'Charging nearby', - evChargingNo: 'No charging nearby', + evChargingLabel: 'EV charging', + evChargingYes: 'EV charging available', + evChargingNo: 'No EV charging', evChargingAny: 'Any', - evChargingExplain: 'Is there EV charging available on-site or nearby?', + evChargingExplain: 'Is there EV charging available at the property?', capacityGuests: '{count} guests', capacityBedrooms: '{count} bedrooms', capacityBeds: '{count} beds', @@ -567,13 +568,14 @@ const baseMessages = { amenityBarbecue: 'Grilli', amenityMicrowave: 'Mikroaaltouuni', amenityFreeParking: 'Maksuton pysäköinti', - amenityEvAvailable: 'Sähköauton lataus lähellä', + amenityEvAvailable: 'Sähköauton lataus', + amenityWheelchairAccessible: 'Esteetön / pyörätuolilla', amenitySkiPass: 'Hissilippu sisältyy', - evChargingLabel: 'Sähköauton lataus lähellä', - evChargingYes: 'Latausta lähellä', - evChargingNo: 'Ei latausta lähellä', + evChargingLabel: 'Sähköauton lataus', + evChargingYes: 'Latausmahdollisuus', + evChargingNo: 'Ei latausta', evChargingAny: 'Kaikki', - evChargingExplain: 'Onko kohteessa tai lähistöllä sähköauton latausmahdollisuus?', + evChargingExplain: 'Onko kohteessa sähköauton latausmahdollisuus?', capacityGuests: '{count} vierasta', capacityBedrooms: '{count} makuuhuonetta', capacityBeds: '{count} vuodetta', @@ -718,15 +720,16 @@ const svMessages: Record = { priceNotSet: 'Ej angivet', listingPrices: 'Priser', capacityUnknown: 'Kapacitet ej angiven', - amenityEvAvailable: 'EV-laddning i närheten', + amenityEvAvailable: 'EV-laddning', + amenityWheelchairAccessible: 'Rullstolsanpassat', amenitySkiPass: 'Liftkort ingår', amenityMicrowave: 'Mikrovågsugn', amenityFreeParking: 'Gratis parkering', - evChargingLabel: 'EV-laddning i närheten', - evChargingYes: 'Laddning i närheten', - evChargingNo: 'Ingen laddning i närheten', + evChargingLabel: 'EV-laddning', + evChargingYes: 'EV-laddning finns', + evChargingNo: 'Ingen EV-laddning', evChargingAny: 'Alla', - evChargingExplain: 'Finns det EV-laddning på plats eller i närheten?', + evChargingExplain: 'Finns det EV-laddning på plats?', footerCookieNotice: 'Vi använder endast nödvändiga cookies för inloggning och säkerhet. Genom att använda sajten godkänner du cookies; om du inte gör det, använd inte webbplatsen.', }; diff --git a/prisma/migrations/20251217_accessibility_amenity/migration.sql b/prisma/migrations/20251217_accessibility_amenity/migration.sql new file mode 100644 index 0000000..ae36528 --- /dev/null +++ b/prisma/migrations/20251217_accessibility_amenity/migration.sql @@ -0,0 +1,3 @@ +-- Add wheelchair accessibility amenity +ALTER TABLE "Listing" ADD COLUMN IF NOT EXISTS "wheelchairAccessible" BOOLEAN NOT NULL DEFAULT false; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0c6fe37..832ee5e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -90,6 +90,7 @@ model Listing { hasFreeParking Boolean @default(false) hasSkiPass Boolean @default(false) evChargingAvailable Boolean @default(false) + wheelchairAccessible Boolean @default(false) calendarUrls String[] @db.Text @default([]) priceWeekdayEuros Int? priceWeekendEuros Int? diff --git a/prisma/seed.js b/prisma/seed.js index 05a3df0..9e3ed23 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -517,7 +517,7 @@ async function main() { hasFreeParking: true, petsAllowed: false, byTheLake: false, - evCharging: 'FREE', + evChargingAvailable: true, priceWeekdayEuros: 105, priceWeekendEuros: 120, cover: { @@ -593,6 +593,7 @@ async function main() { hasMicrowave: item.hasMicrowave ?? randBool(0.7), hasFreeParking: item.hasFreeParking ?? randBool(0.6), evChargingAvailable: item.evChargingAvailable ?? randBool(0.4), + wheelchairAccessible: item.wheelchairAccessible ?? randBool(0.25), hasSkiPass: item.hasSkiPass ?? randBool(0.2), }; }); @@ -631,7 +632,8 @@ async function main() { hasFreeParking: item.hasFreeParking ?? false, petsAllowed: item.petsAllowed, byTheLake: item.byTheLake, - evCharging: item.evCharging, + evChargingAvailable: item.evChargingAvailable ?? false, + wheelchairAccessible: item.wheelchairAccessible ?? false, priceWeekdayEuros: item.priceWeekdayEuros, priceWeekendEuros: item.priceWeekendEuros, contactName: 'Sample Host', @@ -681,7 +683,8 @@ async function main() { hasFreeParking: item.hasFreeParking ?? false, petsAllowed: item.petsAllowed, byTheLake: item.byTheLake, - evCharging: item.evCharging, + evChargingAvailable: item.evChargingAvailable ?? false, + wheelchairAccessible: item.wheelchairAccessible ?? false, priceWeekdayEuros: item.priceWeekdayEuros, priceWeekendEuros: item.priceWeekendEuros, contactName: 'Sample Host',