From 928c2f9bb9cc0068e0041d63616a34abe31c6cbe Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Thu, 18 Dec 2025 13:23:56 +0200 Subject: [PATCH 1/2] ui: clarify listing prices as starting-from --- PROGRESS.md | 1 + app/listings/[slug]/page.tsx | 12 +++++++++--- app/listings/page.tsx | 9 +++++++-- lib/i18n.ts | 27 +++++++++++++++------------ 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index f842433..be8a5ce 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -88,3 +88,4 @@ - 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 separate EV charging flags (on-site vs nearby) plus wheelchair accessibility, including browse filters and admin approvals view badges. - Navbar: combined admin actions (approvals/users/monitoring) under a single “Admin” dropdown menu. +- Pricing copy: treat listing prices as indicative “starting from” values and show starting-from line on browse cards. diff --git a/app/listings/[slug]/page.tsx b/app/listings/[slug]/page.tsx index 4b9032b..f418f82 100644 --- a/app/listings/[slug]/page.tsx +++ b/app/listings/[slug]/page.tsx @@ -114,11 +114,17 @@ export default async function ListingPage({ params }: ListingPageProps) { const capacityLine = capacityParts.length ? capacityParts.join(' · ') : t('capacityUnknown'); const contactLine = `${listing.contactName} · ${listing.contactEmail}${listing.contactPhone ? ` · ${listing.contactPhone}` : ''}`; const coverImage = listing.images.find((img) => img.isCover) ?? listing.images[0] ?? null; + const priceCandidates = [listing.priceWeekdayEuros, listing.priceWeekendEuros].filter((p): p is number => typeof p === 'number'); + const startingFromEuros = priceCandidates.length ? Math.min(...priceCandidates) : null; const priceLine = listing.priceWeekdayEuros || listing.priceWeekendEuros - ? [listing.priceWeekdayEuros ? t('priceWeekdayShort', { price: listing.priceWeekdayEuros }) : null, listing.priceWeekendEuros ? t('priceWeekendShort', { price: listing.priceWeekendEuros }) : null] - .filter(Boolean) - .join(' · ') + ? `${startingFromEuros !== null ? t('priceStartingFromShort', { price: startingFromEuros }) : ''}${ + listing.priceWeekdayEuros || listing.priceWeekendEuros + ? ` (${[listing.priceWeekdayEuros ? t('priceWeekdayShort', { price: listing.priceWeekdayEuros }) : null, listing.priceWeekendEuros ? t('priceWeekendShort', { price: listing.priceWeekendEuros }) : null] + .filter(Boolean) + .join(' · ')})` + : '' + }` : t('priceNotSet'); const isDraftOrPending = listing.status !== ListingStatus.PUBLISHED; const isOwnerView = viewerId && listing.ownerId === viewerId; diff --git a/app/listings/page.tsx b/app/listings/page.tsx index e82908e..75a6c39 100644 --- a/app/listings/page.tsx +++ b/app/listings/page.tsx @@ -469,13 +469,18 @@ export default function ListingsIndexPage() { ) : null}

{l.teaser ?? ''}

+ {l.priceWeekdayEuros || l.priceWeekendEuros ? ( +
+ {t('priceStartingFromShort', { + price: Math.min(...([l.priceWeekdayEuros, l.priceWeekendEuros].filter((p): p is number => typeof p === 'number'))), + })} +
+ ) : null}
{l.streetAddress ? `${l.streetAddress}, ` : ''} {l.city}, {l.region}
- {l.priceWeekdayEuros ? {t('priceWeekdayShort', { price: l.priceWeekdayEuros })} : null} - {l.priceWeekendEuros ? {t('priceWeekendShort', { price: l.priceWeekendEuros })} : null} {t('capacityGuests', { count: l.maxGuests })} {t('capacityBedrooms', { count: l.bedrooms })} {l.hasCalendar ? {t('calendarConnected')} : null} diff --git a/lib/i18n.ts b/lib/i18n.ts index 6f4fa66..d6450c5 100644 --- a/lib/i18n.ts +++ b/lib/i18n.ts @@ -153,7 +153,7 @@ const baseMessages = { listingLocation: 'Location', listingAddress: 'Address', listingCapacity: 'Capacity', - listingPrices: 'Pricing', + listingPrices: 'Price (starting from)', listingAmenities: 'Amenities', listingNoAmenities: 'No amenities listed yet.', listingContact: 'Contact', @@ -218,9 +218,10 @@ const baseMessages = { bedroomsLabel: 'Bedrooms', bedsLabel: 'Beds', bathroomsLabel: 'Bathrooms', - priceWeekdayLabel: 'Weeknight price (€ / night)', - priceWeekendLabel: 'Weekend price (€ / night)', - priceHintHelp: 'Set separate weeknight and weekend prices in euros (optional, not a binding offer).', + priceWeekdayLabel: 'Price starting from (weeknight, € / night)', + priceWeekendLabel: 'Price starting from (weekend, € / night)', + priceHintHelp: 'These prices are indicative only (starting from), not a binding offer.', + priceStartingFromShort: 'Starting from {price}€ / night', priceWeekdayShort: '{price}€ weekday', priceWeekendShort: '{price}€ weekend', priceNotSet: 'Not provided', @@ -500,7 +501,7 @@ const baseMessages = { listingLocation: 'Sijainti', listingAddress: 'Osoite', listingCapacity: 'Tilat', - listingPrices: 'Hinta', + listingPrices: 'Hinta (alkaen)', listingAmenities: 'Varustelu', listingNoAmenities: 'Varustelua ei ole listattu.', listingContact: 'Yhteystiedot', @@ -538,9 +539,10 @@ const baseMessages = { bedroomsLabel: 'Makuuhuoneita', bedsLabel: 'Vuoteita', bathroomsLabel: 'Kylpyhuoneita', - priceWeekdayLabel: 'Arkiyön hinta (€ / yö)', - priceWeekendLabel: 'Viikonlopun hinta (€ / yö)', - priceHintHelp: 'Aseta erilliset hinnat arki- ja viikonloppuyöille euroissa (valinnainen, ei sitova).', + priceWeekdayLabel: 'Hinta alkaen (arki, € / yö)', + priceWeekendLabel: 'Hinta alkaen (viikonloppu, € / yö)', + priceHintHelp: 'Hinnat ovat suuntaa-antavia (alkaen), eivät sitovia.', + priceStartingFromShort: 'Alkaen {price}€ / yö', priceWeekdayShort: '{price}€ arki', priceWeekendShort: '{price}€ viikonloppu', priceNotSet: 'Ei ilmoitettu', @@ -719,13 +721,14 @@ const svMessages: Record = { slugTaken: 'Sluggen används redan', slugCheckError: 'Kunde inte kontrollera sluggen nu', teaserHelp: 'Kort ingress som syns i korten', - priceWeekdayLabel: 'Vardagspris (€ / natt)', - priceWeekendLabel: 'Helgpris (€ / natt)', - priceHintHelp: 'Ange separata priser för vardag och helg i euro per natt (frivilligt).', + priceWeekdayLabel: 'Pris från (vardag, € / natt)', + priceWeekendLabel: 'Pris från (helg, € / natt)', + priceHintHelp: 'Priserna är endast vägledande (från), inte ett bindande erbjudande.', + priceStartingFromShort: 'Från {price}€ / natt', priceWeekdayShort: '{price}€ vardag', priceWeekendShort: '{price}€ helg', priceNotSet: 'Ej angivet', - listingPrices: 'Priser', + listingPrices: 'Pris (från)', capacityUnknown: 'Kapacitet ej angiven', amenityEvAvailable: 'EV-laddning i närheten', amenityEvNearby: 'EV-laddning i närheten', -- 2.45.3 From b03743dde69cecfc058ed0d4ab7dfa210d1d7b13 Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Thu, 18 Dec 2025 13:30:48 +0200 Subject: [PATCH 2/2] ui: show starting-from price in latest carousel Fixes issue #33 https://redmine.halla-aho.net/issues/33 --- PROGRESS.md | 2 +- app/page.tsx | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/PROGRESS.md b/PROGRESS.md index be8a5ce..7ec40fa 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -88,4 +88,4 @@ - 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 separate EV charging flags (on-site vs nearby) plus wheelchair accessibility, including browse filters and admin approvals view badges. - Navbar: combined admin actions (approvals/users/monitoring) under a single “Admin” dropdown menu. -- Pricing copy: treat listing prices as indicative “starting from” values and show starting-from line on browse cards. +- Pricing copy: treat listing prices as indicative “starting from” values and show starting-from line on browse cards + home latest carousel. diff --git a/app/page.tsx b/app/page.tsx index 1d3147b..945f05d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -13,6 +13,8 @@ type LatestListing = { city: string; region: string; isSample: boolean; + priceWeekdayEuros: number | null; + priceWeekendEuros: number | null; }; export const dynamic = 'force-dynamic'; @@ -98,6 +100,15 @@ export default function HomePage() {

{item.title}

{item.teaser}

+ {item.priceWeekdayEuros || item.priceWeekendEuros ? ( +
+ {t('priceStartingFromShort', { + price: Math.min( + ...([item.priceWeekdayEuros, item.priceWeekendEuros].filter((p): p is number => typeof p === 'number')), + ), + })} +
+ ) : null}
{t('openListing')} -- 2.45.3