Merged in feat/SW-1051-amenities-in-sidepeek (pull request #1007)

feat(SW-1051): fix sidepeek select hotel page

* feat(SW-1051): fix sidepeek select hotel page


Approved-by: Niclas Edenvin
This commit is contained in:
Bianca Widstam
2024-12-02 13:45:31 +00:00
parent 403e1e7cf8
commit 4f871a3220
27 changed files with 359 additions and 138 deletions

View File

@@ -13,7 +13,7 @@ export default function FooterMainNav({ mainLinks }: FooterMainNavProps) {
<ul className={styles.mainNavigationList}>
{mainLinks.map((link) => (
<li key={link.title} className={styles.mainNavigationItem}>
<Subtitle color="baseTextMediumContrast" type="two" asChild>
<Subtitle color="baseTextHighContrast" type="two" asChild>
<Link
color="burgundy"
href={link.url}
@@ -40,7 +40,7 @@ export function FooterMainNavSkeleton() {
<ul className={styles.mainNavigationList}>
{items.map((x) => (
<li key={x} className={styles.mainNavigationItem}>
<Subtitle color="baseTextMediumContrast" type="two" asChild>
<Subtitle color="baseTextHighContrast" type="two" asChild>
<span className={styles.mainNavigationLink}>
<SkeletonShimmer width="80%" />
<ArrowRightIcon color="peach80" />

View File

@@ -22,10 +22,6 @@
flex-direction: column;
}
.heading {
font-weight: 500;
}
.soMeIcons {
display: flex;
gap: var(--Spacing-x-one-and-half);
@@ -35,14 +31,20 @@
display: flex;
align-items: center;
column-gap: var(--Spacing-x-one-and-half);
grid-column: 2 / 3;
grid-row: 3 / 4;
grid-column: 1 / 3;
grid-row: 4 / 4;
font-size: var(--typography-Footnote-Regular-fontSize);
line-height: ();
margin-bottom: var(--Spacing-x1);
}
.ecoLabel img {
flex-shrink: 0;
}
.ecoLabelText {
display: flex;
color: var(--UI-Text-Medium-contrast);
flex-direction: column;
justify-content: center;
}

View File

@@ -5,6 +5,7 @@ import { useIntl } from "react-intl"
import { FacebookIcon, InstagramIcon } from "@/components/Icons"
import Image from "@/components/Image"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import useLang from "@/hooks/useLang"
import styles from "./contact.module.css"
@@ -20,17 +21,17 @@ export default function Contact({ hotel }: ContactProps) {
<address className={styles.address}>
<ul className={styles.contactInfo}>
<li>
<span className={styles.heading}>
<Body textTransform="bold">
{intl.formatMessage({ id: "Address" })}
</span>
<span>
</Body>
<Body>
{`${hotel.address.streetAddress}, ${hotel.address.city}`}
</span>
</Body>
</li>
<li>
<span className={styles.heading}>
<Body textTransform="bold">
{intl.formatMessage({ id: "Driving directions" })}
</span>
</Body>
<a
href={`https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`}
className={styles.googleMaps}
@@ -40,20 +41,9 @@ export default function Contact({ hotel }: ContactProps) {
</a>
</li>
<li>
<span className={styles.heading}>
{intl.formatMessage({ id: "Email" })}
</span>
<Link
href={`mailto:${hotel.contactInformation.email}`}
color="peach80"
>
{hotel.contactInformation.email}
</Link>
</li>
<li>
<span className={styles.heading}>
<Body textTransform="bold">
{intl.formatMessage({ id: "Contact us" })}
</span>
</Body>
<Link
href={`tel:${hotel.contactInformation.phoneNumber}`}
color="peach80"
@@ -62,25 +52,44 @@ export default function Contact({ hotel }: ContactProps) {
</Link>
</li>
<li>
<span className={styles.heading}>
{intl.formatMessage({ id: "Follow us" })}
</span>
<div className={styles.soMeIcons}>
<Link href="#" target="_blank">
<InstagramIcon color="burgundy" />
</Link>
<Link href="#" target="_blank">
<FacebookIcon color="burgundy" />
</Link>
</div>
{(hotel.socialMedia.facebook || hotel.socialMedia.instagram) && (
<>
<Body textTransform="bold">
{intl.formatMessage({ id: "Follow us" })}
</Body>
<div className={styles.soMeIcons}>
{hotel.socialMedia.instagram && (
<Link href={hotel.socialMedia.instagram} target="_blank">
<InstagramIcon color="burgundy" />
</Link>
)}
{hotel.socialMedia.facebook && (
<Link href={hotel.socialMedia.facebook} target="_blank">
<FacebookIcon color="burgundy" />
</Link>
)}
</div>
</>
)}
</li>
<li>
<Body textTransform="bold">
{intl.formatMessage({ id: "Email" })}
</Body>
<Link
href={`mailto:${hotel.contactInformation.email}`}
color="peach80"
>
{hotel.contactInformation.email}
</Link>
</li>
</ul>
</address>
{hotel.hotelFacts.ecoLabels.nordicEcoLabel ? (
{hotel.hotelFacts.ecoLabels?.nordicEcoLabel ? (
<div className={styles.ecoLabel}>
<Image
height={38}
width={38}
width={43}
alt={intl.formatMessage({ id: "Nordic Swan Ecolabel" })}
src={`/_static/img/icons/swan-eco/swan_eco_dark_${lang}.png`}
/>

View File

@@ -0,0 +1,27 @@
import { iconVariants } from "./variants"
import type { IconProps } from "@/types/components/icon"
export default function FilledHeartIcon({
className,
color,
...props
}: IconProps) {
const classNames = iconVariants({ className, color })
return (
<svg
className={classNames}
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8.00091 13.4171L7.23424 12.7255C6.1424 11.7524 5.24166 10.9189 4.53203 10.2249C3.82239 9.53081 3.26322 8.91489 2.85451 8.37709C2.44579 7.8393 2.16246 7.34774 2.00451 6.90241C1.84656 6.45708 1.76758 6.00088 1.76758 5.53379C1.76758 4.57813 2.09533 3.77255 2.75083 3.11704C3.40634 2.46154 4.21192 2.13379 5.16758 2.13379C5.70411 2.13379 6.22436 2.25046 6.72831 2.48379C7.23227 2.71712 7.65647 3.05046 8.00091 3.48379C8.36202 3.05046 8.7898 2.71712 9.28425 2.48379C9.77869 2.25046 10.2954 2.13379 10.8342 2.13379C11.7899 2.13379 12.5955 2.46154 13.251 3.11704C13.9065 3.77255 14.2342 4.57813 14.2342 5.53379C14.2342 6.00088 14.158 6.45153 14.0056 6.88574C13.8533 7.31996 13.5727 7.80319 13.164 8.33542C12.7553 8.86767 12.1933 9.48637 11.4781 10.1915C10.7629 10.8967 9.84831 11.7524 8.73425 12.7588L8.00091 13.4171Z"
fill="#B05B65"
/>
</svg>
)
}

View File

@@ -0,0 +1,21 @@
import { useIntl } from "react-intl"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Body from "@/components/TempDesignSystem/Text/Body"
import { AccessibilityProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { IconName } from "@/types/components/icon"
export default function Accessibility({
accessibilityElevatorPitchText,
}: AccessibilityProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({ id: "Accessibility" })}
icon={IconName.Accessibility}
>
<Body>{accessibilityElevatorPitchText}</Body>
</AccordionItem>
)
}

View File

@@ -0,0 +1,22 @@
import { useIntl } from "react-intl"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Body from "@/components/TempDesignSystem/Text/Body"
import { CheckInCheckOutProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { IconName } from "@/types/components/icon"
export default function CheckinCheckOut({ checkin }: CheckInCheckOutProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({ id: "Check-in/Check-out" })}
icon={IconName.Calendar}
>
<Body textTransform="bold">{intl.formatMessage({ id: "Hours" })}</Body>
<Body>{`${intl.formatMessage({ id: "Check in from" })}: ${checkin.checkInTime}`}</Body>
<Body>{`${intl.formatMessage({ id: "Check out at latest" })}: ${checkin.checkOutTime}`}</Body>
</AccordionItem>
)
}

View File

@@ -0,0 +1,21 @@
import { useIntl } from "react-intl"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Body from "@/components/TempDesignSystem/Text/Body"
import { MeetingsAndConferencesProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { IconName } from "@/types/components/icon"
export default function MeetingsAndConferences({
meetingDescription,
}: MeetingsAndConferencesProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({ id: "Meetings & Conferences" })}
icon={IconName.Business}
>
<Body>{meetingDescription}</Body>
</AccordionItem>
)
}

View File

@@ -0,0 +1,55 @@
import { useIntl } from "react-intl"
import FilledHeartIcon from "@/components/Icons/FilledHeart"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./sidePeekAccordion.module.css"
import { ParkingProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { IconName } from "@/types/components/icon"
export default function Parking({ parking }: ParkingProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({ id: "Parking" })}
icon={IconName.Parking}
className={styles.parking}
>
{parking.map((p) => (
<div key={p.name}>
<Subtitle type="two">
{`${intl.formatMessage({ id: p.type })} ${p?.name ? ` (${p.name})` : ""}`}
</Subtitle>
<ul className={styles.list}>
{p?.address && (
<li>
<FilledHeartIcon color="baseIconLowContrast" />
{`${intl.formatMessage({ id: "Address" })}: ${p.address}`}
</li>
)}
{p?.numberOfParkingSpots !== undefined && (
<li>
<FilledHeartIcon color="baseIconLowContrast" />
{intl.formatMessage(
{ id: "Number of parking spots" },
{ number: p.numberOfParkingSpots }
)}
</li>
)}
{p?.numberOfChargingSpaces !== undefined && (
<li>
<FilledHeartIcon color="baseIconLowContrast" />
{intl.formatMessage(
{ id: "Number of charging points for electric cars" },
{ number: p.numberOfChargingSpaces }
)}
</li>
)}
</ul>
</div>
))}
</AccordionItem>
)
}

View File

@@ -0,0 +1,22 @@
import { useIntl } from "react-intl"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Body from "@/components/TempDesignSystem/Text/Body"
import { RestaurantProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { IconName } from "@/types/components/icon"
export default function Restaurant({
restaurantsContentDescriptionMedium,
}: RestaurantProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({ id: "Restaurant" }, { count: 1 })}
icon={IconName.Restaurant}
>
<Body>{restaurantsContentDescriptionMedium}</Body>
</AccordionItem>
)
}

View File

@@ -0,0 +1,23 @@
.list {
font-family: var(--typography-Body-Regular-fontFamily);
list-style-position: inside;
list-style-type: none;
margin-top: var(--Spacing-x-one-and-half);
}
.list li {
display: flex;
align-items: center;
gap: var(--Spacing-x1);
padding-left: var(--Spacing-x1);
}
.list li svg {
flex-shrink: 0;
}
.parking details > div {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}

View File

@@ -22,8 +22,3 @@
.noIcon {
margin-left: var(--Spacing-x4);
}
.list {
font-family: var(--typography-Body-Regular-fontFamily);
list-style: inside;
}

View File

@@ -2,29 +2,21 @@ import { useIntl } from "react-intl"
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
import Contact from "@/components/HotelReservation/Contact"
import { AccessibilityIcon } 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 Accessibility from "./Accordions/Accessibility"
import CheckinCheckOut from "./Accordions/CheckInCheckOut"
import MeetingsAndConferences from "./Accordions/MeetingsAndConferences"
import Parking from "./Accordions/Parking"
import Restaurant from "./Accordions/Restaurant"
import styles from "./hotelSidePeek.module.css"
import type { HotelSidePeekProps } from "@/types/components/hotelReservation/hotelSidePeek"
import type { ParkingProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import { IconName } from "@/types/components/icon"
import type { Amenities, Hotel } from "@/types/hotel"
function getAmenitiesList(hotel: Hotel) {
const detailedAmenities: Amenities = hotel.detailedFacilities.filter(
// Remove Parking facilities since parking accordion is based on hotel.parking
(facility) => !facility.name.startsWith("Parking") && facility.public
)
return detailedAmenities
}
export default function HotelSidePeek({
hotel,
@@ -33,7 +25,12 @@ export default function HotelSidePeek({
showCTA,
}: HotelSidePeekProps) {
const intl = useIntl()
const amenitiesList = getAmenitiesList(hotel)
const amenitiesList = hotel.detailedFacilities.filter(
(facility) => facility.public
)
const parking = hotel.parking.filter(
(p) => p?.numberOfParkingSpots || p?.numberOfChargingSpaces || p?.address
)
return (
<SidePeek
@@ -42,30 +39,38 @@ export default function HotelSidePeek({
handleClose={close}
>
<div className={styles.content}>
<Subtitle>
<Subtitle color="baseTextHighContrast">
{intl.formatMessage({ id: "Practical information" })}
</Subtitle>
<Contact hotel={hotel} />
<Accordion>
{/* parking */}
{hotel.parking.length ? (
<AccordionItem
title={intl.formatMessage({ id: "Parking" })}
icon={IconName.Parking}
>
{hotel.parking.map((p) => (
<Parking key={p.name} parking={p} />
))}
</AccordionItem>
) : null}
<div className={styles.amenity}>
<AccessibilityIcon
width={24}
height={24}
color="uiTextMediumContrast"
{parking?.length > 0 && <Parking parking={parking} />}
{hotel.hotelContent?.restaurantsOverviewPage
?.restaurantsContentDescriptionMedium && (
<Restaurant
restaurantsContentDescriptionMedium={
hotel.hotelContent.restaurantsOverviewPage
.restaurantsContentDescriptionMedium
}
/>
{intl.formatMessage({ id: "Accessibility" })}
</div>
)}
{hotel?.accessibilityElevatorPitchText && (
<Accessibility
accessibilityElevatorPitchText={
hotel.accessibilityElevatorPitchText
}
/>
)}
{hotel.hotelFacts?.checkin && (
<CheckinCheckOut checkin={hotel.hotelFacts.checkin} />
)}
{hotel.hotelContent.texts?.meetingDescription?.medium && (
<MeetingsAndConferences
meetingDescription={
hotel.hotelContent.texts.meetingDescription.medium
}
/>
)}
{amenitiesList.map((amenity) => {
const Icon = mapFacilityToIcon(amenity.id)
return (
@@ -84,54 +89,13 @@ export default function HotelSidePeek({
)
})}
</Accordion>
{showCTA && (
/* TODO: handle linking to Hotel Page */
<Button theme={"base"}>To the hotel</Button>
)}
{/* TODO: handle linking to Hotel Page */}
{/* {showCTA && (
<Button theme="base" intent="secondary" size="large">
Read more about the hotel
</Button>
)} */}
</div>
</SidePeek>
)
}
function Parking({ parking }: ParkingProps) {
const intl = useIntl()
return (
<div>
<Body>
{`${intl.formatMessage({ id: parking.type })}${parking?.name ? ` (${parking.name})` : ""}`}
</Body>
<ul className={styles.list}>
{parking?.numberOfChargingSpaces !== undefined && (
<li>
{intl.formatMessage(
{ id: "Number of charging points for electric cars" },
{ number: parking.numberOfChargingSpaces }
)}
</li>
)}
{parking?.canMakeReservation && (
<li>{`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${parking.canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}</li>
)}
{parking?.numberOfParkingSpots !== undefined && (
<li>
{intl.formatMessage(
{ id: "Number of parking spots" },
{ number: parking.numberOfParkingSpots }
)}
</li>
)}
{parking?.distanceToHotel !== undefined && (
<li>
{intl.formatMessage(
{ id: "Distance to hotel" },
{ distance: parking.distanceToHotel }
)}
</li>
)}
{parking?.address && (
<li>{`${intl.formatMessage({ id: "Address" })}: ${parking.address}`}</li>
)}
</ul>
</div>
)
}

View File

@@ -5,6 +5,8 @@ import { useRef } from "react"
import { ChevronDownIcon } from "@/components/Icons"
import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
import Body from "../../Text/Body"
import Subtitle from "../../Text/Subtitle"
import { accordionItemVariants } from "./variants"
import styles from "./accordionItem.module.css"
@@ -49,7 +51,23 @@ export default function AccordionItem({
<details ref={detailsRef} onToggle={toggleAccordion}>
<summary className={styles.summary}>
{IconComp && <IconComp className={styles.icon} color="burgundy" />}
<span className={styles.title}>{title}</span>
{variant === "card" ? (
<Body
textTransform="bold"
color="baseTextHighContrast"
className={styles.title}
>
{title}
</Body>
) : (
<Subtitle
className={styles.title}
type="two"
color="baseTextHighContrast"
>
{title}
</Subtitle>
)}
<ChevronDownIcon
className={styles.chevron}
color="burgundy"

View File

@@ -63,7 +63,7 @@
color: var(--Scandic-Brand-Pale-Peach);
}
.baseTextMediumContrast {
.baseTextHighContrast {
color: var(--Base-Text-High-contrast);
}

View File

@@ -9,7 +9,7 @@ const config = {
burgundy: styles.burgundy,
baseTextDisabled: styles.baseTextDisabled,
pale: styles.pale,
baseTextMediumContrast: styles.baseTextMediumContrast,
baseTextHighContrast: styles.baseTextHighContrast,
uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast,
uiTextPlaceholder: styles.uiTextPlaceholder,