Merged in feat/SW-1276-implement-design (pull request #1348)

Feat/SW-1276 implement design

* feat(SW-1276) UI implementation Desktop part 1 for MyStay

* feat(SW-1276) UI implementation Desktop part 2 for MyStay

* feat(SW-1276) UI implementation Mobile part 1 for MyStay

* refactor: move files from MyStay/MyStay to MyStay

* feat(SW-1276) Sidepeek implementation

* feat(SW-1276): Refactoring

* feat(SW-1276) UI implementation Mobile part 2 for MyStay

* feat(SW-1276): translations

* feat(SW-1276) fixed skeleton

* feat(SW-1276): Added missing translations

* feat(SW-1276): Removed console log

* feat(SW-1276) fixed translations

* feat(SW-1276): Added translations

* feat(SW-1276) fix dynamic ID:s

* feat(SW-1276) removed createElement

* feat(SW-1276): Fixed build errors

* feat(SW-1276): Updated label

* feat(SW-1276): Rewrite SummaryCard


Approved-by: Niclas Edenvin
This commit is contained in:
Pontus Dreij
2025-02-18 14:20:54 +00:00
parent 90fee1b0c4
commit 8616e4ab76
56 changed files with 4163 additions and 227 deletions

View File

@@ -0,0 +1,97 @@
import { useIntl } from "react-intl"
import { DiamondIcon, EditIcon } from "@/components/Icons"
import MembershipLevelIcon from "@/components/Levels/Icon"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import styles from "./room.module.css"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { User } from "@/types/user"
export default function GuestDetails({
user,
booking,
isMobile = false,
}: {
user: User | null
booking: BookingConfirmation["booking"]
isMobile?: boolean
}) {
const intl = useIntl()
const containerClass = isMobile
? styles.guestDetailsMobile
: styles.guestDetailsDesktop
return (
<div className={containerClass}>
{user?.membership && (
<div className={styles.userDetails}>
<div className={styles.row}>
<div className={styles.rowTitle}>
<Caption
type="bold"
color="burgundy"
textTransform="uppercase"
textAlign="center"
>
{intl.formatMessage({ id: "Your member tier" })}
</Caption>
</div>
<MembershipLevelIcon
level={user.membership.membershipLevel}
color="red"
height={isMobile ? "40" : "20"}
width={isMobile ? "80" : "40"}
/>
</div>
<div className={styles.totalPoints}>
{isMobile && (
<div className={styles.totalPointsIcon}>
<DiamondIcon color="uiTextHighContrast" />
</div>
)}
<Caption
type="bold"
color="uiTextHighContrast"
textTransform="uppercase"
>
{intl.formatMessage({ id: "Total points" })}
</Caption>
<Body color="uiTextHighContrast" className={styles.totalPointsText}>
{user.membership.currentPoints}
</Body>
</div>
</div>
)}
<div className={styles.guest}>
<Body textTransform="bold" color="uiTextHighContrast">
{booking.guest.firstName} {booking.guest.lastName}
</Body>
{user?.membership && (
<Body color="uiTextHighContrast">
{intl.formatMessage({ id: "Member no." })}{" "}
{user.membership.membershipNumber}
</Body>
)}
<Caption color="uiTextHighContrast">{booking.guest.email}</Caption>
<Caption color="uiTextHighContrast">
{booking.guest.phoneNumber}
</Caption>
</div>
<Button
variant="icon"
color="burgundy"
intent={isMobile ? "secondary" : "text"}
>
<EditIcon color="burgundy" width={20} height={20} />
<Caption color="burgundy">
{intl.formatMessage({ id: "Modify guest details" })}
</Caption>
</Button>
</div>
)
}

View File

@@ -0,0 +1,298 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
import {
BedDoubleIcon,
CoffeeIcon,
ContractIcon,
DoorOpenIcon,
PersonIcon,
} from "@/components/Icons"
import RocketLaunch from "@/components/Icons/Refresh"
import Image from "@/components/Image"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import ToggleSidePeek from "../../EnterDetails/SelectedRoom/ToggleSidePeek"
import PriceDetailsModal from "../../PriceDetailsModal"
import GuestDetails from "./GuestDetails"
import styles from "./room.module.css"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import type { Hotel, Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { User } from "@/types/user"
interface RoomProps {
booking: BookingConfirmation["booking"]
room:
| (Room & {
bedType: Room["roomTypes"][number]
})
| null
hotel: Hotel
user: User | null
}
function hasBreakfastPackage(
packages: BookingConfirmation["booking"]["packages"]
) {
return packages.some(
(p) =>
p.code === BreakfastPackageEnum.REGULAR_BREAKFAST ||
p.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST ||
p.code === BreakfastPackageEnum.SPECIAL_PACKAGE_BREAKFAST
)
}
function RoomHeader({
room,
hotel,
}: {
room: RoomProps["room"]
hotel: Hotel
}) {
if (!room) return null
return (
<div className={styles.roomHeader}>
<Subtitle
textTransform="uppercase"
color="burgundy"
className={styles.roomName}
>
{room.name}
</Subtitle>
<ToggleSidePeek
hotelId={hotel.operaId}
roomTypeCode={room.roomTypes[0].code}
intent="text"
/>
</div>
)
}
export function Room({ booking, room, hotel, user }: RoomProps) {
const intl = useIntl()
const lang = useLang()
if (!room) return null
const fromDate = dt(booking.checkInDate).locale(lang)
return (
<div className={styles.roomContainer}>
<article className={styles.room}>
<RoomHeader room={room} hotel={hotel} />
<div className={styles.booking}>
<div className={styles.chipContainer}>
{booking.packages
.filter((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
)
.map((item) => {
const Icon = getIconForFeatureCode(
item.code as RoomPackageCodeEnum
)
return (
<span className={styles.chip} key={item.code}>
<Icon width={16} height={16} color="burgundy" />
</span>
)
})}
</div>
<div className={styles.images}>
{room.images.slice(0, 2).map((image) => (
<Image
key={image.imageSizes.large}
src={image.imageSizes.large}
className={styles.image}
alt={room?.name ?? hotel.name}
width={700}
height={450}
/>
))}
</div>
<div className={styles.roomDetails}>
<div className={styles.bookingDetails}>
<div className={styles.row}>
<span className={styles.rowTitle}>
<ContractIcon color="grey80" width={20} height={20} />
<Body textTransform="bold" color="uiTextHighContrast">
{intl.formatMessage({ id: "Booking policy" })}
</Body>
</span>
<div className={styles.rowContent}>
<Body color="uiTextHighContrast">
{booking.rateDefinition.title}
</Body>
</div>
</div>
<div className={styles.row}>
<span className={styles.rowTitle}>
<RocketLaunch color="grey80" width={20} height={20} />
<Body textTransform="bold" color="uiTextHighContrast">
{intl.formatMessage({ id: "Rebooking" })}
</Body>
</span>
<div className={styles.rowContent}>
<Body color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Until {time}, {date}" },
{ time: "18:00", date: fromDate.format("dddd D MMM") }
)}
</Body>
</div>
</div>
{booking.packages.some((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
) && (
<div className={styles.row}>
<span className={styles.rowTitle}>
<DoorOpenIcon color="grey80" width={20} height={20} />
<Body textTransform="bold" color="uiTextHighContrast">
{intl.formatMessage({ id: "Room type" })}
</Body>
</span>
<div className={styles.rowContent}>
<Body color="uiTextHighContrast">
{booking.packages
.filter((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
)
.map((item) => item.description)
.join(", ")}
</Body>
</div>
</div>
)}
<div className={styles.row}>
<span className={styles.rowTitle}>
<PersonIcon color="grey80" width={20} height={20} />
<Body textTransform="bold" color="uiTextHighContrast">
{intl.formatMessage({ id: "Guests" })}
</Body>
</span>
<div className={styles.rowContent}>
<Body color="uiTextHighContrast">
{booking.childrenAges.length > 0
? intl.formatMessage(
{ id: "{adults} adults, {children} children" },
{
adults: booking.adults,
children: booking.childrenAges.length,
}
)
: intl.formatMessage(
{ id: "{adults} adults" },
{
adults: booking.adults,
}
)}
</Body>
</div>
</div>
<div className={styles.row}>
<span className={styles.rowTitle}>
<CoffeeIcon color="grey80" width={20} height={20} />
<Body textTransform="bold" color="uiTextHighContrast">
{intl.formatMessage({ id: "Breakfast" })}
</Body>
</span>
<div className={styles.rowContent}>
<Body color="uiTextHighContrast">
{hasBreakfastPackage(booking.packages)
? intl.formatMessage({ id: "Included" })
: intl.formatMessage({ id: "Not included" })}
</Body>
</div>
</div>
<div className={styles.row}>
<span className={styles.rowTitle}>
<BedDoubleIcon color="grey80" width={20} height={20} />
<Body textTransform="bold" color="uiTextHighContrast">
{intl.formatMessage({ id: "Bed preference" })}
</Body>
</span>
<div className={styles.rowContent}>
<Body color="uiTextHighContrast">
{room.bedType.mainBed.description}
{room.bedType.mainBed.widthRange.min ===
room.bedType.mainBed.widthRange.max
? ` (${room.bedType.mainBed.widthRange.min} ${intl.formatMessage({ id: "cm" })})`
: ` (${room.bedType.mainBed.widthRange.min} - ${room.bedType.mainBed.widthRange.max} ${intl.formatMessage({ id: "cm" })})`}
</Body>
</div>
</div>
</div>
<GuestDetails user={user} booking={booking} isMobile={false} />
</div>
<div className={styles.bookingInformation}>
<div className={styles.bookingCode}></div>
<div className={styles.priceDetails}>
<div className={styles.price}>
<Body color="uiTextHighContrast">
{intl.formatMessage({ id: "Room total" })}
</Body>
<Body color="uiTextHighContrast" textTransform="bold">
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}
</Body>
</div>
<PriceDetailsModal
fromDate={dt(booking.checkInDate).format("YYYY-MM-DD")}
toDate={dt(booking.checkOutDate).format("YYYY-MM-DD")}
rooms={[
{
adults: booking.adults,
childrenInRoom: undefined,
roomPrice: {
perNight: {
requested: undefined,
local: {
currency: booking.currencyCode,
price: booking.totalPrice,
},
},
perStay: {
requested: undefined,
local: {
currency: booking.currencyCode,
price: booking.totalPrice,
},
},
},
roomType: room.name,
},
]}
totalPrice={{
requested: undefined,
local: {
currency: booking.currencyCode,
price: booking.totalPrice,
},
}}
vat={booking.vatPercentage}
/>
</div>
</div>
</div>
</article>
<GuestDetails user={user} booking={booking} isMobile={true} />
</div>
)
}

View File

@@ -0,0 +1,284 @@
.room {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
background-color: var(--Base-Background-Primary-Normal);
padding: var(--Spacing-x3) 0;
}
@media (min-width: 768px) {
.room {
background-color: transparent;
padding: 0;
}
}
.roomHeader {
display: flex;
flex-direction: column;
width: var(--max-width-content);
margin: 0 auto;
align-items: flex-start;
gap: var(--Spacing-x1);
}
@media (min-width: 768px) {
.roomHeader {
justify-content: space-between;
align-items: center;
flex-direction: row;
}
}
.booking {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
position: relative;
width: var(--max-width-content);
margin: 0 auto;
}
@media (min-width: 768px) {
.booking {
border-radius: var(--Corner-radius-Large);
background-color: var(--Base-Background-Primary-Normal);
padding: var(--Spacing-x2);
}
}
.chipContainer {
position: absolute;
top: 300px;
left: 25px;
display: flex;
flex-direction: row;
gap: var(--Spacing-x1);
}
.chip {
background-color: var(--Main-Grey-White);
padding: var(--Spacing-x-half) var(--Spacing-x1);
border-radius: var(--Corner-radius-Small);
}
.images {
display: grid;
gap: var(--Spacing-x-one-and-half);
grid-template-columns: 1fr;
height: 210px;
overflow: hidden;
}
@media (min-width: 768px) {
.images {
height: 320px;
grid-template-columns: 1fr 1fr;
}
}
.image {
border-radius: var(--Corner-radius-Medium);
width: 100%;
height: 210px;
aspect-ratio: 16/9;
object-fit: cover;
}
.image:last-child {
display: none;
}
@media (min-width: 768px) {
.image {
height: 100%;
}
.image:last-child {
display: block;
}
}
.roomDetails {
display: grid;
gap: var(--Spacing-x5);
}
@media (min-width: 768px) {
.roomDetails {
grid-template-columns: minmax(0, 700px) 1fr;
}
}
.bookingDetails {
max-width: 100%;
padding: 0 var(--Spacing-x2);
}
@media (min-width: 768px) {
.bookingDetails {
padding: 0;
}
}
.row {
display: flex;
flex-direction: column;
padding: var(--Spacing-x-one-and-half) 0;
}
@media (min-width: 768px) {
.row {
border-bottom: 1px solid var(--Base-Border-Subtle);
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
.row:last-child {
border-bottom: none;
}
.rowTitle {
display: flex;
flex-direction: row;
gap: var(--Spacing-x1);
}
.rowTitle svg {
width: 24px;
height: 24px;
}
@media (min-width: 768px) {
.rowTitle svg {
width: 20px;
height: 20px;
}
}
.rowContent {
padding-left: var(--Spacing-x4);
}
.guestDetailsDesktop {
flex-direction: column;
align-items: flex-end;
display: none;
}
@media (min-width: 768px) {
.guestDetailsDesktop {
display: flex;
}
}
.guestDetailsMobile {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: var(--Spacing-x2);
background-color: var(--Main-Brand-PalePeach);
padding: var(--Spacing-x3) 0;
}
.guestDetailsMobile .row {
align-items: center;
}
.guestDetailsMobile .rowTitle {
margin-bottom: var(--Spacing-x1);
}
.guestDetailsMobile .userDetails {
width: calc(100% - var(--Spacing-x4) - var(--Spacing-x4));
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider);
padding-bottom: var(--Spacing-x3);
margin-bottom: var(--Spacing-x3);
}
.guestDetailsMobile .totalPoints {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: var(--Spacing-x1);
padding-top: var(--Spacing-x3);
}
.guestDetailsMobile .totalPointsText {
margin-left: auto;
}
.guestDetailsMobile .guest {
align-items: center;
}
@media (min-width: 768px) {
.guestDetailsMobile {
display: none;
}
.totalPoints {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: var(--Spacing-x-one-and-half) 0;
}
}
.guest {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-bottom: var(--Spacing-x2);
}
.bookingInformation {
display: flex;
flex-direction: column-reverse;
align-items: center;
gap: var(--Spacing-x2);
}
@media (min-width: 768px) {
.bookingInformation {
flex-direction: row;
justify-content: space-between;
}
}
.priceDetails {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--Spacing-x1);
border-top: 1px solid var(--Base-Border-Subtle);
border-bottom: 1px solid var(--Base-Border-Subtle);
padding: var(--Spacing-x-one-and-half) 0;
width: calc(100% - var(--Spacing-x4));
justify-content: center;
margin: 0 auto;
}
@media (min-width: 768px) {
.priceDetails {
border: none;
margin: 0;
width: auto;
flex-direction: column;
align-items: flex-end;
}
}
@media (min-width: 768px) {
.price {
display: flex;
gap: var(--Spacing-x1);
}
}
.userDetails {
width: 100%;
border-bottom: 1px solid var(--Base-Border-Subtle);
margin-bottom: var(--Spacing-x1);
}