Merged in feat/SW-1454-hotel-listing-city-page (pull request #1250)
feat(SW-1454): added hotel listing * feat(SW-1454): added hotel listing Approved-by: Fredrik Thorsson
This commit is contained in:
@@ -19,7 +19,9 @@ export default async function CityListing({ cities }: CityListingProps) {
|
||||
<div className={styles.listHeader}>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{ id: `{count} Locations` },
|
||||
{
|
||||
id: `{count, plural, one {{count} Location} other {{count} Locations}}`,
|
||||
},
|
||||
{ count: cities.length }
|
||||
)}
|
||||
</Subtitle>
|
||||
|
||||
+3
-1
@@ -16,7 +16,9 @@
|
||||
.mainSection {
|
||||
grid-area: mainSection;
|
||||
padding-bottom: var(--Spacing-x7);
|
||||
min-height: 500px; /* This is a temporary value because of no content atm */
|
||||
max-width: var(--max-width-page);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
||||
@@ -7,9 +7,9 @@ import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/Bread
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import ExperienceList from "../ExperienceList"
|
||||
import HotelListing from "../HotelListing"
|
||||
import SidebarContentWrapper from "../SidebarContentWrapper"
|
||||
import DestinationPageSidePeek from "../Sidepeek"
|
||||
import StaticMap from "../StaticMap"
|
||||
@@ -20,16 +20,13 @@ import styles from "./destinationCityPage.module.css"
|
||||
import { PageContentTypeEnum } from "@/types/requests/contentType"
|
||||
|
||||
export default async function DestinationCityPage() {
|
||||
const [intl, pageData] = await Promise.all([
|
||||
getIntl(),
|
||||
getDestinationCityPage(),
|
||||
])
|
||||
const pageData = await getDestinationCityPage()
|
||||
|
||||
if (!pageData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { tracking, destinationCityPage } = pageData
|
||||
const { tracking, destinationCityPage, hotels } = pageData
|
||||
const {
|
||||
images,
|
||||
heading,
|
||||
@@ -55,8 +52,7 @@ export default async function DestinationCityPage() {
|
||||
/>
|
||||
</header>
|
||||
<main className={styles.mainSection}>
|
||||
{/* TODO: Add hotel listing by cityIdentifier */}
|
||||
{">>>> MAIN CONTENT <<<<"}
|
||||
<HotelListing hotels={hotels} />
|
||||
</main>
|
||||
<aside className={styles.sidebar}>
|
||||
<SidebarContentWrapper>
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
.container {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
align-content: start;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.intro {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.captions {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.amenityList {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
flex-wrap: wrap;
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Caption-Underline-fontSize);
|
||||
}
|
||||
|
||||
.amenityItem {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ctaWrapper {
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(250px, 350px) auto;
|
||||
}
|
||||
|
||||
.ctaWrapper {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||
import HotelLogo from "@/components/Icons/Logos"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
import { getSingleDecimal } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./hotelListingItem.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
|
||||
interface HotelListingItemProps {
|
||||
hotel: Hotel
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export default function HotelListingItem({
|
||||
hotel,
|
||||
url,
|
||||
}: HotelListingItemProps) {
|
||||
const intl = useIntl()
|
||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||
const amenities = hotel.detailedFacilities.slice(0, 5)
|
||||
|
||||
return (
|
||||
<article className={styles.container}>
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
title={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: hotel.name }
|
||||
)}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.intro}>
|
||||
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||
<Subtitle type="one" asChild>
|
||||
<h3>{hotel.name}</h3>
|
||||
</Subtitle>
|
||||
<div className={styles.captions}>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{hotel.address.streetAddress}
|
||||
</Caption>
|
||||
<Divider variant="vertical" color="beige" />
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "{number} km to city center" },
|
||||
{
|
||||
number: getSingleDecimal(
|
||||
hotel.location.distanceToCentre / 1000
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<Body>{hotel.hotelContent.texts.descriptions.short}</Body>
|
||||
<ul className={styles.amenityList}>
|
||||
{amenities.map((amenity) => {
|
||||
const IconComponent = mapFacilityToIcon(amenity.id)
|
||||
return (
|
||||
<li className={styles.amenityItem} key={amenity.id}>
|
||||
{IconComponent && (
|
||||
<IconComponent color="grey80" width={20} height={20} />
|
||||
)}
|
||||
{amenity.name}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<Button intent="text" variant="icon" theme="base">
|
||||
{intl.formatMessage({ id: "See on map" })}
|
||||
<ChevronRightSmallIcon />
|
||||
</Button>
|
||||
{url && (
|
||||
<>
|
||||
<Divider variant="horizontal" color="primaryLightSubtle" />
|
||||
|
||||
<div className={styles.ctaWrapper}>
|
||||
<Button intent="tertiary" theme="base" size="small" asChild>
|
||||
<Link href={url}>
|
||||
{intl.formatMessage({ id: "See hotel details" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
.container {
|
||||
--scroll-margin-top: calc(
|
||||
var(--booking-widget-mobile-height) + var(--Spacing-x2)
|
||||
);
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
scroll-margin-top: var(--scroll-margin-top);
|
||||
}
|
||||
|
||||
.listHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hotelList {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.hotelList:not(.allVisible) li:nth-child(n + 6) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
--scroll-margin-top: calc(
|
||||
var(--booking-widget-desktop-height) + var(--Spacing-x2)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client"
|
||||
|
||||
import { useRef, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||
|
||||
import HotelListingItem from "./HotelListingItem"
|
||||
|
||||
import styles from "./hotelListing.module.css"
|
||||
|
||||
import type { HotelData } from "@/types/hotel"
|
||||
|
||||
interface HotelListingProps {
|
||||
hotels: (HotelData & { url: string | null })[]
|
||||
}
|
||||
|
||||
export default function HotelListing({ hotels }: HotelListingProps) {
|
||||
const intl = useIntl()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const showToggleButton = hotels.length > 5
|
||||
const [allHotelsVisible, setAllHotelsVisible] = useState(!showToggleButton)
|
||||
const { showBackToTop, scrollToTop } = useScrollToTop({
|
||||
threshold: 300,
|
||||
elementRef: scrollRef,
|
||||
})
|
||||
|
||||
function handleShowMore() {
|
||||
if (scrollRef.current && allHotelsVisible) {
|
||||
scrollRef.current.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
setAllHotelsVisible((state) => !state)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.container} ref={scrollRef}>
|
||||
<div className={styles.listHeader}>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: `{count, plural, one {{count} Hotel} other {{count} Hotels}}`,
|
||||
},
|
||||
{ count: hotels.length }
|
||||
)}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<ul
|
||||
className={`${styles.hotelList} ${allHotelsVisible ? styles.allVisible : ""}`}
|
||||
>
|
||||
{hotels.map(({ hotel, url }) => (
|
||||
<li key={hotel.name}>
|
||||
<HotelListingItem hotel={hotel} url={url} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{showToggleButton ? (
|
||||
<ShowMoreButton
|
||||
loadMoreData={handleShowMore}
|
||||
showLess={allHotelsVisible}
|
||||
textShowMore={intl.formatMessage({
|
||||
id: "Show more",
|
||||
})}
|
||||
textShowLess={intl.formatMessage({
|
||||
id: "Show less",
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
{showBackToTop && (
|
||||
<BackToTopButton position="center" onClick={scrollToTop} />
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user