diff --git a/PROGRESS.md b/PROGRESS.md index f23072b..7df2037 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -47,6 +47,7 @@ - Soft rejection/removal states for users/listings with timestamps; owner listing removal; login redirects home; listing visibility hides removed/not-published. - Profile page now allows editing name and password (email immutable). - Docs: Added docs in `docs/` (tracked, not shipped) with HTML + PlantUML sequences + draw.io diagrams. Ignored from deploy via runtime paths; kept in git. +- Listing details: right rail now surfaces quick facts + amenity icons; browse map given fixed height so OpenStreetMap tiles show reliably; footer links to privacy page with version indicator. To resume: 1) If desired, render diagrams locally: PlantUML in `docs/plantuml`, draw.io in `docs/drawio`. diff --git a/app/globals.css b/app/globals.css index 2825333..30dd59f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -210,6 +210,7 @@ p { .map-frame { position: relative; width: 100%; + height: 420px; min-height: 360px; border-radius: 12px; overflow: hidden; @@ -250,6 +251,51 @@ p { gap: 16px; } +.listing-layout { + display: grid; + gap: 16px; + grid-template-columns: 2fr 1fr; +} + +.listing-main { + display: grid; + gap: 12px; +} + +.listing-aside { + display: grid; + gap: 10px; + align-self: start; + position: sticky; + top: 24px; +} + +.fact-row { + display: grid; + grid-template-columns: 28px 1fr; + gap: 10px; + align-items: start; +} + +.amenity-list { + display: grid; + gap: 8px; +} + +.amenity-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid rgba(148, 163, 184, 0.25); + background: rgba(255, 255, 255, 0.02); +} + +.amenity-icon { + font-size: 18px; +} + .breadcrumb { color: var(--muted); font-size: 14px; @@ -360,4 +406,12 @@ textarea:focus { .latest-card { grid-template-columns: 1fr; } + + .listing-layout { + grid-template-columns: 1fr; + } + + .listing-aside { + position: static; + } } diff --git a/app/listings/[slug]/page.tsx b/app/listings/[slug]/page.tsx index 437282d..b44bf6d 100644 --- a/app/listings/[slug]/page.tsx +++ b/app/listings/[slug]/page.tsx @@ -40,79 +40,98 @@ export default async function ListingPage({ params }: ListingPageProps) { } const { listing, title, description, teaser, locale: translationLocale } = translation; + const amenities = [ + listing.hasSauna ? { icon: amenityIcons.sauna, label: t('amenitySauna') } : null, + listing.hasFireplace ? { icon: amenityIcons.fireplace, label: t('amenityFireplace') } : null, + listing.hasWifi ? { icon: amenityIcons.wifi, label: t('amenityWifi') } : null, + 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.evCharging === 'FREE' ? { icon: amenityIcons.ev, label: t('amenityEvFree') } : null, + listing.evCharging === 'PAID' ? { icon: amenityIcons.ev, label: t('amenityEvPaid') } : null, + ].filter(Boolean) as { icon: string; label: string }[]; + const addressLine = `${listing.streetAddress ? `${listing.streetAddress}, ` : ''}${listing.city}, ${listing.region}, ${listing.country}`; + const capacityLine = `${t('capacityGuests', { count: listing.maxGuests })} · ${t('capacityBedrooms', { count: listing.bedrooms })} · ${t('capacityBeds', { count: listing.beds })} · ${t('capacityBathrooms', { count: listing.bathrooms })}`; + const contactLine = `${listing.contactName} · ${listing.contactEmail}${listing.contactPhone ? ` · ${listing.contactPhone}` : ''}`; return (
{t('homeCrumb')} / {params.slug}
-
-

{title}

-

{teaser ?? description}

-
- {t('listingAddress')}: {listing.streetAddress ? `${listing.streetAddress}, ` : ''} - {listing.city}, {listing.region}, {listing.country} -
- {listing.addressNote ? ( -
- {listing.addressNote} -
- ) : null} -
- {t('listingCapacity')}: {t('capacityGuests', { count: listing.maxGuests })} - {t('capacityBedrooms', { count: listing.bedrooms })} -{' '} - {t('capacityBeds', { count: listing.beds })} - {t('capacityBathrooms', { count: listing.bathrooms })} -
-
- {t('listingAmenities')}: -
- {listing.hasSauna ? {amenityIcons.sauna} {t('amenitySauna')} : null} - {listing.hasFireplace ? {amenityIcons.fireplace} {t('amenityFireplace')} : null} - {listing.hasWifi ? {amenityIcons.wifi} {t('amenityWifi')} : null} - {listing.petsAllowed ? {amenityIcons.pets} {t('amenityPets')} : null} - {listing.byTheLake ? {amenityIcons.lake} {t('amenityLake')} : null} - {listing.hasAirConditioning ? {amenityIcons.ac} {t('amenityAirConditioning')} : null} - {listing.evCharging === 'FREE' ? {amenityIcons.ev} {t('amenityEvFree')} : null} - {listing.evCharging === 'PAID' ? {amenityIcons.ev} {t('amenityEvPaid')} : null} - {!( - listing.hasSauna || - listing.hasFireplace || - listing.hasWifi || - listing.petsAllowed || - listing.byTheLake || - listing.hasAirConditioning || - listing.evCharging !== 'NONE' - ) - ? - - : null} -
-
-
- {t('listingContact')}: {listing.contactName} - {listing.contactEmail} - {listing.contactPhone ? ` - ${listing.contactPhone}` : ''} +
+
+

{title}

+

{teaser ?? description}

+ {listing.addressNote ? ( +
+ {listing.addressNote} +
+ ) : null} {listing.externalUrl ? ( - <> - {' - '} - + ) : null} -
- {listing.images.length > 0 ? ( -
- {listing.images.map((img) => ( -
- {img.altText - {img.altText ? ( -
{img.altText}
- ) : null} -
- ))} + {listing.images.length > 0 ? ( +
+ {listing.images.map((img) => ( +
+ {img.altText + {img.altText ? ( +
{img.altText}
+ ) : null} +
+ ))} +
+ ) : null} +
+ {t('localeLabel')}: {translationLocale}
- ) : null} -
- {t('localeLabel')}: {translationLocale}
+
); diff --git a/lib/i18n.ts b/lib/i18n.ts index fcd0f26..c58a2f6 100644 --- a/lib/i18n.ts +++ b/lib/i18n.ts @@ -109,6 +109,7 @@ const allMessages = { listingAddress: 'Address', listingCapacity: 'Capacity', listingAmenities: 'Amenities', + listingNoAmenities: 'No amenities listed yet.', listingContact: 'Contact', listingMoreInfo: 'More info', localeLabel: 'Locale', @@ -289,6 +290,7 @@ const allMessages = { listingAddress: 'Osoite', listingCapacity: 'Tilat', listingAmenities: 'Varustelu', + listingNoAmenities: 'Varustelua ei ole listattu.', listingContact: 'Yhteystiedot', listingMoreInfo: 'Lisätietoja', localeLabel: 'Kieli',