Merged in feat/SW-2113-allow-feature-combinations (pull request #1719)
Feat/SW-2113 allow feature combinations * feat(SW-2113): Refactor features data to be fetched on filter room filter change * feat(SW-2113): added loading state * fix: now clear room selection when applying filter and room doesnt exists. And added room features to mobile summary * fix * fix: add package to price details * feat(SW-2113): added buttons to room filter * fix: active room * fix: remove console log * fix: added form and close handler to room package filter * fix: add restriction so you cannot select pet room with allergy room and vice versa * fix: fixes from review feedback * fix * fix: hide modify button if on nextcoming rooms if no selection is made, and adjust filter logic in togglePackage * fix: forgot to use roomFeatureCodes from input.. * fix: naming Approved-by: Simon.Emanuelsson
This commit is contained in:
@@ -20,9 +20,10 @@ import { RateEnum } from "@/types/enums/rate"
|
||||
|
||||
export default function SelectedRoomPanel() {
|
||||
const intl = useIntl()
|
||||
const { isUserLoggedIn, roomCategories } = useRatesStore((state) => ({
|
||||
const { isUserLoggedIn, roomCategories, rooms } = useRatesStore((state) => ({
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
roomCategories: state.roomCategories,
|
||||
rooms: state.rooms,
|
||||
}))
|
||||
const {
|
||||
actions: { modifyRate },
|
||||
@@ -89,6 +90,10 @@ export default function SelectedRoomPanel() {
|
||||
return null
|
||||
}
|
||||
|
||||
const showModifyButton =
|
||||
isMainRoom ||
|
||||
(!isMainRoom && rooms.slice(0, roomNr).every((room) => room.selectedRate))
|
||||
|
||||
return (
|
||||
<div className={styles.selectedRoomPanel}>
|
||||
<div className={styles.content}>
|
||||
@@ -118,14 +123,16 @@ export default function SelectedRoomPanel() {
|
||||
width={600}
|
||||
/>
|
||||
) : null}
|
||||
<div className={styles.modifyButtonContainer}>
|
||||
<Button clean onClick={modifyRate}>
|
||||
<Chip size="small" variant="uiTextHighContrast">
|
||||
<MaterialIcon icon="edit_square" />
|
||||
{intl.formatMessage({ id: "Modify" })}
|
||||
</Chip>
|
||||
</Button>
|
||||
</div>
|
||||
{showModifyButton && (
|
||||
<div className={styles.modifyButtonContainer}>
|
||||
<Button clean onClick={modifyRate}>
|
||||
<Chip size="small" variant="uiTextHighContrast">
|
||||
<MaterialIcon icon="edit_square" />
|
||||
{intl.formatMessage({ id: "Modify" })}
|
||||
</Chip>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -32,6 +32,15 @@
|
||||
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;
|
||||
|
||||
@@ -14,6 +14,7 @@ interface CheckboxProps {
|
||||
value: string
|
||||
isSelected: boolean
|
||||
iconName: MaterialSymbolProps["icon"]
|
||||
isDisabled: boolean
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
@@ -22,12 +23,14 @@ export default function Checkbox({
|
||||
name,
|
||||
value,
|
||||
iconName,
|
||||
isDisabled,
|
||||
onChange,
|
||||
}: CheckboxProps) {
|
||||
return (
|
||||
<AriaCheckbox
|
||||
className={styles.checkboxWrapper}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
onChange={() => onChange(value)}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
@@ -35,7 +38,10 @@ export default function Checkbox({
|
||||
<span className={styles.checkbox}>
|
||||
{isSelected && <MaterialIcon icon="check" color="Icon/Inverted" />}
|
||||
</span>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdRegular"
|
||||
className={styles.text}
|
||||
>
|
||||
<span>{name}</span>
|
||||
</Typography>
|
||||
{iconName ? (
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
"use client"
|
||||
import { Button, Dialog, DialogTrigger, Popover } from "react-aria-components"
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
Button as AriaButton,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Popover,
|
||||
} from "react-aria-components"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
@@ -16,20 +24,46 @@ import { getIconNameByPackageCode } from "./utils"
|
||||
|
||||
import styles from "./roomPackageFilter.module.css"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
|
||||
type FormValues = {
|
||||
selectedPackages: RoomPackageCodeEnum[]
|
||||
}
|
||||
|
||||
export default function RoomPackageFilter() {
|
||||
const intl = useIntl()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const packageOptions = useRatesStore((state) => state.packageOptions)
|
||||
const {
|
||||
actions: { togglePackage },
|
||||
actions: { togglePackages },
|
||||
selectedPackages,
|
||||
} = useRoomContext()
|
||||
const intl = useIntl()
|
||||
|
||||
const { setValue, handleSubmit, control } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
selectedPackages: selectedPackages,
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setValue("selectedPackages", selectedPackages)
|
||||
}, [selectedPackages, setValue])
|
||||
|
||||
function onSubmit(data: FormValues) {
|
||||
togglePackages(data.selectedPackages)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.roomPackageFilter}>
|
||||
{selectedPackages.map((pkg) => (
|
||||
<Button
|
||||
<AriaButton
|
||||
key={pkg}
|
||||
onPress={() => togglePackage(pkg)}
|
||||
onPress={() => {
|
||||
const packages = selectedPackages.filter((s) => s !== pkg)
|
||||
togglePackages(packages)
|
||||
}}
|
||||
className={styles.activeFilterButton}
|
||||
>
|
||||
<MaterialIcon
|
||||
@@ -42,9 +76,9 @@ export default function RoomPackageFilter() {
|
||||
size={16}
|
||||
color="Icon/Interactive/Default"
|
||||
/>
|
||||
</Button>
|
||||
</AriaButton>
|
||||
))}
|
||||
<DialogTrigger>
|
||||
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<ChipButton variant="Outlined">
|
||||
{intl.formatMessage({ id: "Room preferences" })}
|
||||
<MaterialIcon
|
||||
@@ -55,39 +89,105 @@ export default function RoomPackageFilter() {
|
||||
</ChipButton>
|
||||
<Popover placement="bottom end">
|
||||
<Dialog className={styles.dialog}>
|
||||
<div className={styles.filters}>
|
||||
{packageOptions.map((option) => (
|
||||
<Checkbox
|
||||
key={option.code}
|
||||
name={option.description}
|
||||
value={option.code}
|
||||
iconName={getIconNameByPackageCode(option.code)}
|
||||
isSelected={selectedPackages.includes(option.code)}
|
||||
onChange={() => togglePackage(option.code)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<Divider color="borderDividerSubtle" />
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.additionalInformation}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.",
|
||||
},
|
||||
{
|
||||
b: (str) => (
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span className={styles.additionalInformationPrice}>
|
||||
{str}
|
||||
</span>
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="selectedPackages"
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
{packageOptions.map((option) => {
|
||||
const isPetRoom =
|
||||
option.code === RoomPackageCodeEnum.PET_ROOM
|
||||
|
||||
const isAllergyRoom =
|
||||
option.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||
|
||||
const hasPetRoom = field.value.includes(
|
||||
RoomPackageCodeEnum.PET_ROOM
|
||||
)
|
||||
|
||||
const hasAllergyRoom = field.value.includes(
|
||||
RoomPackageCodeEnum.ALLERGY_ROOM
|
||||
)
|
||||
|
||||
const isDisabled =
|
||||
(isPetRoom && hasAllergyRoom) ||
|
||||
(isAllergyRoom && hasPetRoom)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
key={option.code}
|
||||
name={option.description}
|
||||
value={option.code}
|
||||
iconName={getIconNameByPackageCode(option.code)}
|
||||
isSelected={field.value.includes(option.code)}
|
||||
isDisabled={isDisabled}
|
||||
onChange={() => {
|
||||
const isSelected = field.value.includes(
|
||||
option.code
|
||||
)
|
||||
const newValue = isSelected
|
||||
? field.value.filter(
|
||||
(pkg) => pkg !== option.code
|
||||
)
|
||||
: [...field.value, option.code]
|
||||
field.onChange(newValue)
|
||||
}}
|
||||
/>
|
||||
{option.code === RoomPackageCodeEnum.PET_ROOM && (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.additionalInformation}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.",
|
||||
},
|
||||
{
|
||||
b: (str) => (
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span
|
||||
className={
|
||||
styles.additionalInformationPrice
|
||||
}
|
||||
>
|
||||
{str}
|
||||
</span>
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<div className={styles.footer}>
|
||||
<Divider color="borderDividerSubtle" />
|
||||
<div className={styles.buttonContainer}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<Button
|
||||
variant="Text"
|
||||
size="Small"
|
||||
onPress={() => {
|
||||
togglePackages([])
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
.additionalInformation {
|
||||
color: var(--Text-Tertiary);
|
||||
padding: var(--Space-x1) var(--Space-x15);
|
||||
}
|
||||
|
||||
.additionalInformationPrice {
|
||||
@@ -40,3 +41,9 @@
|
||||
border-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { RoomCardSkeleton } from "@/components/HotelReservation/RoomCardSkeleton/RoomCardSkeleton"
|
||||
|
||||
import styles from "./roomsListSkeleton.module.css"
|
||||
|
||||
type Props = {
|
||||
count?: number
|
||||
}
|
||||
|
||||
export function RoomsListSkeleton({ count = 4 }: Props) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.skeletonContainer}>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<RoomCardSkeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,12 +2,18 @@
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import RoomListItem from "./RoomListItem"
|
||||
import { RoomsListSkeleton } from "./RoomsListSkeleton"
|
||||
import ScrollToList from "./ScrollToList"
|
||||
|
||||
import styles from "./rooms.module.css"
|
||||
|
||||
export default function RoomsList() {
|
||||
const { rooms } = useRoomContext()
|
||||
const { rooms, isFetchingRoomFeatures } = useRoomContext()
|
||||
|
||||
if (isFetchingRoomFeatures) {
|
||||
return <RoomsListSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollToList />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.roomList > li {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.container {
|
||||
max-width: var(--max-width-page);
|
||||
}
|
||||
|
||||
.skeletonContainer {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
/* used to hide overflowing rows */
|
||||
grid-template-rows: auto;
|
||||
grid-auto-rows: 0;
|
||||
overflow: hidden;
|
||||
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
Reference in New Issue
Block a user