From c4eafa419bb46211c336e8922bfa8ffb107ce8f7 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 4 Feb 2025 11:11:51 +0000 Subject: [PATCH] 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 --- .../AccordionAmenities/Parking/index.tsx | 61 +-------------- .../Parking/parkingAmenity.module.css | 22 +----- .../ParkingAdditionalContent.tsx | 27 +++++++ .../additionalContent.module.css | 4 + .../HotelSubpage/AdditionalContent/index.tsx | 9 ++- .../HotelSubpage/Sidebar/ParkingSidebar.tsx | 64 +++++++++++++++ .../HotelSubpage/Sidebar/index.tsx | 8 +- components/ContentType/HotelSubpage/utils.ts | 25 ++++-- .../ParkingInformation/ParkingList/index.tsx | 64 +++++++++++++++ .../ParkingList/parkingList.module.css | 11 +++ .../ParkingPrices/index.tsx | 69 ++++++++++++++++ .../ParkingPrices/parkingPrices.module.css | 13 ++++ components/ParkingInformation/index.tsx | 78 +++++++++++++++++++ .../parkingInformation.module.css | 18 +++++ i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + .../routers/hotels/schemas/additionalData.ts | 1 + 21 files changed, 393 insertions(+), 87 deletions(-) create mode 100644 components/ContentType/HotelSubpage/AdditionalContent/ParkingAdditionalContent.tsx create mode 100644 components/ContentType/HotelSubpage/Sidebar/ParkingSidebar.tsx create mode 100644 components/ParkingInformation/ParkingList/index.tsx create mode 100644 components/ParkingInformation/ParkingList/parkingList.module.css create mode 100644 components/ParkingInformation/ParkingPrices/index.tsx create mode 100644 components/ParkingInformation/ParkingPrices/parkingPrices.module.css create mode 100644 components/ParkingInformation/index.tsx create mode 100644 components/ParkingInformation/parkingInformation.module.css diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx index 0738b7645..480a5f06b 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx @@ -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({
{parkingElevatorPitch} {parking.map((data) => ( -
-
- {data.type} - -
-
- - {intl.formatMessage({ id: "Prices" })} - -
- - {intl.formatMessage({ id: "Weekday prices" })} - - - -
-
- - {intl.formatMessage({ id: "Weekend prices" })} - - - -
-
- {data.externalParkingUrl && ( - - )} -
+ ))} {hasExtraParkingPage && (
+ ) +} diff --git a/components/ContentType/HotelSubpage/AdditionalContent/additionalContent.module.css b/components/ContentType/HotelSubpage/AdditionalContent/additionalContent.module.css index e69de29bb..3011afbbc 100644 --- a/components/ContentType/HotelSubpage/AdditionalContent/additionalContent.module.css +++ b/components/ContentType/HotelSubpage/AdditionalContent/additionalContent.module.css @@ -0,0 +1,4 @@ +.additionalContent { + display: grid; + gap: var(--Spacing-x4); +} diff --git a/components/ContentType/HotelSubpage/AdditionalContent/index.tsx b/components/ContentType/HotelSubpage/AdditionalContent/index.tsx index e1d859d67..ca86c1a2b 100644 --- a/components/ContentType/HotelSubpage/AdditionalContent/index.tsx +++ b/components/ContentType/HotelSubpage/AdditionalContent/index.tsx @@ -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 case wellnessSubPage[lang]: return null default: diff --git a/components/ContentType/HotelSubpage/Sidebar/ParkingSidebar.tsx b/components/ContentType/HotelSubpage/Sidebar/ParkingSidebar.tsx new file mode 100644 index 000000000..52fc4902b --- /dev/null +++ b/components/ContentType/HotelSubpage/Sidebar/ParkingSidebar.tsx @@ -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 ( + + ) +} diff --git a/components/ContentType/HotelSubpage/Sidebar/index.tsx b/components/ContentType/HotelSubpage/Sidebar/index.tsx index 7608eeefc..acbbd9865 100644 --- a/components/ContentType/HotelSubpage/Sidebar/index.tsx +++ b/components/ContentType/HotelSubpage/Sidebar/index.tsx @@ -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 case wellnessSubPage[lang]: return default: diff --git a/components/ContentType/HotelSubpage/utils.ts b/components/ContentType/HotelSubpage/utils.ts index 7bda855f4..30ffc4d75 100644 --- a/components/ContentType/HotelSubpage/utils.ts +++ b/components/ContentType/HotelSubpage/utils.ts @@ -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, } diff --git a/components/ParkingInformation/ParkingList/index.tsx b/components/ParkingInformation/ParkingList/index.tsx new file mode 100644 index 000000000..9bc1a30cf --- /dev/null +++ b/components/ParkingInformation/ParkingList/index.tsx @@ -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 ( + +
    + {numberOfChargingSpaces ? ( +
  • + {intl.formatMessage( + { id: "Number of charging points for electric cars: {number}" }, + { number: numberOfChargingSpaces } + )} +
  • + ) : null} +
  • + {canMakeReservation + ? canMakeReservationYesMsg + : canMakeReservationNoMsg} +
  • + {numberOfParkingSpots ? ( +
  • + {intl.formatMessage( + { id: "Number of parking spots: {number}" }, + { number: numberOfParkingSpots } + )} +
  • + ) : null} + {distanceToHotel ? ( +
  • + {intl.formatMessage( + { id: "Distance to hotel: {distanceInM} m" }, + { distanceInM: distanceToHotel } + )} +
  • + ) : null} + {address ? ( +
  • + {intl.formatMessage({ id: "Address: {address}" }, { address })} +
  • + ) : null} +
+ + ) +} diff --git a/components/ParkingInformation/ParkingList/parkingList.module.css b/components/ParkingInformation/ParkingList/parkingList.module.css new file mode 100644 index 000000000..6e837a4fd --- /dev/null +++ b/components/ParkingInformation/ParkingList/parkingList.module.css @@ -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); +} diff --git a/components/ParkingInformation/ParkingPrices/index.tsx b/components/ParkingInformation/ParkingPrices/index.tsx new file mode 100644 index 000000000..0302efdb0 --- /dev/null +++ b/components/ParkingInformation/ParkingPrices/index.tsx @@ -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 ( +
+ {filteredPeriods?.map((parking) => ( +
+
+ + {getPeriod(parking.period)} + + + {parking.amount + ? freeParking + ? intl.formatMessage({ id: "Free parking" }) + : formatPrice(intl, parking.amount, currency) + : intl.formatMessage({ id: "N/A" })} + +
+ {parking.startTime && + parking.endTime && + parking.period !== Periods.allDay && ( +
+ + {intl.formatMessage({ id: "From" })} + + + {`${parking.startTime}-${parking.endTime}`} + +
+ )} +
+ ))} +
+ ) +} diff --git a/components/ParkingInformation/ParkingPrices/parkingPrices.module.css b/components/ParkingInformation/ParkingPrices/parkingPrices.module.css new file mode 100644 index 000000000..436e883ec --- /dev/null +++ b/components/ParkingInformation/ParkingPrices/parkingPrices.module.css @@ -0,0 +1,13 @@ +.wrapper { + display: grid; + row-gap: var(--Spacing-x1); +} + +.period { + display: flex; + gap: var(--Spacing-x5); +} + +.information { + flex: 1; +} diff --git a/components/ParkingInformation/index.tsx b/components/ParkingInformation/index.tsx new file mode 100644 index 000000000..8f7b2a37e --- /dev/null +++ b/components/ParkingInformation/index.tsx @@ -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 ( +
+
+ +

{parking.type}

+
+ +
+
+ +
{intl.formatMessage({ id: "Prices" })}
+
+
+ + {intl.formatMessage({ id: "Weekday prices" })} + + + +
+
+ + {intl.formatMessage({ id: "Weekend prices" })} + + + +
+
+ {parking.externalParkingUrl && showExternalParkingButton && ( + + )} +
+ ) +} diff --git a/components/ParkingInformation/parkingInformation.module.css b/components/ParkingInformation/parkingInformation.module.css new file mode 100644 index 000000000..a6549552a --- /dev/null +++ b/components/ParkingInformation/parkingInformation.module.css @@ -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); +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 306f30c13..6e7cb554e 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -60,6 +60,7 @@ "Book a table online": "Book et bord online", "Book parking": "Book parkering", "Book reward night": "Book bonusnat", + "Book {type} parking": "Book {type} parkering", "Booking confirmation": "Booking bekræftelse", "Booking number": "Bookingnummer", "Breakfast": "Morgenmad", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index ed0f928e2..421909a98 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -60,6 +60,7 @@ "Book a table online": "Tisch online buchen", "Book parking": "Parkplatz buchen", "Book reward night": "Bonusnacht buchen", + "Book {type} parking": "Buchen Sie {type} Parkplatz", "Booking confirmation": "Buchungsbestätigung", "Booking number": "Buchungsnummer", "Breakfast": "Frühstück", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index f40723212..8d85eecf2 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -63,6 +63,7 @@ "Book parking": "Book parking", "Book reward night": "Book reward night", "Book your next stay": "Book your next stay", + "Book {type} parking": "Book {type} parking", "Booking": "Booking", "Booking confirmation": "Booking confirmation", "Booking number": "Booking number", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 74dd19e36..476968158 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -60,6 +60,7 @@ "Book a table online": "Varaa pöytä verkossa", "Book parking": "Varaa pysäköinti", "Book reward night": "Kirjapalkinto-ilta", + "Book {type} parking": "Varaa {type} pysäköinti", "Booking confirmation": "Varausvahvistus", "Booking number": "Varausnumero", "Breakfast": "Aamiainen", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 4ac8c63c4..248fdf18d 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -60,6 +60,7 @@ "Book a table online": "Bestill bord online", "Book parking": "Bestill parkering", "Book reward night": "Bestill belønningskveld", + "Book {type} parking": "Book {type} parkering", "Booking confirmation": "Bestillingsbekreftelse", "Booking number": "Bestillingsnummer", "Breakfast": "Frokost", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 4c214b8c6..db89a7482 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -60,6 +60,7 @@ "Book a table online": "Boka ett bord online", "Book parking": "Boka parkering", "Book reward night": "Boka frinatt", + "Book {type} parking": "Boka {type} parkering", "Booking confirmation": "Bokningsbekräftelse", "Booking number": "Bokningsnummer", "Breakfast": "Frukost", diff --git a/server/routers/hotels/schemas/additionalData.ts b/server/routers/hotels/schemas/additionalData.ts index 28602b9fe..b2b3ecc53 100644 --- a/server/routers/hotels/schemas/additionalData.ts +++ b/server/routers/hotels/schemas/additionalData.ts @@ -49,6 +49,7 @@ export const additionalDataSchema = z.object({ conferencesAndMeetings: facilitySchema.optional(), healthAndWellness: facilitySchema.optional(), restaurantImages: facilitySchema.optional(), + parkingImages: facilitySchema.optional(), restaurantsOverviewPage: restaurantsOverviewPageSchema, meetingRooms: extraPageSchema, healthAndFitness: extraPageSchema,