From df32c0835082c25d27e905f90028c4d67a9311df Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 1 Apr 2025 09:54:09 +0000 Subject: [PATCH] feat(SW-2043): Added new room packages filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(SW-2043): Added new room packages filter * fix(SW-2043): Fixed issue with not updating price when selecting pet room Approved-by: Tobias Johansson Approved-by: Matilda Landström --- .../RoomsContainer/RateSummary/index.tsx | 10 +- .../RoomsContainer/RateSummary/utils.ts | 2 +- .../bookingCodeFilter.module.css | 7 -- .../Checkbox/checkbox.module.css | 43 +++++++++ .../RoomPackageFilter/Checkbox/index.tsx | 48 ++++++++++ .../Rooms/RoomPackageFilter/index.tsx | 96 +++++++++++++++++++ .../roomPackageFilter.module.css | 42 ++++++++ .../Rooms/RoomPackageFilter/utils.ts | 18 ++++ .../Rooms/RoomTypeFilter/index.tsx | 93 ------------------ .../RoomTypeFilter/roomFilter.module.css | 70 -------------- .../Rooms/RoomsHeader/index.tsx | 57 +++++++++++ .../Rooms/RoomsHeader/roomsHeader.module.css | 21 ++++ .../RoomsList/RoomListItem/Rates/index.tsx | 7 +- .../RoomListItem/RoomImage/index.tsx | 4 +- .../SelectRate/RoomsContainer/Rooms/index.tsx | 6 +- apps/scandic-web/i18n/dictionaries/da.json | 3 + apps/scandic-web/i18n/dictionaries/de.json | 3 + apps/scandic-web/i18n/dictionaries/en.json | 3 + apps/scandic-web/i18n/dictionaries/fi.json | 3 + apps/scandic-web/i18n/dictionaries/no.json | 3 + apps/scandic-web/i18n/dictionaries/sv.json | 3 + apps/scandic-web/stores/select-rate/index.ts | 54 +++++++---- .../hotelReservation/selectRate/selectRate.ts | 2 +- apps/scandic-web/types/stores/rates.ts | 6 +- .../ChipButton/ChipButton.stories.tsx | 25 ++++- .../lib/components/ChipButton/ChipButton.tsx | 15 +-- .../ChipButton/chip-button.module.css | 28 ++++-- .../lib/components/ChipButton/types.ts | 10 ++ .../lib/components/ChipButton/variants.ts | 29 ++++++ 29 files changed, 489 insertions(+), 222 deletions(-) create mode 100644 apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/checkbox.module.css create mode 100644 apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/roomPackageFilter.module.css create mode 100644 apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/utils.ts delete mode 100644 apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomTypeFilter/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomTypeFilter/roomFilter.module.css create mode 100644 apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/roomsHeader.module.css create mode 100644 packages/design-system/lib/components/ChipButton/types.ts create mode 100644 packages/design-system/lib/components/ChipButton/variants.ts diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx index 370cdc08d..ff6631e8e 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx @@ -239,7 +239,10 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { { + ( + total, + { features, packages: roomPackages, product } + ) => { if (!("member" in product) || !product.member) { return total } @@ -248,8 +251,9 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { if (!memberPrice) { return total } - const hasSelectedPetRoom = - roomPackage === RoomPackageCodeEnum.PET_ROOM + const hasSelectedPetRoom = roomPackages.includes( + RoomPackageCodeEnum.PET_ROOM + ) if (!hasSelectedPetRoom) { return total + memberPrice } diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts index 190ae82d5..439d30a0e 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts @@ -38,7 +38,7 @@ export function calculateTotalPrice( if ( petRoomPackage && isPetRoom && - room.package === RoomPackageCodeEnum.PET_ROOM + room.packages.includes(RoomPackageCodeEnum.PET_ROOM) ) { petRoomPrice = Number(petRoomPackage.localPrice.totalPrice) } diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/BookingCodeFilter/bookingCodeFilter.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/BookingCodeFilter/bookingCodeFilter.module.css index 0dd8e71b7..82a6ff8bb 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/BookingCodeFilter/bookingCodeFilter.module.css +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/BookingCodeFilter/bookingCodeFilter.module.css @@ -1,15 +1,8 @@ .bookingCodeFilter { display: flex; justify-content: flex-end; - width: 100%; } .bookingCodeFilterSelect { min-width: 200px; } - -@media screen and (max-width: 767px) { - .bookingCodeFilter { - margin-bottom: var(--Spacing-x3); - } -} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/checkbox.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/checkbox.module.css new file mode 100644 index 000000000..0d19d9c37 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/checkbox.module.css @@ -0,0 +1,43 @@ +.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); +} + +@media screen and (max-width: 767px) { + .checkboxWrapper:hover { + background-color: transparent; + } + + .checkboxWrapper[data-selected] { + background-color: transparent; + } +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/index.tsx new file mode 100644 index 000000000..9a0389cf8 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/index.tsx @@ -0,0 +1,48 @@ +"use client" + +import { Checkbox as AriaCheckbox } from "react-aria-components" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./checkbox.module.css" + +import type { MaterialSymbolProps } from "react-material-symbols" + +interface CheckboxProps { + name: string + value: string + isSelected: boolean + iconName: MaterialSymbolProps["icon"] + onChange: (value: string) => void +} + +export default function Checkbox({ + isSelected, + name, + value, + iconName, + onChange, +}: CheckboxProps) { + return ( + onChange(value)} + > + {({ isSelected }) => ( + <> + + {isSelected && } + + + {name} + + {iconName ? ( + + ) : null} + + )} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/index.tsx new file mode 100644 index 000000000..3cab3c07b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/index.tsx @@ -0,0 +1,96 @@ +"use client" +import { Button, 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" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useRatesStore } from "@/stores/select-rate" + +import Divider from "@/components/TempDesignSystem/Divider" +import { useRoomContext } from "@/contexts/SelectRate/Room" + +import Checkbox from "./Checkbox" +import { getIconNameByPackageCode } from "./utils" + +import styles from "./roomPackageFilter.module.css" + +export default function RoomPackageFilter() { + const packageOptions = useRatesStore((state) => state.packageOptions) + const { + actions: { togglePackage }, + selectedPackages, + } = useRoomContext() + const intl = useIntl() + + return ( +
+ {selectedPackages.map((pkg) => ( + + ))} + + + {intl.formatMessage({ id: "Room preferences" })} + + + + +
+ {packageOptions.map((option) => ( + togglePackage(option.code)} + /> + ))} +
+
+ + +

+ {intl.formatMessage( + { + id: "200 SEK/night Important information on pricing and features of pet-friendly rooms.", + }, + { + b: (str) => ( + + + {str} + + + ), + } + )} +

+
+
+
+
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/roomPackageFilter.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/roomPackageFilter.module.css new file mode 100644 index 000000000..c20ffca67 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/roomPackageFilter.module.css @@ -0,0 +1,42 @@ +.roomPackageFilter { + display: flex; + gap: var(--Space-x1); +} + +.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; +} + +.footer { + display: grid; + gap: var(--Space-x1); + padding: 0 var(--Space-x15); +} + +.additionalInformation { + color: var(--Text-Tertiary); +} + +.additionalInformationPrice { + color: var(--Text-Default); +} + +.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; +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/utils.ts new file mode 100644 index 000000000..7f0da0eff --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/utils.ts @@ -0,0 +1,18 @@ +import type { MaterialSymbolProps } from "react-material-symbols" + +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" + +export function getIconNameByPackageCode( + packageCode: RoomPackageCodeEnum +): MaterialSymbolProps["icon"] { + switch (packageCode) { + case RoomPackageCodeEnum.PET_ROOM: + return "pets" + case RoomPackageCodeEnum.ACCESSIBILITY_ROOM: + return "accessible" + case RoomPackageCodeEnum.ALLERGY_ROOM: + return "mode_fan" + default: + return "star" + } +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomTypeFilter/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomTypeFilter/index.tsx deleted file mode 100644 index 5736d63c4..000000000 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomTypeFilter/index.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client" -import { - type Key, - ToggleButton, - ToggleButtonGroup, -} from "react-aria-components" -import { useIntl } from "react-intl" - -import { useRatesStore } from "@/stores/select-rate" - -import Caption from "@/components/TempDesignSystem/Text/Caption" -import { useRoomContext } from "@/contexts/SelectRate/Room" - -import styles from "./roomFilter.module.css" - -import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" -import type { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" - -export default function RoomTypeFilter() { - const filterOptions = useRatesStore((state) => state.filterOptions) - const { - actions: { selectPackage }, - rooms, - selectedPackage, - totalRooms, - } = useRoomContext() - const intl = useIntl() - - const availableRooms = rooms.filter( - (room) => room.status === AvailabilityEnum.Available - ).length - - // const tooltipText = intl.formatMessage({ - // id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", - // }) - - function handleChange(selectedFilter: Set) { - if (selectedFilter.size) { - const selected = selectedFilter.values().next() - selectPackage(selected.value as RoomPackageCodeEnum) - } else { - selectPackage(undefined) - } - } - - const notAllRoomsAvailableText = intl.formatMessage( - { - id: "{availableRooms}/{numberOfRooms, plural, one {# room type} other {# room types}} available", - }, - { - availableRooms, - numberOfRooms: totalRooms, - } - ) - - const allRoomsAvailableText = intl.formatMessage( - { - id: "{numberOfRooms, plural, one {# room type} other {# room types}} available", - }, - { - numberOfRooms: totalRooms, - } - ) - - return ( -
- - {availableRooms !== totalRooms - ? notAllRoomsAvailableText - : allRoomsAvailableText} - - - {filterOptions.map((option) => ( - -
- {option.description} - - ))} - -
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomTypeFilter/roomFilter.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomTypeFilter/roomFilter.module.css deleted file mode 100644 index 91bf6cef5..000000000 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomTypeFilter/roomFilter.module.css +++ /dev/null @@ -1,70 +0,0 @@ -.container { - align-items: center; - display: grid; - gap: var(--Spacing-x3); -} - -.roomsFilter { - align-items: center; - display: flex; - flex-wrap: wrap; - gap: var(--Spacing-x2); - justify-content: flex-start; -} - -.radio { - align-items: center; - background: none; - border: none; - cursor: pointer; - display: flex; - gap: var(--Spacing-x-one-and-half); - outline: none; - padding: 0; -} - -.circle { - border: 1px solid var(--UI-Input-Controls-Border-Normal); - border-radius: 50%; - grid-area: input; - height: 24px; - position: relative; - transition: background-color 200ms ease; - width: 24px; -} - -.radio:hover .circle { - background-color: var(--UI-Input-Controls-Fill-Selected-hover); -} - -.radio[data-selected="true"] .circle { - background-color: var(--UI-Input-Controls-Fill-Selected); -} - -.radio[data-selected="true"]:hover .circle { - background-color: var(--UI-Input-Controls-Fill-Selected-hover); - border-color: var(--UI-Input-Controls-Border-Hover); -} - -.radio[data-selected="true"] .circle::after { - background-color: var(--Main-Grey-White); - border-radius: 50%; - content: ""; - height: 8px; - left: 50%; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); - width: 8px; -} - -@media screen and (min-width: 768px) { - .container { - grid-template-columns: auto 1fr; - } - - .roomsFilter { - flex-wrap: nowrap; - justify-content: flex-end; - } -} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/index.tsx new file mode 100644 index 000000000..6a5ca73e2 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/index.tsx @@ -0,0 +1,57 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useRoomContext } from "@/contexts/SelectRate/Room" + +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 intl = useIntl() + + const availableRooms = rooms.filter( + (room) => room.status === AvailabilityEnum.Available + ).length + + const notAllRoomsAvailableText = intl.formatMessage( + { + id: "{availableRooms}/{numberOfRooms, plural, one {# room type} other {# room types}} available", + }, + { + availableRooms, + numberOfRooms: totalRooms, + } + ) + + const allRoomsAvailableText = intl.formatMessage( + { + id: "{numberOfRooms, plural, one {# room type} other {# room types}} available", + }, + { + numberOfRooms: totalRooms, + } + ) + + return ( +
+ +

+ {availableRooms !== totalRooms + ? notAllRoomsAvailableText + : allRoomsAvailableText} +

+
+
+ + +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/roomsHeader.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/roomsHeader.module.css new file mode 100644 index 000000000..455f18593 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/roomsHeader.module.css @@ -0,0 +1,21 @@ +.container { + display: grid; + gap: var(--Space-x3); + align-items: center; +} + +.availableRooms { + color: var(--Text-Default); +} + +.filters { + display: flex; + gap: var(--Space-x1); + align-items: center; +} + +@media screen and (min-width: 768px) { + .container { + grid-template-columns: 1fr auto; + } +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/index.tsx index 3a8ad1f53..a379e6c76 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/index.tsx @@ -35,7 +35,7 @@ export default function Rates({ actions: { selectRate }, isFetchingAdditionalRate, selectedFilter, - selectedPackage, + selectedPackages, } = useRoomContext() const { nights, petRoomPackage } = useRatesStore((state) => ({ nights: dt(state.booking.toDate).diff(state.booking.fromDate, "days"), @@ -46,8 +46,9 @@ export default function Rates({ selectRate({ features, product, roomType, roomTypeCode }) } - const petRoomPackageSelected = - selectedPackage === RoomPackageCodeEnum.PET_ROOM + const petRoomPackageSelected = selectedPackages.includes( + RoomPackageCodeEnum.PET_ROOM + ) const sharedProps = { handleSelectRate, diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/RoomImage/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/RoomImage/index.tsx index 53702d19b..3fb93a93e 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/RoomImage/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/RoomImage/index.tsx @@ -20,7 +20,7 @@ export default function RoomImage({ roomTypeCode, }: RoomListItemImageProps) { const intl = useIntl() - const { selectedPackage } = useRoomContext() + const { selectedPackages } = useRoomContext() const roomCategories = useRatesStore((state) => state.roomCategories) const showLowInventory = roomsLeft > 0 && roomsLeft < 5 @@ -45,7 +45,7 @@ export default function RoomImage({ ) : null} {features - .filter((feature) => selectedPackage === feature.code) + .filter((feature) => selectedPackages.includes(feature.code)) .map((feature) => ( {IconForFeatureCode({ featureCode: feature.code, size: 16 })} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx index 2a7da5724..cfbbcc0e3 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx @@ -6,11 +6,10 @@ import { useRatesStore } from "@/stores/select-rate" import RoomProvider from "@/providers/SelectRate/RoomProvider" import { trackLowestRoomPrice } from "@/utils/tracking" -import BookingCodeFilter from "./BookingCodeFilter" import MultiRoomWrapper from "./MultiRoomWrapper" import NoAvailabilityAlert from "./NoAvailabilityAlert" +import RoomsHeader from "./RoomsHeader" import RoomsList from "./RoomsList" -import RoomTypeFilter from "./RoomTypeFilter" import styles from "./rooms.module.css" @@ -77,9 +76,8 @@ export default function Rooms() { room={rooms[idx]} > 1}> + - - diff --git a/apps/scandic-web/i18n/dictionaries/da.json b/apps/scandic-web/i18n/dictionaries/da.json index 63f18347c..3f25bd9fa 100644 --- a/apps/scandic-web/i18n/dictionaries/da.json +++ b/apps/scandic-web/i18n/dictionaries/da.json @@ -2,6 +2,7 @@ "+46 8 517 517 00": "+46 8 517 517 00", "/night per adult": "/nat per voksen", "1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points", + "200 SEK/night Important information on pricing and features of pet-friendly rooms.": "200 SEK/nat Vigtig information om priser og funktioner i kæledyrsvenlige værelser.", "Included (based on availability)": "Inkluderet (baseret på tilgængelighed)", "Total price (incl VAT)": "Samlet pris (inkl. moms)", "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points": "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points", @@ -444,6 +445,7 @@ "Log out": "Log ud", "Long {long} ∙ Lat {lat}": "Long {long} ∙ Lat {lat}", "Low floor": "Lav etage", + "Lowest price (last 30 days)": "Laveste pris (sidste 30 dage)", "MY SAVED CARDS": "MINE SAVEDE KORT", "Main guest": "Main guest", "Main menu": "Hovedmenu", @@ -683,6 +685,7 @@ "Room details": "Room details", "Room facilities": "Værelsesfaciliteter", "Room is prepaid": "Værelset er forudbetalt", + "Room preferences": "Værelsespræferencer", "Room sold out": "Værelse solgt ud", "Room total": "Værelse total", "Room type": "Værelsestype", diff --git a/apps/scandic-web/i18n/dictionaries/de.json b/apps/scandic-web/i18n/dictionaries/de.json index 3e75a76e4..dd74e4abb 100644 --- a/apps/scandic-web/i18n/dictionaries/de.json +++ b/apps/scandic-web/i18n/dictionaries/de.json @@ -2,6 +2,7 @@ "+46 8 517 517 00": "+46 8 517 517 00", "/night per adult": "/Nacht pro Erwachsenem", "1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points", + "200 SEK/night Important information on pricing and features of pet-friendly rooms.": "200 SEK/Nacht Wichtige Informationen zu Preisen und Eigenschaften von haustierfreundlichen Zimmern.", "Included (based on availability)": "Inbegriffen (je nach Verfügbarkeit)", "Total price (incl VAT)": "Gesamtpreis (inkl. MwSt.)", "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points": "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points", @@ -445,6 +446,7 @@ "Log out": "Ausloggen", "Long {long} ∙ Lat {lat}": "Long {long} ∙ Lat {lat}", "Low floor": "Niedrige Etage", + "Lowest price (last 30 days)": "Niedrigster Preis (letzte 30 Tage)", "MY SAVED CARDS": "MEINE SAVEDEN KARTEN", "Main guest": "Main guest", "Main menu": "Hauptmenü", @@ -682,6 +684,7 @@ "Room details": "Room details", "Room facilities": "Zimmerausstattung", "Room is prepaid": "Zimmer ist vorausbezahlt", + "Room preferences": "Zimmerpräferenzen", "Room sold out": "Zimmer verkauft", "Room total": "Zimmer total", "Room type": "Zimmertyp", diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json index d0938d1f6..7601d9629 100644 --- a/apps/scandic-web/i18n/dictionaries/en.json +++ b/apps/scandic-web/i18n/dictionaries/en.json @@ -2,6 +2,7 @@ "+46 8 517 517 00": "+46 8 517 517 00", "/night per adult": "/night per adult", "1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points", + "200 SEK/night Important information on pricing and features of pet-friendly rooms.": "200 SEK/night Important information on pricing and features of pet-friendly rooms.", "Included (based on availability)": "Included (based on availability)", "Total price (incl VAT)": "Total price (incl VAT)", "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points": "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points", @@ -443,6 +444,7 @@ "Log out": "Log out", "Long {long} ∙ Lat {lat}": "Long {long} ∙ Lat {lat}", "Low floor": "Low floor", + "Lowest price (last 30 days)": "Lowest price (last 30 days)", "MY SAVED CARDS": "MY SAVED CARDS", "Main guest": "Main guest", "Main menu": "Main menu", @@ -681,6 +683,7 @@ "Room details": "Room details", "Room facilities": "Room facilities", "Room is prepaid": "Room is prepaid", + "Room preferences": "Room preferences", "Room sold out": "Room sold out", "Room total": "Room total", "Room type": "Room type", diff --git a/apps/scandic-web/i18n/dictionaries/fi.json b/apps/scandic-web/i18n/dictionaries/fi.json index 309dc5cb6..296b612aa 100644 --- a/apps/scandic-web/i18n/dictionaries/fi.json +++ b/apps/scandic-web/i18n/dictionaries/fi.json @@ -2,6 +2,7 @@ "+46 8 517 517 00": "+46 8 517 517 00", "/night per adult": "/yötä aikuista kohti", "1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points", + "200 SEK/night Important information on pricing and features of pet-friendly rooms.": "200 SEK/yö Tärkeitä tietoja hinnoista ja lemmikkieläinystävällisten huoneiden ominaisuuksista.", "Included (based on availability)": "Sisältyy (saatavuuden mukaan)", "Total price (incl VAT)": "Kokonaishinta (sis. ALV)", "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points": "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points", @@ -444,6 +445,7 @@ "Log out": "Kirjaudu ulos", "Long {long} ∙ Lat {lat}": "Long {long} ∙ Lat {lat}", "Low floor": "Alhainen kerros", + "Lowest price (last 30 days)": "Alin hinta (viimeiset 30 päivää)", "MY SAVED CARDS": "MINUN SAVED CARDS", "Main guest": "Main guest", "Main menu": "Päävalikko", @@ -681,6 +683,7 @@ "Room details": "Room details", "Room facilities": "Huoneen varustelu", "Room is prepaid": "Huone on maksettu etukäteen", + "Room preferences": "Huoneen mieltymykset", "Room sold out": "Huone slutsattu", "Room total": "Huoneen kokonaishinta", "Room type": "Huonetyyppi", diff --git a/apps/scandic-web/i18n/dictionaries/no.json b/apps/scandic-web/i18n/dictionaries/no.json index 7e8c1c0dd..99fb7d733 100644 --- a/apps/scandic-web/i18n/dictionaries/no.json +++ b/apps/scandic-web/i18n/dictionaries/no.json @@ -2,6 +2,7 @@ "+46 8 517 517 00": "+46 8 517 517 00", "/night per adult": "/natt per voksen", "1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points", + "200 SEK/night Important information on pricing and features of pet-friendly rooms.": "200 SEK/natt Viktig informasjon om priser og funksjoner for dyrevennlige rom.", "Included (based on availability)": "Inkludert (basert på tilgjengelighet)", "Total price (incl VAT)": "Totalpris (inkl. mva)", "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points": "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points", @@ -443,6 +444,7 @@ "Log out": "Logg ut", "Long {long} ∙ Lat {lat}": "Long {long} ∙ Lat {lat}", "Low floor": "Lav posisjon", + "Lowest price (last 30 days)": "Laveste pris (siste 30 dager)", "MY SAVED CARDS": "MINE SAVEDE KORT", "Main guest": "Main guest", "Main menu": "Hovedmeny", @@ -680,6 +682,7 @@ "Room details": "Room details", "Room facilities": "Romfasiliteter", "Room is prepaid": "Rommet er forhåndsbetalt", + "Room preferences": "Rompreferanser", "Room total": "Rom total", "Room type": "Romtype", "Room {roomIndex}": "Rom {roomIndex}", diff --git a/apps/scandic-web/i18n/dictionaries/sv.json b/apps/scandic-web/i18n/dictionaries/sv.json index bda69d630..603f4113c 100644 --- a/apps/scandic-web/i18n/dictionaries/sv.json +++ b/apps/scandic-web/i18n/dictionaries/sv.json @@ -2,6 +2,7 @@ "+46 8 517 517 00": "+46 8 517 517 00", "/night per adult": "/natt per vuxen", "1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points", + "200 SEK/night Important information on pricing and features of pet-friendly rooms.": "200 SEK/natt Viktig information om prissättning och funktioner för husdjursvänliga rum.", "Included (based on availability)": "Ingår (baserat på tillgänglighet)", "Total price (incl VAT)": "Totalpris (inkl moms)", "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points": "{sasPoints, number} EuroBonus points to {scandicPoints, number} Scandic Friends points", @@ -443,6 +444,7 @@ "Log out": "Logga ut", "Long {long} ∙ Lat {lat}": "Long {long} ∙ Lat {lat}", "Low floor": "Lågt läge", + "Lowest price (last 30 days)": "Lägsta pris (senaste 30 dagarna)", "MY SAVED CARDS": "MINE SAVEDE KORT", "Main guest": "Main guest", "Main menu": "Huvudmeny", @@ -680,6 +682,7 @@ "Room details": "Room details", "Room facilities": "Rumfaciliteter", "Room is prepaid": "Rummet är förbetalt", + "Room preferences": "Rumspreferenser", "Room sold out": "Rum slutsålt", "Room total": "Rum total", "Room type": "Rumstyp", diff --git a/apps/scandic-web/stores/select-rate/index.ts b/apps/scandic-web/stores/select-rate/index.ts index 1fdcc1a7b..17f275909 100644 --- a/apps/scandic-web/stores/select-rate/index.ts +++ b/apps/scandic-web/stores/select-rate/index.ts @@ -29,7 +29,7 @@ export function createRatesStore({ searchParams, vat, }: InitialState) { - const filterOptions = [ + const packageOptions = [ { code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM, description: labels.accessibilityRoom, @@ -84,10 +84,10 @@ export function createRatesStore({ rateSummary[idx] = { features: selectedRoom.features, product, + packages: room.packages ?? [], rate: product.rate, roomType: selectedRoom.roomType, roomTypeCode: selectedRoom.roomTypeCode, - package: room.packages?.[0], } } } @@ -106,7 +106,7 @@ export function createRatesStore({ return { activeRoom, booking, - filterOptions, + packageOptions, hotelType, isUserLoggedIn, packages, @@ -132,14 +132,16 @@ export function createRatesStore({ // Since features are fetched async based on query string, we need to read from query string to apply correct filtering const packagesParam = searchParams.get(`room[${idx}].packages`) - const selectedPackage = isRoomPackageCode(packagesParam) - ? packagesParam - : undefined + const selectedPackages = packagesParam + ? packagesParam.split(",").filter(isRoomPackageCode) + : [] let rooms: RoomConfiguration[] = roomConfiguration - if (selectedPackage) { + if (selectedPackages.length) { rooms = roomConfiguration.filter((r) => - r.features.find((f) => f.code === selectedPackage) + selectedPackages.some((pkg) => + r.features.find((f) => f.code === pkg) + ) ) } @@ -203,35 +205,48 @@ export function createRatesStore({ }) ) }, - selectPackage(code) { + togglePackage(code) { return set( produce((state: RatesState) => { - state.rooms[idx].selectedPackage = code + const isSelected = + state.rooms[idx].selectedPackages.includes(code) + const selectedPackages = isSelected + ? state.rooms[idx].selectedPackages.filter( + (pkg) => pkg !== code + ) + : [...state.rooms[idx].selectedPackages, code] + state.rooms[idx].selectedPackages = selectedPackages + const roomConfiguration = state.roomConfigurations[idx] if (roomConfiguration) { const searchParams = new URLSearchParams(state.searchParams) - if (code) { + if (selectedPackages.length) { state.rooms[idx].rooms = roomConfiguration.filter( (room) => - room.features.find((feat) => feat.code === code) + selectedPackages.every((pkg) => + room.features.find((feat) => feat.code === pkg) + ) + ) + searchParams.set( + `room[${idx}].packages`, + selectedPackages.join(",") ) - searchParams.set(`room[${idx}].packages`, code) if (state.rateSummary[idx]) { - state.rateSummary[idx].package = code + state.rateSummary[idx].packages = selectedPackages } } else { state.rooms[idx].rooms = roomConfiguration - searchParams.delete(`room[${idx}].packages`) - if (state.rateSummary[idx]) { - state.rateSummary[idx].package = undefined + state.rateSummary[idx].packages = [] } + searchParams.delete(`room[${idx}].packages`) } state.searchParams = new ReadonlyURLSearchParams( searchParams ) + window.history.pushState( {}, "", @@ -251,7 +266,7 @@ export function createRatesStore({ state.rooms[idx].selectedRate = selectedRate state.rateSummary[idx] = { features: selectedRate.features, - package: state.rooms[idx].selectedPackage, + packages: state.rooms[idx].selectedPackages, product: selectedRate.product, rate: selectedRate.product.rate, roomType: selectedRate.roomType, @@ -346,11 +361,12 @@ export function createRatesStore({ selectedFilter: booking.bookingCode ? BookingCodeFilterEnum.Discounted : BookingCodeFilterEnum.All, - selectedPackage, + selectedPackages, selectedRate: selectedRate && product ? { features: selectedRate.features, + packages: selectedPackages, product, roomType: selectedRate.roomType, roomTypeCode: selectedRate.roomTypeCode, diff --git a/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts b/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts index 125effffb..3dba3bd1f 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts @@ -31,7 +31,7 @@ export interface SelectRateSearchParams { export type Rate = { features: RoomConfiguration["features"] - package?: RoomPackageCodeEnum | undefined + packages: RoomPackageCodeEnum[] priceName?: string priceTerm?: string product: Product diff --git a/apps/scandic-web/types/stores/rates.ts b/apps/scandic-web/types/stores/rates.ts index 91d04e6e7..3379c4bd9 100644 --- a/apps/scandic-web/types/stores/rates.ts +++ b/apps/scandic-web/types/stores/rates.ts @@ -28,7 +28,7 @@ interface Actions { closeSection: () => void modifyRate: () => void selectFilter: (filter: BookingCodeFilterEnum) => void - selectPackage: (code: RoomPackageCodeEnum | undefined) => void + togglePackage: (code: RoomPackageCodeEnum) => void selectRate: (rate: SelectedRate) => void } @@ -44,14 +44,14 @@ export interface SelectedRoom { bookingRoom: RoomBooking rooms: RoomConfiguration[] selectedFilter: BookingCodeFilterEnum | undefined - selectedPackage: RoomPackageCodeEnum | undefined + selectedPackages: RoomPackageCodeEnum[] selectedRate: SelectedRate | null } export interface RatesState { activeRoom: number booking: SelectRateSearchParams - filterOptions: DefaultFilterOptions[] + packageOptions: DefaultFilterOptions[] hotelType: string | undefined isUserLoggedIn: boolean packages: NonNullable diff --git a/packages/design-system/lib/components/ChipButton/ChipButton.stories.tsx b/packages/design-system/lib/components/ChipButton/ChipButton.stories.tsx index 055ddb5ff..c83bc43cd 100644 --- a/packages/design-system/lib/components/ChipButton/ChipButton.stories.tsx +++ b/packages/design-system/lib/components/ChipButton/ChipButton.stories.tsx @@ -1,14 +1,22 @@ +import 'react-material-symbols/rounded' + import type { Meta, StoryObj } from '@storybook/react' import { fn } from '@storybook/test' -import { ChipButton } from './ChipButton.tsx' import { MaterialSymbol } from 'react-material-symbols' +import { ChipButton } from './ChipButton.tsx' +import { config as chipButtonConfig } from './variants' const meta: Meta = { - title: 'Components/Chip/ChipButton 🚧', + title: 'Components/Chip/ChipButton', component: ChipButton, argTypes: { + variant: { + control: 'select', + type: 'string', + options: Object.keys(chipButtonConfig.variants.variant), + }, onPress: { table: { disable: true, @@ -32,3 +40,16 @@ export const Default: Story = { ), }, } + +export const Outlined: Story = { + args: { + variant: 'Outlined', + onPress: fn(), + children: ( + <> + Button Chip + + + ), + }, +} diff --git a/packages/design-system/lib/components/ChipButton/ChipButton.tsx b/packages/design-system/lib/components/ChipButton/ChipButton.tsx index e7d849f79..e05a2788b 100644 --- a/packages/design-system/lib/components/ChipButton/ChipButton.tsx +++ b/packages/design-system/lib/components/ChipButton/ChipButton.tsx @@ -1,19 +1,22 @@ import { Button as ButtonRAC } from 'react-aria-components' import { Typography } from '../Typography' - -import styles from './chip-button.module.css' - -import type { ComponentPropsWithRef } from 'react' +import { ChipButtonProps } from './types' +import { variants } from './variants' export function ChipButton({ children, + variant, className, ...props -}: ComponentPropsWithRef) { +}: ChipButtonProps) { + const classNames = variants({ + variant, + }) + return ( - + {children} diff --git a/packages/design-system/lib/components/ChipButton/chip-button.module.css b/packages/design-system/lib/components/ChipButton/chip-button.module.css index fe4ab6477..56fcfbe0c 100644 --- a/packages/design-system/lib/components/ChipButton/chip-button.module.css +++ b/packages/design-system/lib/components/ChipButton/chip-button.module.css @@ -1,19 +1,31 @@ .chip { background-color: var(--Component-Button-Inverted-Fill-Default); - border-color: var(--Component-Button-Inverted-Border-Default); - border-style: solid; - border-width: 1px; border-radius: var(--Corner-radius-sm); padding: var(--Space-x1) var(--Space-x15); color: var(--Text-Interactive-Default); display: inline-flex; align-items: center; + justify-content: center; cursor: pointer; } -.chip:hover { - /* TODO: change to proper Component-variable once it is available */ - background-color: var(--Scandic-Peach-10); - /* TODO: change to proper Component-variable once it is available */ - color: var(--Scandic-Red-100); +.Default { + border: 1px solid var(--Component-Button-Inverted-Border-Default); +} + +.Default:hover { + background-color: var(--Surface-Primary-Hover-Accent); +} + +.Outlined { + border: 1px solid var(--Border-Intense); +} + +.Outlined:hover { + background-color: var(--Surface-Primary-Hover); +} + +.Outlined:focus, +.Outlined:active { + border-color: var(--Border-Interactive-Selected); } diff --git a/packages/design-system/lib/components/ChipButton/types.ts b/packages/design-system/lib/components/ChipButton/types.ts new file mode 100644 index 000000000..66b2c7bba --- /dev/null +++ b/packages/design-system/lib/components/ChipButton/types.ts @@ -0,0 +1,10 @@ +import { Button } from 'react-aria-components' + +import type { VariantProps } from 'class-variance-authority' +import type { ComponentProps } from 'react' + +import type { variants } from './variants' + +export interface ChipButtonProps + extends ComponentProps, + VariantProps {} diff --git a/packages/design-system/lib/components/ChipButton/variants.ts b/packages/design-system/lib/components/ChipButton/variants.ts new file mode 100644 index 000000000..e79d8b801 --- /dev/null +++ b/packages/design-system/lib/components/ChipButton/variants.ts @@ -0,0 +1,29 @@ +import { cva } from 'class-variance-authority' + +import { deepmerge } from 'deepmerge-ts' +import styles from './chip-button.module.css' + +export const config = { + variants: { + variant: { + Default: styles.Default, + Outlined: styles.Outlined, + }, + }, + defaultVariants: { + variant: 'Default', + }, +} as const + +export const variants = cva(styles.chip, config) + +const chipConfig = { + variants: { + typography: config.variants.variant, + }, + defaultVariants: config.defaultVariants, +} as const + +export function withChipButton(config: T) { + return deepmerge(chipConfig, config) +}