feat/SW-451-ui-hotel-card (pull request #700)
Feat/SW-451 ui hotel card * feat(SW-451): initial design * feat(SW-451): add gallery icon and responsive design for mobile * feat(SW-451): refactor name, add routing to sidepeek * feat(SW-451): add updated design * feat(SW-451): add tripadvisor chip * feat(SW-451): fix gallery icon * feat(SW-451): fix additional falsy value check * feat(SW-451): fix import type * feat(SW-451): remove galleryIcon until image data exists * feat(SW-451): fix css styling * feat(SW-451): add new design for mobile * feat(SW-451): add translation * feat(SW-451): change css to mobile first * feat(SW-451): change div to article Approved-by: Matilda Landström
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
|
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
||||||
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
||||||
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
@@ -50,9 +51,8 @@ export default async function SelectRatePage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<HotelInfoCard hotelData={hotelData} />
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* TODO: Add Hotel Listing Card */}
|
|
||||||
<div>Hotel Listing Card TBI</div>
|
|
||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
<RoomSelection
|
<RoomSelection
|
||||||
roomConfigurations={roomConfigurations}
|
roomConfigurations={roomConfigurations}
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<ReadMore hotelId={hotelData.operaId} hotel={hotelData} />
|
<ReadMore
|
||||||
|
label={intl.formatMessage({ id: "See hotel details" })}
|
||||||
|
hotelId={hotelData.operaId}
|
||||||
|
hotel={hotelData}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section className={styles.prices}>
|
<section className={styles.prices}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function getAmenitiesList(hotel: Hotel) {
|
|||||||
return [...detailedAmenities, ...simpleAmenities]
|
return [...detailedAmenities, ...simpleAmenities]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReadMore({ hotel, hotelId }: ReadMoreProps) {
|
export default function ReadMore({ label, hotel, hotelId }: ReadMoreProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const [sidePeekOpen, setSidePeekOpen] = useState(false)
|
const [sidePeekOpen, setSidePeekOpen] = useState(false)
|
||||||
@@ -46,11 +46,12 @@ export default function ReadMore({ hotel, hotelId }: ReadMoreProps) {
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSidePeekOpen(true)
|
setSidePeekOpen(true)
|
||||||
}}
|
}}
|
||||||
intent={"text"}
|
intent="text"
|
||||||
color="burgundy"
|
theme="base"
|
||||||
|
wrapping
|
||||||
className={styles.detailsButton}
|
className={styles.detailsButton}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "See hotel details" })}
|
{label}
|
||||||
<ChevronRightIcon color="burgundy" />
|
<ChevronRightIcon color="burgundy" />
|
||||||
</Button>
|
</Button>
|
||||||
<SidePeek
|
<SidePeek
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
.container {
|
||||||
|
background-color: var(--Base-Surface-Subtle-Normal);
|
||||||
|
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: var(--max-width-navigation);
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageWrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 200px;
|
||||||
|
max-width: 360px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tripAdvisor {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
position: absolute;
|
||||||
|
left: var(--Spacing-x2);
|
||||||
|
top: var(--Spacing-x2);
|
||||||
|
padding: 0 var(--Spacing-x1);
|
||||||
|
border-radius: var(--Corner-radius-Small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelInformation {
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
width: min(607px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
margin-top: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilities {
|
||||||
|
padding: var(--Spacing-x3) 0 var(--Spacing-x-quarter);
|
||||||
|
gap: var(--Spacing-x-one-and-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilityList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--Spacing-x-one-and-half);
|
||||||
|
padding-bottom: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilitiesItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilityName {
|
||||||
|
color: var(--UI-Text-Medium-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.container {
|
||||||
|
padding: var(--Spacing-x4) var(--Spacing-x5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelContent {
|
||||||
|
gap: var(--Spacing-x6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelInformation {
|
||||||
|
padding-right: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilities {
|
||||||
|
padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilityList {
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
padding-bottom: var(--Spacing-x-half);
|
||||||
|
}
|
||||||
|
.facilityTitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.hotelContent {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.imageWrapper {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
|
import TripAdvisorIcon from "@/components/Icons/TripAdvisor"
|
||||||
|
import Image from "@/components/Image"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
|
||||||
|
import ReadMore from "../../ReadMore"
|
||||||
|
|
||||||
|
import styles from "./hotelInfoCard.module.css"
|
||||||
|
|
||||||
|
import type { HotelInfoCardProps } from "@/types/components/hotelReservation/selectRate/hotelInfoCardProps"
|
||||||
|
|
||||||
|
export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
|
||||||
|
const hotelAttributes = hotelData?.data.attributes
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const sortedFacilities = hotelAttributes?.detailedFacilities
|
||||||
|
.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||||
|
.slice(0, 5)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className={styles.container}>
|
||||||
|
{hotelAttributes && (
|
||||||
|
<section className={styles.wrapper}>
|
||||||
|
<div className={styles.imageWrapper}>
|
||||||
|
<Image
|
||||||
|
src={hotelAttributes.hotelContent.images.imageSizes.medium}
|
||||||
|
alt={hotelAttributes.hotelContent.images.metaData.altText}
|
||||||
|
className={styles.image}
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
{hotelAttributes.ratings?.tripAdvisor && (
|
||||||
|
<div className={styles.tripAdvisor}>
|
||||||
|
<TripAdvisorIcon color="burgundy" />
|
||||||
|
<Caption color="burgundy">
|
||||||
|
{hotelAttributes.ratings.tripAdvisor.rating}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* TODO: gallery icon and image carousel */}
|
||||||
|
</div>
|
||||||
|
<div className={styles.hotelContent}>
|
||||||
|
<div className={styles.hotelInformation}>
|
||||||
|
<Title
|
||||||
|
level="h3"
|
||||||
|
textTransform="uppercase"
|
||||||
|
className={styles.title}
|
||||||
|
>
|
||||||
|
{hotelAttributes.name}
|
||||||
|
</Title>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${hotelAttributes.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
|
||||||
|
</Caption>
|
||||||
|
<Body color="uiTextHighContrast" className={styles.body}>
|
||||||
|
{hotelAttributes.hotelContent.texts.descriptions.medium}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<Divider color="subtle" variant="vertical" />
|
||||||
|
<div className={styles.facilities}>
|
||||||
|
<div className={styles.facilityList}>
|
||||||
|
<Body textTransform="bold" className={styles.facilityTitle}>
|
||||||
|
{intl.formatMessage({ id: "At the hotel" })}
|
||||||
|
</Body>
|
||||||
|
{sortedFacilities?.map((facility) => {
|
||||||
|
const IconComponent = mapFacilityToIcon(facility.name)
|
||||||
|
return (
|
||||||
|
<div className={styles.facilitiesItem} key={facility.id}>
|
||||||
|
{IconComponent && (
|
||||||
|
<IconComponent
|
||||||
|
className={styles.facilitiesIcon}
|
||||||
|
color="grey80"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Caption className={styles.facilityName}>
|
||||||
|
{facility.name}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<ReadMore
|
||||||
|
label={intl.formatMessage({ id: "Show all amenities" })}
|
||||||
|
hotelId={hotelAttributes.operaId}
|
||||||
|
hotel={hotelAttributes}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ export interface DetailedAmenity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ReadMoreProps {
|
export interface ReadMoreProps {
|
||||||
|
label: string
|
||||||
hotelId: string
|
hotelId: string
|
||||||
hotel: Hotel
|
hotel: Hotel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { HotelData } from "@/types/hotel"
|
||||||
|
|
||||||
|
export type HotelInfoCardProps = {
|
||||||
|
hotelData: HotelData | null
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user