feat: bedtypes is selectable again

This commit is contained in:
Simon Emanuelsson
2025-04-07 13:43:52 +02:00
committed by Michael Zetterberg
parent f62723c6e5
commit afb37d0cc5
69 changed files with 2135 additions and 2349 deletions

View File

@@ -0,0 +1,8 @@
.bookingCodeFilter {
display: flex;
justify-content: flex-end;
}
.bookingCodeFilterSelect {
min-width: 200px;
}

View File

@@ -0,0 +1,102 @@
"use client"
import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client"
import { useRatesStore } from "@/stores/select-rate"
import Select from "@/components/TempDesignSystem/Select"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import useLang from "@/hooks/useLang"
import styles from "./bookingCodeFilter.module.css"
import type { Key } from "react"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
import { RateTypeEnum } from "@/types/enums/rateType"
export default function BookingCodeFilter() {
const intl = useIntl()
const lang = useLang()
const utils = trpc.useUtils()
const {
actions: { appendRegularRates, selectFilter },
bookingRoom,
rooms,
selectedFilter,
selectedPackages,
} = useRoomContext()
const booking = useRatesStore((state) => state.booking)
const bookingCodeFilterItems = [
{
label: intl.formatMessage({ id: "Discounted rooms" }),
value: BookingCodeFilterEnum.Discounted,
},
{
label: intl.formatMessage({ id: "Full-priced rooms" }),
value: BookingCodeFilterEnum.Regular,
},
{
label: intl.formatMessage({ id: "All rooms" }),
value: BookingCodeFilterEnum.All,
},
]
async function handleChangeFilter(selectedFilter: Key) {
selectFilter(selectedFilter as BookingCodeFilterEnum)
const room = await utils.hotel.availability.selectRate.room.fetch({
booking: {
...booking,
room: {
...bookingRoom,
bookingCode:
selectedFilter === BookingCodeFilterEnum.Discounted
? booking.bookingCode
: undefined,
packages: selectedPackages.map((pkg) => pkg.code),
},
},
lang,
})
appendRegularRates(room?.roomConfigurations)
}
const hideFilterDespiteBookingCode =
rooms.length &&
rooms.every((room) =>
room.products.every((product) => {
const isRedemption = Array.isArray(product)
if (isRedemption) {
return true
}
const isCorporateCheque =
product.rateDefinition?.rateType === RateTypeEnum.CorporateCheque
const isVoucher =
product.rateDefinition?.rateType === RateTypeEnum.Voucher
return isCorporateCheque || isVoucher
})
)
if (
(booking.bookingCode && hideFilterDespiteBookingCode) ||
!booking.bookingCode
) {
return null
}
return (
<div className={styles.bookingCodeFilter}>
<Select
aria-label={intl.formatMessage({ id: "Booking Code filter" })}
className={styles.bookingCodeFilterSelect}
name="bookingCodeFilter"
onSelect={handleChangeFilter}
label=""
items={bookingCodeFilterItems}
defaultSelectedKey={selectedFilter}
/>
</div>
)
}

View File

@@ -0,0 +1,40 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./petRoom.module.css"
export default function PetRoomMessage() {
const intl = useIntl()
const { petRoomPackage } = useRoomContext()
if (!petRoomPackage) {
return null
}
return (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p className={styles.additionalInformation}>
{intl.formatMessage(
{
id: "Pet-friendly rooms include a charge of approx. <b>{price}/stay</b>",
},
{
b: (str) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.additionalInformationPrice}>{str}</span>
</Typography>
),
price: formatPrice(
intl,
petRoomPackage.localPrice.price,
petRoomPackage.localPrice.currency
),
}
)}
</p>
</Typography>
)
}

View File

@@ -0,0 +1,8 @@
.additionalInformation {
color: var(--Text-Tertiary);
padding: var(--Space-x1) var(--Space-x15);
}
.additionalInformationPrice {
color: var(--Text-Default);
}

View File

@@ -0,0 +1,52 @@
.checkboxWrapper {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--Spacing-x-one-and-half);
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
cursor: pointer;
border-radius: var(--Corner-radius-Medium);
transition: background-color 0.3s;
color: var(--Text-Default);
}
.checkboxWrapper:hover {
background-color: var(--UI-Input-Controls-Surface-Hover);
}
.checkbox {
width: 24px;
height: 24px;
min-width: 24px;
border: 1px solid var(--UI-Input-Controls-Border-Normal);
border-radius: var(--Corner-radius-Small);
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--UI-Input-Controls-Surface-Normal);
}
.checkboxWrapper[data-selected] .checkbox {
border-color: var(--UI-Input-Controls-Fill-Selected);
background-color: var(--UI-Input-Controls-Fill-Selected);
}
.checkboxWrapper[data-disabled] .checkbox {
border-color: var(--UI-Input-Controls-Border-Disabled);
background-color: var(--UI-Input-Controls-Surface-Disabled);
}
.checkboxWrapper[data-disabled] .text {
color: var(--Base-Text-Disabled);
}
@media screen and (max-width: 767px) {
.checkboxWrapper:hover {
background-color: transparent;
}
.checkboxWrapper[data-selected] {
background-color: transparent;
}
}

View File

@@ -0,0 +1,80 @@
"use client"
import { Fragment } from "react"
import { Checkbox, CheckboxGroup } from "react-aria-components"
import { Controller, useFormContext } from "react-hook-form"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useRatesStore } from "@/stores/select-rate"
import { getIconNameByPackageCode } from "../../utils"
import PetRoomMessage from "./PetRoomMessage"
import {
checkIsAllergyRoom,
checkIsPetRoom,
includesAllergyRoom,
includesPetRoom,
} from "./utils"
import styles from "./checkbox.module.css"
import type { FormValues } from "../formValues"
export default function Checkboxes() {
const packageOptions = useRatesStore((state) => state.packageOptions)
const { control } = useFormContext<FormValues>()
return (
<Controller
control={control}
name="selectedPackages"
render={({ field }) => {
const allergyRoomSelected = includesAllergyRoom(field.value)
const petRoomSelected = includesPetRoom(field.value)
return (
<CheckboxGroup {...field}>
<div>
{packageOptions.map((option) => {
const isAllergyRoom = checkIsAllergyRoom(option.code)
const isPetRoom = checkIsPetRoom(option.code)
const isDisabled =
(isPetRoom && allergyRoomSelected) ||
(isAllergyRoom && petRoomSelected)
const isSelected = field.value.includes(option.code)
const iconName = getIconNameByPackageCode(option.code)
return (
<Fragment key={option.code}>
<Checkbox
key={option.code}
className={styles.checkboxWrapper}
isDisabled={isDisabled}
value={option.code}
>
<span className={styles.checkbox}>
{isSelected ? (
<MaterialIcon icon="check" color="Icon/Inverted" />
) : null}
</span>
<Typography
className={styles.text}
variant="Body/Paragraph/mdRegular"
>
<span>{option.description}</span>
</Typography>
{iconName ? (
<MaterialIcon icon={iconName} color="Icon/Default" />
) : null}
</Checkbox>
{isPetRoom ? <PetRoomMessage /> : null}
</Fragment>
)
})}
</div>
</CheckboxGroup>
)
}}
/>
)
}

View File

@@ -0,0 +1,18 @@
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { PackageEnum } from "@/types/requests/packages"
export function includesAllergyRoom(codes: PackageEnum[]) {
return codes.includes(RoomPackageCodeEnum.ALLERGY_ROOM)
}
export function includesPetRoom(codes: PackageEnum[]) {
return codes.includes(RoomPackageCodeEnum.PET_ROOM)
}
export function checkIsAllergyRoom(code: PackageEnum) {
return code === RoomPackageCodeEnum.ALLERGY_ROOM
}
export function checkIsPetRoom(code: PackageEnum) {
return code === RoomPackageCodeEnum.PET_ROOM
}

View File

@@ -0,0 +1,11 @@
.footer {
display: grid;
gap: var(--Space-x1);
padding: 0 var(--Space-x15);
}
.buttonContainer {
align-items: center;
display: flex;
justify-content: space-between;
}

View File

@@ -0,0 +1,5 @@
import type { PackageEnum } from "@/types/requests/packages"
export type FormValues = {
selectedPackages: PackageEnum[]
}

View File

@@ -0,0 +1,98 @@
"use client"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@/lib/trpc/client"
import { useRatesStore } from "@/stores/select-rate"
import Divider from "@/components/TempDesignSystem/Divider"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import useLang from "@/hooks/useLang"
import Checkboxes from "./Checkboxes"
import styles from "./form.module.css"
import type { PackageEnum } from "@/types/requests/packages"
import type { FormValues } from "./formValues"
export default function Form({ close }: { close: VoidFunction }) {
const intl = useIntl()
const lang = useLang()
const utils = trpc.useUtils()
const {
actions: { removeSelectedPackages, selectPackages, updateRooms },
bookingRoom,
selectedPackages,
} = useRoomContext()
const booking = useRatesStore((state) => state.booking)
const methods = useForm<FormValues>({
values: {
selectedPackages: selectedPackages.map((pkg) => pkg.code),
},
})
async function getFilteredRates(packages: PackageEnum[]) {
const filterRates = await utils.hotel.availability.selectRate.room.fetch({
booking: {
fromDate: booking.fromDate,
hotelId: booking.hotelId,
searchType: booking.searchType,
toDate: booking.toDate,
room: {
...bookingRoom,
bookingCode: bookingRoom.rateCode
? bookingRoom.bookingCode
: booking.bookingCode,
packages,
},
},
lang,
})
updateRooms(filterRates?.roomConfigurations)
}
function clearSelectedPackages() {
removeSelectedPackages()
close()
getFilteredRates([])
}
function onSubmit(data: FormValues) {
selectPackages(data.selectedPackages)
close()
getFilteredRates(data.selectedPackages)
}
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Checkboxes />
<div className={styles.footer}>
<Divider color="borderDividerSubtle" />
<div className={styles.buttonContainer}>
<Typography variant="Body/Supporting text (caption)/smBold">
<Button
onPress={clearSelectedPackages}
size="Small"
variant="Text"
>
{intl.formatMessage({ id: "Clear" })}
</Button>
</Typography>
<Typography variant="Body/Supporting text (caption)/smBold">
<Button variant="Tertiary" size="Small" type="submit">
{intl.formatMessage({ id: "Apply" })}
</Button>
</Typography>
</div>
</div>
</form>
</FormProvider>
)
}

View File

@@ -0,0 +1,101 @@
"use client"
import { useState } from "react"
import {
Button as AriaButton,
Dialog,
DialogTrigger,
Popover,
} from "react-aria-components"
import { useIntl } from "react-intl"
import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { trpc } from "@/lib/trpc/client"
import { useRatesStore } from "@/stores/select-rate"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import useLang from "@/hooks/useLang"
import Form from "./Form"
import { getIconNameByPackageCode } from "./utils"
import styles from "./roomPackageFilter.module.css"
import type { PackageEnum } from "@/types/requests/packages"
export default function RoomPackageFilter() {
const intl = useIntl()
const lang = useLang()
const utils = trpc.useUtils()
const [isOpen, setIsOpen] = useState(false)
const {
actions: { removeSelectedPackage, updateRooms },
bookingRoom,
selectedPackages,
} = useRoomContext()
const booking = useRatesStore((state) => state.booking)
async function deleteSelectedPackage(code: PackageEnum) {
removeSelectedPackage(code)
const filterRates = await utils.hotel.availability.selectRate.room.fetch({
booking: {
fromDate: booking.fromDate,
hotelId: booking.hotelId,
searchType: booking.searchType,
toDate: booking.toDate,
room: {
...bookingRoom,
bookingCode: bookingRoom.rateCode
? bookingRoom.bookingCode
: booking.bookingCode,
packages: selectedPackages
.filter((pkg) => pkg.code !== code)
.map((pkg) => pkg.code),
},
},
lang,
})
updateRooms(filterRates?.roomConfigurations)
}
return (
<div className={styles.roomPackageFilter}>
{selectedPackages.map((pkg) => (
<AriaButton
key={pkg.code}
className={styles.activeFilterButton}
onPress={() => deleteSelectedPackage(pkg.code)}
>
<MaterialIcon
icon={getIconNameByPackageCode(pkg.code)}
size={16}
color="Icon/Interactive/Default"
/>
<MaterialIcon
icon="close"
size={16}
color="Icon/Interactive/Default"
/>
</AriaButton>
))}
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
<ChipButton variant="Outlined">
{intl.formatMessage({ id: "Room preferences" })}
<MaterialIcon
icon="keyboard_arrow_down"
size={20}
color="CurrentColor"
/>
</ChipButton>
<Popover placement="bottom end">
<Dialog className={styles.dialog}>
<Form close={() => setIsOpen(false)} />
</Dialog>
</Popover>
</DialogTrigger>
</div>
)
}

View File

@@ -0,0 +1,28 @@
.roomPackageFilter {
display: flex;
gap: var(--Space-x1);
}
.activeFilterButton {
display: flex;
justify-content: center;
align-items: center;
padding: 0 var(--Space-x1);
gap: var(--Space-x05);
border-radius: var(--Corner-radius-Small);
background-color: var(--Surface-Secondary-Default-dark);
border-width: 0;
cursor: pointer;
}
.dialog {
display: grid;
gap: var(--Space-x1);
padding: var(--Space-x2);
flex-direction: column;
align-items: flex-end;
border-radius: var(--Corner-radius-md);
background-color: var(--Surface-Primary-Default);
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
max-width: 340px;
}

View File

@@ -0,0 +1,19 @@
import type { SymbolCodepoints } from "react-material-symbols"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { PackageEnum } from "@/types/requests/packages"
export function getIconNameByPackageCode(
packageCode: PackageEnum
): SymbolCodepoints {
switch (packageCode) {
case RoomPackageCodeEnum.PET_ROOM:
return "pets"
case RoomPackageCodeEnum.ACCESSIBILITY_ROOM:
return "accessible"
case RoomPackageCodeEnum.ALLERGY_ROOM:
return "mode_fan"
default:
return "star"
}
}

View File

@@ -5,15 +5,15 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import BookingCodeFilter from "../BookingCodeFilter"
import RoomPackageFilter from "../RoomPackageFilter"
import BookingCodeFilter from "./BookingCodeFilter"
import RoomPackageFilter from "./RoomPackageFilter"
import styles from "./roomsHeader.module.css"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
export default function RoomsHeader() {
const { rooms, totalRooms } = useRoomContext()
const { isFetchingPackages, rooms, totalRooms } = useRoomContext()
const intl = useIntl()
const availableRooms = rooms.filter(
@@ -42,11 +42,15 @@ export default function RoomsHeader() {
return (
<div className={styles.container}>
<Typography variant="Title/Subtitle/md" className={styles.availableRooms}>
<p>
{availableRooms !== totalRooms
? notAllRoomsAvailableText
: allRoomsAvailableText}
</p>
{isFetchingPackages ? (
<p></p>
) : (
<p>
{availableRooms !== totalRooms
? notAllRoomsAvailableText
: allRoomsAvailableText}
</p>
)}
</Typography>
<div className={styles.filters}>
<RoomPackageFilter />