diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 3db576744..a6c4a892d 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -48,12 +48,6 @@ gap: var(--Spacing-x-half); } -.link { - display: flex; - padding: var(--Spacing-x2) var(--Spacing-x0); - border-bottom: 1px solid var(--Base-Border-Subtle); -} - .prices { display: flex; flex-direction: column; @@ -115,7 +109,7 @@ padding-bottom: var(--Spacing-x2); } - .link { + .detailsButton { border-bottom: none; } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 76d702b98..82b0c4b91 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,11 +1,5 @@ -import { useIntl } from "react-intl" - import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" -import { - ChevronRightIcon, - PriceTagIcon, - ScandicLogoIcon, -} from "@/components/Icons" +import { PriceTagIcon, ScandicLogoIcon } from "@/components/Icons" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" @@ -14,13 +8,16 @@ import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import ReadMore from "../ReadMore" import styles from "./hotelCard.module.css" import { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" -export default function HotelCard({ hotel }: HotelCardProps) { - const intl = useIntl() +export default async function HotelCard({ hotel }: HotelCardProps) { + const intl = await getIntl() const { hotelData } = hotel const { price } = hotel @@ -51,7 +48,7 @@ export default function HotelCard({ hotel }: HotelCardProps) { {hotelData.name} - + {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} @@ -70,10 +67,7 @@ export default function HotelCard({ hotel }: HotelCardProps) { ) })} - - {intl.formatMessage({ id: "See hotel details" })} - - +
diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index bba0ce99d..fb17cb2a5 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -1,5 +1,3 @@ -"use client" - import Title from "@/components/TempDesignSystem/Text/Title" import HotelCard from "../HotelCard" diff --git a/components/HotelReservation/ReadMore/Contact/contact.module.css b/components/HotelReservation/ReadMore/Contact/contact.module.css new file mode 100644 index 000000000..53f6e01f3 --- /dev/null +++ b/components/HotelReservation/ReadMore/Contact/contact.module.css @@ -0,0 +1,48 @@ +.wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto; + gap: var(--Spacing-x2); + font-family: var(--typography-Body-Regular-fontFamily); +} + +.address, +.contactInfo { + display: grid; + grid-template-columns: subgrid; + grid-template-rows: subgrid; + grid-column: 1 / 3; + grid-row: 1 / 4; +} + +.contactInfo > li { + font-style: normal; + list-style-type: none; + display: flex; + flex-direction: column; +} + +.heading { + font-weight: 500; +} + +.soMeIcons { + display: flex; + gap: var(--Spacing-x-one-and-half); +} + +.ecoLabel { + display: grid; + grid-template-columns: auto 1fr; + column-gap: var(--Spacing-x-one-and-half); + grid-column: 2 / 3; + grid-row: 3 / 4; + font-size: var(--typography-Footnote-Regular-fontSize); + line-height: (); +} + +.ecoLabelText { + display: flex; + flex-direction: column; + justify-content: center; +} diff --git a/components/HotelReservation/ReadMore/Contact/index.tsx b/components/HotelReservation/ReadMore/Contact/index.tsx new file mode 100644 index 000000000..46491fb3d --- /dev/null +++ b/components/HotelReservation/ReadMore/Contact/index.tsx @@ -0,0 +1,85 @@ +"use client" + +import { useIntl } from "react-intl" + +import FacebookIcon from "@/components/Icons/Facebook" +import InstagramIcon from "@/components/Icons/Instagram" +import Image from "@/components/Image" +import Link from "@/components/TempDesignSystem/Link" +import useLang from "@/hooks/useLang" + +import styles from "./contact.module.css" + +import { ContactProps } from "@/types/components/hotelReservation/selectHotel/selectHotel" + +export default function Contact({ hotel }: ContactProps) { + const lang = useLang() + const intl = useIntl() + + return ( +
+
+
    +
  • + + {intl.formatMessage({ id: "Address" })} + + {hotel.address.streetAddress} + {hotel.address.city} +
  • +
  • + + {intl.formatMessage({ id: "Driving directions" })} + + {intl.formatMessage({ id: "Google Maps" })} +
  • +
  • + + {intl.formatMessage({ id: "Email" })} + + + {hotel.contactInformation.email} + +
  • +
  • + + {intl.formatMessage({ id: "Contact us" })} + + + {hotel.contactInformation.phoneNumber} + +
  • +
  • + + {intl.formatMessage({ id: "Follow us" })} + +
    + + + + + + +
    +
  • +
+
+ {hotel.hotelFacts.ecoLabels.nordicEcoLabel ? ( +
+ {intl.formatMessage({ +
+ {intl.formatMessage({ id: "Nordic Swan Ecolabel" })} + + {hotel.hotelFacts.ecoLabels.svanenEcoLabelCertificateNumber} + +
+
+ ) : null} +
+ ) +} diff --git a/components/HotelReservation/ReadMore/index.tsx b/components/HotelReservation/ReadMore/index.tsx new file mode 100644 index 000000000..e988ec40d --- /dev/null +++ b/components/HotelReservation/ReadMore/index.tsx @@ -0,0 +1,119 @@ +"use client" + +import { useState } from "react" +import { useIntl } from "react-intl" + +import { ChevronRightIcon } from "@/components/Icons" +import Accordion from "@/components/TempDesignSystem/Accordion" +import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem" +import Button from "@/components/TempDesignSystem/Button" +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import Contact from "./Contact" + +import styles from "./readMore.module.css" + +import { + DetailedAmenity, + ParkingProps, + ReadMoreProps, +} from "@/types/components/hotelReservation/selectHotel/selectHotel" +import { Hotel } from "@/types/hotel" + +function getAmenitiesList(hotel: Hotel) { + const detailedAmenities: DetailedAmenity[] = Object.entries( + hotel.hotelFacts.hotelFacilityDetail + ).map(([key, value]) => ({ name: key, ...value })) + + // Remove Parking facilities since parking accordion is based on hotel.parking + const simpleAmenities = hotel.detailedFacilities.filter( + (facility) => !facility.name.startsWith("Parking") + ) + return [...detailedAmenities, ...simpleAmenities] +} + +export default function ReadMore({ hotel, hotelId }: ReadMoreProps) { + const intl = useIntl() + + const [sidePeekOpen, setSidePeekOpen] = useState(false) + + const amenitiesList = getAmenitiesList(hotel) + return ( + <> + + { + setSidePeekOpen(false) + }} + > +
+ + {intl.formatMessage({ id: "Practical information" })} + + + + {/* parking */} + {hotel.parking.length ? ( + + {hotel.parking.map((p) => ( + + ))} + + ) : null} + + TODO: What content should be in the accessibility section? + + {amenitiesList.map((amenity) => { + return "description" in amenity ? ( + + {amenity.description} + + ) : ( +
+ {amenity.name} +
+ ) + })} +
+ {/* TODO: handle linking to Hotel Page */} + +
+
+ + ) +} + +function Parking({ parking }: ParkingProps) { + const intl = useIntl() + return ( +
+ {`${intl.formatMessage({ id: parking.type })} (${parking.name})`} +
    +
  • + {`${intl.formatMessage({ + id: "Number of charging points for electric cars", + })}: ${parking.numberOfChargingSpaces}`} +
  • +
  • {`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${parking.canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}
  • +
  • {`${intl.formatMessage({ id: "Number of parking spots" })}: ${parking.numberOfParkingSpots}`}
  • +
  • {`${intl.formatMessage({ id: "Distance to hotel" })}: ${parking.distanceToHotel}`}
  • +
  • {`${intl.formatMessage({ id: "Address" })}: ${parking.address}`}
  • +
+
+ ) +} diff --git a/components/HotelReservation/ReadMore/readMore.module.css b/components/HotelReservation/ReadMore/readMore.module.css new file mode 100644 index 000000000..f7859a8fc --- /dev/null +++ b/components/HotelReservation/ReadMore/readMore.module.css @@ -0,0 +1,25 @@ +.detailsButton { + align-self: start; + border-radius: 0; + height: auto; + padding-left: 0; + padding-right: 0; +} + +.content { + display: grid; + gap: var(--Spacing-x2); +} + +.amenity { + font-family: var(--typography-Body-Regular-fontFamily); + border-bottom: 1px solid var(--Base-Border-Subtle); + /* padding set to align with AccordionItem which has a different composition */ + padding: var(--Spacing-x2) + calc(var(--Spacing-x1) + var(--Spacing-x-one-and-half)); +} + +.list { + font-family: var(--typography-Body-Regular-fontFamily); + list-style: inside; +} diff --git a/components/TempDesignSystem/Link/index.tsx b/components/TempDesignSystem/Link/index.tsx index 2301ce932..771c60e82 100644 --- a/components/TempDesignSystem/Link/index.tsx +++ b/components/TempDesignSystem/Link/index.tsx @@ -82,6 +82,11 @@ export default function Link({ // track navigation nor start a router transition. return } + if (href.startsWith("tel:") || href.startsWith("mailto:")) { + // If href contains tel or mailto protocols we don't want to + // track navigation nor start a router transition. + return + } e.preventDefault() trackPageViewStart() startTransition(() => { diff --git a/components/TempDesignSystem/SidePeek/index.tsx b/components/TempDesignSystem/SidePeek/index.tsx index df0b8dc44..6df535b54 100644 --- a/components/TempDesignSystem/SidePeek/index.tsx +++ b/components/TempDesignSystem/SidePeek/index.tsx @@ -1,7 +1,7 @@ "use client" import { useIsSSR } from "@react-aria/ssr" -import { useContext } from "react" +import { useContext, useState } from "react" import { Dialog, DialogTrigger, @@ -29,6 +29,15 @@ function SidePeek({ }: React.PropsWithChildren) { const isSSR = useIsSSR() const intl = useIntl() + + const [rootDiv, setRootDiv] = useState(undefined) + + function setRef(node: HTMLDivElement | null) { + if (node) { + setRootDiv(node) + } + } + const context = useContext(SidePeekContext) function onClose() { const closeHandler = handleClose || context?.handleClose @@ -44,42 +53,45 @@ function SidePeek({ ) } return ( - - - - - - - - - + + + +
{children}
+ + + + + +
) } diff --git a/components/TempDesignSystem/SidePeek/sidePeek.module.css b/components/TempDesignSystem/SidePeek/sidePeek.module.css index 2d6de4f00..65ad886b1 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.module.css +++ b/components/TempDesignSystem/SidePeek/sidePeek.module.css @@ -78,6 +78,7 @@ .sidePeekContent { padding: var(--Spacing-x4); + overflow-y: auto; } @media screen and (min-width: 1367px) { .modal { @@ -94,8 +95,4 @@ .modal[data-exiting] { animation: slide-in 250ms reverse; } - - .overlay { - top: 0; - } } diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 7f57ba6cd..d0d95903a 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -298,7 +298,7 @@ const parkingPricingSchema = z.object({ .optional(), }) -const parkingSchema = z.object({ +export const parkingSchema = z.object({ type: z.string(), name: z.string(), address: z.string().optional(), diff --git a/types/components/hotelReservation/selectHotel/selectHotel.ts b/types/components/hotelReservation/selectHotel/selectHotel.ts index d4c40ac56..6d1cecfca 100644 --- a/types/components/hotelReservation/selectHotel/selectHotel.ts +++ b/types/components/hotelReservation/selectHotel/selectHotel.ts @@ -1,4 +1,25 @@ +import { Hotel, ParkingData } from "@/types/hotel" + export enum AvailabilityEnum { Available = "Available", NotAvailable = "NotAvailable", } + +export interface DetailedAmenity { + name: string + heading: string + description: string +} + +export interface ReadMoreProps { + hotelId: string + hotel: Hotel +} + +export interface ContactProps { + hotel: Hotel +} + +export interface ParkingProps { + parking: ParkingData +} diff --git a/types/hotel.ts b/types/hotel.ts index 54d9c8125..f4436fb6c 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -2,6 +2,7 @@ import { z } from "zod" import { getHotelDataSchema, + parkingSchema, pointOfInterestSchema, roomSchema, } from "@/server/routers/hotels/output" @@ -49,3 +50,5 @@ export enum PointOfInterestGroupEnum { PARKING = "Parking", SHOPPING_DINING = "Shopping & Dining", } + +export type ParkingData = z.infer