Merged in feat/SW-1062-parking-page (pull request #1242)

Feat/SW-1062 parking page

* feat(SW-1062): Added parking sub page


Approved-by: Christian Andolf
Approved-by: Fredrik Thorsson
This commit is contained in:
Erik Tiekstra
2025-02-04 11:11:51 +00:00
parent 2e311be924
commit c4eafa419b
21 changed files with 393 additions and 87 deletions

View File

@@ -1,18 +1,12 @@
import { parkingSubPage } from "@/constants/routes/hotelSubpages"
import { OpenInNewIcon } from "@/components/Icons"
import ParkingInformation from "@/components/ParkingInformation"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import ParkingList from "./ParkingList"
import ParkingPrices from "./ParkingPrices"
import styles from "./parkingAmenity.module.css"
import type { ParkingAmenityProps } from "@/types/components/hotelPage/sidepeek/parking"
@@ -36,58 +30,7 @@ export default async function ParkingAmenity({
<div className={styles.wrapper}>
{parkingElevatorPitch}
{parking.map((data) => (
<div key={data.type} className={styles.information}>
<div className={styles.list}>
<Body textTransform="bold">{data.type}</Body>
<ParkingList
numberOfChargingSpaces={data.numberOfChargingSpaces}
canMakeReservation={data.canMakeReservation}
numberOfParkingSpots={data.numberOfParkingSpots}
distanceToHotel={data.distanceToHotel}
address={data.address}
/>
</div>
<div className={styles.prices}>
<Body textTransform="bold">
{intl.formatMessage({ id: "Prices" })}
</Body>
<div className={styles.weekday}>
<Caption color="uiTextMediumContrast" textTransform="uppercase">
{intl.formatMessage({ id: "Weekday prices" })}
</Caption>
<Divider color="baseSurfaceSubtleHover" />
<ParkingPrices
pricing={data.pricing.localCurrency?.ordinary}
currency={data.pricing.localCurrency?.currency}
freeParking={data.pricing.freeParking}
/>
</div>
<div className={styles.weekend}>
<Caption color="uiTextMediumContrast" textTransform="uppercase">
{intl.formatMessage({ id: "Weekend prices" })}
</Caption>
<Divider color="baseSurfaceSubtleHover" />
<ParkingPrices
pricing={data.pricing.localCurrency?.weekend}
currency={data.pricing.localCurrency?.currency}
freeParking={data.pricing.freeParking}
/>
</div>
</div>
{data.externalParkingUrl && (
<Button theme="base" intent="primary" asChild>
<Link
href={data.externalParkingUrl}
color="white"
weight="bold"
target="_blank"
>
{intl.formatMessage({ id: "Book parking" })}
<OpenInNewIcon color="white" />
</Link>
</Button>
)}
</div>
<ParkingInformation key={data.type} parking={data} />
))}
{hasExtraParkingPage && (
<Button

View File

@@ -1,24 +1,4 @@
.wrapper,
.information {
.wrapper {
display: grid;
gap: var(--Spacing-x3);
}
.list,
.prices {
display: grid;
gap: var(--Spacing-x-one-and-half);
}
.weekday,
.weekend {
background-color: var(--Base-Surface-Subtle-Normal);
border-radius: var(--Corner-radius-Medium);
padding: var(--Spacing-x2) var(--Spacing-x3);
display: grid;
gap: var(--Spacing-x1);
}
.parkingPageLink {
margin-top: var(--Spacing-x2);
}

View File

@@ -0,0 +1,27 @@
import ParkingInformation from "@/components/ParkingInformation"
import styles from "./additionalContent.module.css"
import type { Hotel } from "@/types/hotel"
interface ParkingAdditionalContentProps {
hotel: Hotel
}
export default async function ParkingAdditionalContent({
hotel,
}: ParkingAdditionalContentProps) {
const parking = hotel.parking
return (
<div className={styles.additionalContent}>
{parking.map((data) => (
<ParkingInformation
key={data.type}
parking={data}
showExternalParkingButton={false}
/>
))}
</div>
)
}

View File

@@ -0,0 +1,4 @@
.additionalContent {
display: grid;
gap: var(--Spacing-x4);
}

View File

@@ -1,7 +1,12 @@
import { wellnessSubPage } from "@/constants/routes/hotelSubpages"
import {
parkingSubPage,
wellnessSubPage,
} from "@/constants/routes/hotelSubpages"
import { getLang } from "@/i18n/serverContext"
import ParkingAdditionalContent from "./ParkingAdditionalContent"
import type { Hotel } from "@/types/hotel"
interface HotelSubpageAdditionalContentProps {
@@ -16,6 +21,8 @@ export default function HotelSubpageAdditionalContent({
const lang = getLang()
switch (subpage) {
case parkingSubPage[lang]:
return <ParkingAdditionalContent hotel={hotel} />
case wellnessSubPage[lang]:
return null
default:

View File

@@ -0,0 +1,64 @@
import NextLink from "next/link"
import { OpenInNewIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import styles from "./sidebar.module.css"
import type { Hotel } from "@/types/hotel"
interface HotelSidebarProps {
hotel: Hotel
}
export default async function ParkingSidebar({ hotel }: HotelSidebarProps) {
const intl = await getIntl()
const parking = hotel.parking
.map((parking) => ({ url: parking.externalParkingUrl, type: parking.type }))
.filter(
(parking): parking is { type: string; url: string } => !!parking.url
)
return (
<aside className={styles.sidebar}>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Address" })}
</Title>
<div>
<Body color="uiTextHighContrast">{hotel.address.streetAddress}</Body>
<Body color="uiTextHighContrast">
{hotel.address.zipCode} {hotel.address.city}
</Body>
<Body color="uiTextHighContrast">{hotel.address.country}</Body>
</div>
<Title level="h3" as="h4">
{intl.formatMessage({ id: "Contact us" })}
</Title>
<Link
href={`tel:${hotel.contactInformation.phoneNumber}`}
color="peach80"
textDecoration="underline"
>
{hotel.contactInformation.phoneNumber}
</Link>
{parking.map(({ url, type }) => (
<Button key={type} theme="base" intent="primary" variant="icon" asChild>
<NextLink href={url} target="_blank">
{intl.formatMessage(
{ id: "Book {type} parking" },
{ type: type.toLowerCase() }
)}
<OpenInNewIcon />
</NextLink>
</Button>
))}
</aside>
)
}

View File

@@ -1,7 +1,11 @@
import { wellnessSubPage } from "@/constants/routes/hotelSubpages"
import {
parkingSubPage,
wellnessSubPage,
} from "@/constants/routes/hotelSubpages"
import { getLang } from "@/i18n/serverContext"
import ParkingSidebar from "./ParkingSidebar"
import WellnessSidebar from "./WellnessSidebar"
import type { Hotel } from "@/types/hotel"
@@ -17,6 +21,8 @@ export default function HotelSubpageSidebar({
}: HotelSubpageSidebarProps) {
const lang = getLang()
switch (subpage) {
case parkingSubPage[lang]:
return <ParkingSidebar hotel={hotel} />
case wellnessSubPage[lang]:
return <WellnessSidebar hotel={hotel} />
default:

View File

@@ -1,4 +1,7 @@
import { wellnessSubPage } from "@/constants/routes/hotelSubpages"
import {
parkingSubPage,
wellnessSubPage,
} from "@/constants/routes/hotelSubpages"
import { getLang } from "@/i18n/serverContext"
@@ -15,17 +18,29 @@ export function getSubpageData(
const additionalData = hotelData.additionalData
const hotel = hotelData.hotel
switch (subpage) {
case parkingSubPage[lang]:
const parkingImage = additionalData.parkingImages?.heroImages[0]
return {
...additionalData.hotelParking,
heading: intl.formatMessage({ id: "Parking" }),
heroImage: parkingImage
? {
src: parkingImage.imageSizes.medium,
alt: parkingImage.metaData.altText || "",
}
: null,
}
case wellnessSubPage[lang]:
const heroImage = hotel.healthFacilities.find(
const wellnessImage = hotel.healthFacilities.find(
(fac) => fac.content.images.length
)?.content.images[0]
return {
...additionalData.healthAndFitness,
heading: intl.formatMessage({ id: "Wellness & Exercise" }),
heroImage: heroImage
heroImage: wellnessImage
? {
src: heroImage.imageSizes.medium,
alt: heroImage.metaData.altText || "",
src: wellnessImage.imageSizes.medium,
alt: wellnessImage.metaData.altText || "",
}
: null,
}

View File

@@ -0,0 +1,64 @@
import Body from "@/components/TempDesignSystem/Text/Body"
import { getIntl } from "@/i18n"
import styles from "./parkingList.module.css"
import type { ParkingListProps } from "@/types/components/hotelPage/sidepeek/parking"
export default async function ParkingList({
numberOfChargingSpaces,
canMakeReservation,
numberOfParkingSpots,
distanceToHotel,
address,
}: ParkingListProps) {
const intl = await getIntl()
const canMakeReservationYesMsg = intl.formatMessage({
id: "Parking can be reserved in advance: Yes",
})
const canMakeReservationNoMsg = intl.formatMessage({
id: "Parking can be reserved in advance: No",
})
return (
<Body color="uiTextHighContrast" asChild>
<ul className={styles.listStyling}>
{numberOfChargingSpaces ? (
<li>
{intl.formatMessage(
{ id: "Number of charging points for electric cars: {number}" },
{ number: numberOfChargingSpaces }
)}
</li>
) : null}
<li>
{canMakeReservation
? canMakeReservationYesMsg
: canMakeReservationNoMsg}
</li>
{numberOfParkingSpots ? (
<li>
{intl.formatMessage(
{ id: "Number of parking spots: {number}" },
{ number: numberOfParkingSpots }
)}
</li>
) : null}
{distanceToHotel ? (
<li>
{intl.formatMessage(
{ id: "Distance to hotel: {distanceInM} m" },
{ distanceInM: distanceToHotel }
)}
</li>
) : null}
{address ? (
<li>
{intl.formatMessage({ id: "Address: {address}" }, { address })}
</li>
) : null}
</ul>
</Body>
)
}

View File

@@ -0,0 +1,11 @@
.listStyling {
list-style-type: none;
}
.listStyling > li::before {
content: url("/_static/icons/heart.svg");
position: relative;
height: 8px;
top: 3px;
margin-right: var(--Spacing-x1);
}

View File

@@ -0,0 +1,69 @@
import Body from "@/components/TempDesignSystem/Text/Body"
import { getIntl } from "@/i18n"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./parkingPrices.module.css"
import {
type ParkingPricesProps,
Periods,
} from "@/types/components/hotelPage/sidepeek/parking"
export default async function ParkingPrices({
currency = "",
freeParking,
pricing,
}: ParkingPricesProps) {
const intl = await getIntl()
const day = intl.formatMessage({ id: "Price per day" })
const night = intl.formatMessage({ id: "Price per night" })
const allDay = intl.formatMessage({ id: "Price per 24 hours" })
function getPeriod(period?: string) {
switch (period) {
case Periods.day:
return day
case Periods.night:
return night
case Periods.allDay:
return allDay
default:
return period
}
}
const filteredPeriods = pricing?.filter((filter) => filter.period !== "Hour")
return (
<div className={styles.wrapper}>
{filteredPeriods?.map((parking) => (
<div key={parking.period} className={styles.period}>
<div className={styles.information}>
<Body textTransform="bold" color="uiTextHighContrast">
{getPeriod(parking.period)}
</Body>
<Body color="uiTextHighContrast">
{parking.amount
? freeParking
? intl.formatMessage({ id: "Free parking" })
: formatPrice(intl, parking.amount, currency)
: intl.formatMessage({ id: "N/A" })}
</Body>
</div>
{parking.startTime &&
parking.endTime &&
parking.period !== Periods.allDay && (
<div className={styles.information}>
<Body color="uiTextHighContrast">
{intl.formatMessage({ id: "From" })}
</Body>
<Body color="uiTextHighContrast">
{`${parking.startTime}-${parking.endTime}`}
</Body>
</div>
)}
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,13 @@
.wrapper {
display: grid;
row-gap: var(--Spacing-x1);
}
.period {
display: flex;
gap: var(--Spacing-x5);
}
.information {
flex: 1;
}

View File

@@ -0,0 +1,78 @@
import Link from "next/link"
import { getIntl } from "@/i18n"
import { OpenInNewIcon } from "../Icons"
import Button from "../TempDesignSystem/Button"
import Divider from "../TempDesignSystem/Divider"
import Caption from "../TempDesignSystem/Text/Caption"
import Subtitle from "../TempDesignSystem/Text/Subtitle"
import ParkingList from "./ParkingList"
import ParkingPrices from "./ParkingPrices"
import styles from "./parkingInformation.module.css"
import type { Parking } from "@/types/hotel"
interface ParkingInformationProps {
parking: Parking
showExternalParkingButton?: boolean
}
export default async function ParkingInformation({
parking,
showExternalParkingButton = true,
}: ParkingInformationProps) {
const intl = await getIntl()
return (
<div className={styles.parkingInformation}>
<div className={styles.list}>
<Subtitle type="two" asChild>
<h4>{parking.type}</h4>
</Subtitle>
<ParkingList
numberOfChargingSpaces={parking.numberOfChargingSpaces}
canMakeReservation={parking.canMakeReservation}
numberOfParkingSpots={parking.numberOfParkingSpots}
distanceToHotel={parking.distanceToHotel}
address={parking.address}
/>
</div>
<div className={styles.prices}>
<Subtitle type="two" asChild>
<h5>{intl.formatMessage({ id: "Prices" })}</h5>
</Subtitle>
<div className={styles.priceWrapper}>
<Caption color="uiTextMediumContrast" textTransform="uppercase">
{intl.formatMessage({ id: "Weekday prices" })}
</Caption>
<Divider color="baseSurfaceSubtleHover" />
<ParkingPrices
pricing={parking.pricing.localCurrency?.ordinary}
currency={parking.pricing.localCurrency?.currency}
freeParking={parking.pricing.freeParking}
/>
</div>
<div className={styles.priceWrapper}>
<Caption color="uiTextMediumContrast" textTransform="uppercase">
{intl.formatMessage({ id: "Weekend prices" })}
</Caption>
<Divider color="baseSurfaceSubtleHover" />
<ParkingPrices
pricing={parking.pricing.localCurrency?.weekend}
currency={parking.pricing.localCurrency?.currency}
freeParking={parking.pricing.freeParking}
/>
</div>
</div>
{parking.externalParkingUrl && showExternalParkingButton && (
<Button theme="base" intent="primary" variant="icon" asChild>
<Link href={parking.externalParkingUrl} target="_blank">
{intl.formatMessage({ id: "Book parking" })}
<OpenInNewIcon />
</Link>
</Button>
)}
</div>
)
}

View File

@@ -0,0 +1,18 @@
.parkingInformation {
display: grid;
gap: var(--Spacing-x3);
}
.list,
.prices {
display: grid;
gap: var(--Spacing-x-one-and-half);
}
.priceWrapper {
background-color: var(--Base-Surface-Subtle-Normal);
border-radius: var(--Corner-radius-Medium);
padding: var(--Spacing-x2) var(--Spacing-x3);
display: grid;
gap: var(--Spacing-x1);
}