ui: clarify listing prices as starting-from
This commit is contained in:
parent
835f47779e
commit
928c2f9bb9
4 changed files with 32 additions and 17 deletions
|
|
@ -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/`.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -114,11 +114,17 @@ export default async function ListingPage({ params }: ListingPageProps) {
|
||||||
const capacityLine = capacityParts.length ? capacityParts.join(' · ') : t('capacityUnknown');
|
const capacityLine = capacityParts.length ? capacityParts.join(' · ') : t('capacityUnknown');
|
||||||
const contactLine = `${listing.contactName} · ${listing.contactEmail}${listing.contactPhone ? ` · ${listing.contactPhone}` : ''}`;
|
const contactLine = `${listing.contactName} · ${listing.contactEmail}${listing.contactPhone ? ` · ${listing.contactPhone}` : ''}`;
|
||||||
const coverImage = listing.images.find((img) => img.isCover) ?? listing.images[0] ?? null;
|
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 =
|
const priceLine =
|
||||||
listing.priceWeekdayEuros || listing.priceWeekendEuros
|
listing.priceWeekdayEuros || listing.priceWeekendEuros
|
||||||
? [listing.priceWeekdayEuros ? t('priceWeekdayShort', { price: listing.priceWeekdayEuros }) : null, listing.priceWeekendEuros ? t('priceWeekendShort', { price: listing.priceWeekendEuros }) : null]
|
? `${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)
|
.filter(Boolean)
|
||||||
.join(' · ')
|
.join(' · ')})`
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
: t('priceNotSet');
|
: t('priceNotSet');
|
||||||
const isDraftOrPending = listing.status !== ListingStatus.PUBLISHED;
|
const isDraftOrPending = listing.status !== ListingStatus.PUBLISHED;
|
||||||
const isOwnerView = viewerId && listing.ownerId === viewerId;
|
const isOwnerView = viewerId && listing.ownerId === viewerId;
|
||||||
|
|
|
||||||
|
|
@ -469,13 +469,18 @@ export default function ListingsIndexPage() {
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<p style={{ margin: 0 }}>{l.teaser ?? ''}</p>
|
<p style={{ margin: 0 }}>{l.teaser ?? ''}</p>
|
||||||
|
{l.priceWeekdayEuros || l.priceWeekendEuros ? (
|
||||||
|
<div style={{ color: '#cbd5e1', fontSize: 14 }}>
|
||||||
|
{t('priceStartingFromShort', {
|
||||||
|
price: Math.min(...([l.priceWeekdayEuros, l.priceWeekendEuros].filter((p): p is number => typeof p === 'number'))),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div style={{ color: '#cbd5e1', fontSize: 14 }}>
|
<div style={{ color: '#cbd5e1', fontSize: 14 }}>
|
||||||
{l.streetAddress ? `${l.streetAddress}, ` : ''}
|
{l.streetAddress ? `${l.streetAddress}, ` : ''}
|
||||||
{l.city}, {l.region}
|
{l.city}, {l.region}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', fontSize: 13 }}>
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', fontSize: 13 }}>
|
||||||
{l.priceWeekdayEuros ? <span className="badge">{t('priceWeekdayShort', { price: l.priceWeekdayEuros })}</span> : null}
|
|
||||||
{l.priceWeekendEuros ? <span className="badge">{t('priceWeekendShort', { price: l.priceWeekendEuros })}</span> : null}
|
|
||||||
<span className="badge">{t('capacityGuests', { count: l.maxGuests })}</span>
|
<span className="badge">{t('capacityGuests', { count: l.maxGuests })}</span>
|
||||||
<span className="badge">{t('capacityBedrooms', { count: l.bedrooms })}</span>
|
<span className="badge">{t('capacityBedrooms', { count: l.bedrooms })}</span>
|
||||||
{l.hasCalendar ? <span className="badge secondary">{t('calendarConnected')}</span> : null}
|
{l.hasCalendar ? <span className="badge secondary">{t('calendarConnected')}</span> : null}
|
||||||
|
|
|
||||||
27
lib/i18n.ts
27
lib/i18n.ts
|
|
@ -153,7 +153,7 @@ const baseMessages = {
|
||||||
listingLocation: 'Location',
|
listingLocation: 'Location',
|
||||||
listingAddress: 'Address',
|
listingAddress: 'Address',
|
||||||
listingCapacity: 'Capacity',
|
listingCapacity: 'Capacity',
|
||||||
listingPrices: 'Pricing',
|
listingPrices: 'Price (starting from)',
|
||||||
listingAmenities: 'Amenities',
|
listingAmenities: 'Amenities',
|
||||||
listingNoAmenities: 'No amenities listed yet.',
|
listingNoAmenities: 'No amenities listed yet.',
|
||||||
listingContact: 'Contact',
|
listingContact: 'Contact',
|
||||||
|
|
@ -218,9 +218,10 @@ const baseMessages = {
|
||||||
bedroomsLabel: 'Bedrooms',
|
bedroomsLabel: 'Bedrooms',
|
||||||
bedsLabel: 'Beds',
|
bedsLabel: 'Beds',
|
||||||
bathroomsLabel: 'Bathrooms',
|
bathroomsLabel: 'Bathrooms',
|
||||||
priceWeekdayLabel: 'Weeknight price (€ / night)',
|
priceWeekdayLabel: 'Price starting from (weeknight, € / night)',
|
||||||
priceWeekendLabel: 'Weekend price (€ / night)',
|
priceWeekendLabel: 'Price starting from (weekend, € / night)',
|
||||||
priceHintHelp: 'Set separate weeknight and weekend prices in euros (optional, not a binding offer).',
|
priceHintHelp: 'These prices are indicative only (starting from), not a binding offer.',
|
||||||
|
priceStartingFromShort: 'Starting from {price}€ / night',
|
||||||
priceWeekdayShort: '{price}€ weekday',
|
priceWeekdayShort: '{price}€ weekday',
|
||||||
priceWeekendShort: '{price}€ weekend',
|
priceWeekendShort: '{price}€ weekend',
|
||||||
priceNotSet: 'Not provided',
|
priceNotSet: 'Not provided',
|
||||||
|
|
@ -500,7 +501,7 @@ const baseMessages = {
|
||||||
listingLocation: 'Sijainti',
|
listingLocation: 'Sijainti',
|
||||||
listingAddress: 'Osoite',
|
listingAddress: 'Osoite',
|
||||||
listingCapacity: 'Tilat',
|
listingCapacity: 'Tilat',
|
||||||
listingPrices: 'Hinta',
|
listingPrices: 'Hinta (alkaen)',
|
||||||
listingAmenities: 'Varustelu',
|
listingAmenities: 'Varustelu',
|
||||||
listingNoAmenities: 'Varustelua ei ole listattu.',
|
listingNoAmenities: 'Varustelua ei ole listattu.',
|
||||||
listingContact: 'Yhteystiedot',
|
listingContact: 'Yhteystiedot',
|
||||||
|
|
@ -538,9 +539,10 @@ const baseMessages = {
|
||||||
bedroomsLabel: 'Makuuhuoneita',
|
bedroomsLabel: 'Makuuhuoneita',
|
||||||
bedsLabel: 'Vuoteita',
|
bedsLabel: 'Vuoteita',
|
||||||
bathroomsLabel: 'Kylpyhuoneita',
|
bathroomsLabel: 'Kylpyhuoneita',
|
||||||
priceWeekdayLabel: 'Arkiyön hinta (€ / yö)',
|
priceWeekdayLabel: 'Hinta alkaen (arki, € / yö)',
|
||||||
priceWeekendLabel: 'Viikonlopun hinta (€ / yö)',
|
priceWeekendLabel: 'Hinta alkaen (viikonloppu, € / yö)',
|
||||||
priceHintHelp: 'Aseta erilliset hinnat arki- ja viikonloppuyöille euroissa (valinnainen, ei sitova).',
|
priceHintHelp: 'Hinnat ovat suuntaa-antavia (alkaen), eivät sitovia.',
|
||||||
|
priceStartingFromShort: 'Alkaen {price}€ / yö',
|
||||||
priceWeekdayShort: '{price}€ arki',
|
priceWeekdayShort: '{price}€ arki',
|
||||||
priceWeekendShort: '{price}€ viikonloppu',
|
priceWeekendShort: '{price}€ viikonloppu',
|
||||||
priceNotSet: 'Ei ilmoitettu',
|
priceNotSet: 'Ei ilmoitettu',
|
||||||
|
|
@ -719,13 +721,14 @@ const svMessages: Record<keyof typeof baseMessages.en, string> = {
|
||||||
slugTaken: 'Sluggen används redan',
|
slugTaken: 'Sluggen används redan',
|
||||||
slugCheckError: 'Kunde inte kontrollera sluggen nu',
|
slugCheckError: 'Kunde inte kontrollera sluggen nu',
|
||||||
teaserHelp: 'Kort ingress som syns i korten',
|
teaserHelp: 'Kort ingress som syns i korten',
|
||||||
priceWeekdayLabel: 'Vardagspris (€ / natt)',
|
priceWeekdayLabel: 'Pris från (vardag, € / natt)',
|
||||||
priceWeekendLabel: 'Helgpris (€ / natt)',
|
priceWeekendLabel: 'Pris från (helg, € / natt)',
|
||||||
priceHintHelp: 'Ange separata priser för vardag och helg i euro per natt (frivilligt).',
|
priceHintHelp: 'Priserna är endast vägledande (från), inte ett bindande erbjudande.',
|
||||||
|
priceStartingFromShort: 'Från {price}€ / natt',
|
||||||
priceWeekdayShort: '{price}€ vardag',
|
priceWeekdayShort: '{price}€ vardag',
|
||||||
priceWeekendShort: '{price}€ helg',
|
priceWeekendShort: '{price}€ helg',
|
||||||
priceNotSet: 'Ej angivet',
|
priceNotSet: 'Ej angivet',
|
||||||
listingPrices: 'Priser',
|
listingPrices: 'Pris (från)',
|
||||||
capacityUnknown: 'Kapacitet ej angiven',
|
capacityUnknown: 'Kapacitet ej angiven',
|
||||||
amenityEvAvailable: 'EV-laddning i närheten',
|
amenityEvAvailable: 'EV-laddning i närheten',
|
||||||
amenityEvNearby: 'EV-laddning i närheten',
|
amenityEvNearby: 'EV-laddning i närheten',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue