From e78847d743298480d42289313c37ead3559c9153 Mon Sep 17 00:00:00 2001 From: Michael Zetterberg Date: Wed, 16 Oct 2024 11:20:09 +0200 Subject: [PATCH 01/49] chore: update-browserslist-db to latest --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index db6a1ca42..237a64c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7801,9 +7801,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001608", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz", - "integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==", + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", "funding": [ { "type": "opencollective", From 69ed852fc4cf05fbe4e691a1b6dc6f03fd7dcc59 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Fri, 25 Oct 2024 15:44:30 +0200 Subject: [PATCH 02/49] fix: only phrasing content is allowed inside buttons meaning no div or p elements, these are now replaced by span --- components/DatePicker/Screen/Desktop.tsx | 4 +-- components/DatePicker/Screen/Mobile.tsx | 4 +-- components/DatePicker/index.tsx | 6 ++-- .../SearchList/ClearSearchButton/index.tsx | 4 +-- .../Forms/BookingWidget/FormContent/index.tsx | 13 ++++--- components/GuestsRoomsPicker/index.tsx | 36 ++++++++++--------- .../Header/MainMenu/MobileMenu/index.tsx | 2 +- .../Header/MainMenu/MyPagesMenu/index.tsx | 6 ++-- .../NavigationMenu/MegaMenu/index.tsx | 4 +-- .../LanguageSwitcherContainer/index.tsx | 12 ++++--- .../TempDesignSystem/Form/Phone/index.tsx | 4 +-- components/TempDesignSystem/Select/index.tsx | 4 +-- 12 files changed, 56 insertions(+), 43 deletions(-) diff --git a/components/DatePicker/Screen/Desktop.tsx b/components/DatePicker/Screen/Desktop.tsx index 9056f9183..d5040a4ae 100644 --- a/components/DatePicker/Screen/Desktop.tsx +++ b/components/DatePicker/Screen/Desktop.tsx @@ -82,8 +82,8 @@ export default function DatePickerDesktop({ size="small" theme="base" > - - {intl.formatMessage({ id: "Select dates" })} + + {intl.formatMessage({ id: "Select dates" })} diff --git a/components/DatePicker/Screen/Mobile.tsx b/components/DatePicker/Screen/Mobile.tsx index 0407f9b63..6e68ba1fc 100644 --- a/components/DatePicker/Screen/Mobile.tsx +++ b/components/DatePicker/Screen/Mobile.tsx @@ -94,8 +94,8 @@ export default function DatePickerMobile({ size="large" theme="base" > - - {intl.formatMessage({ id: "Select dates" })} + + {intl.formatMessage({ id: "Select dates" })}
diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx index a68f137ea..b18a4d9ca 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/index.tsx @@ -89,8 +89,10 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { return (
diff --git a/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx index 3b4391998..ae1f8f4d0 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx @@ -33,8 +33,8 @@ export default function ClearSearchButton({ type="button" > - - {intl.formatMessage({ id: "Clear searches" })} + + {intl.formatMessage({ id: "Clear searches" })} ) diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index bd7a65ba6..eca1634d8 100644 --- a/components/Forms/BookingWidget/FormContent/index.tsx +++ b/components/Forms/BookingWidget/FormContent/index.tsx @@ -65,12 +65,17 @@ export default function FormContent({ theme="base" type="submit" > - - {intl.formatMessage({ id: "Search" })} + + {intl.formatMessage({ id: "Search" })} -
+ -
+
diff --git a/components/GuestsRoomsPicker/index.tsx b/components/GuestsRoomsPicker/index.tsx index f12ecebde..8533f79bd 100644 --- a/components/GuestsRoomsPicker/index.tsx +++ b/components/GuestsRoomsPicker/index.tsx @@ -53,23 +53,25 @@ export default function GuestsRoomsPickerForm() { return (
diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index 1f2660770..ae79675dd 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -68,7 +68,7 @@ export default function MobileMenu({ })} onClick={() => toggleDropdown(DropdownTypeEnum.HamburgerMenu)} > - + toggleDropdown(DropdownTypeEnum.MyPagesMenu)} > - - {intl.formatMessage({ id: "Hi" })} {user.firstName}! + + + {intl.formatMessage({ id: "Hi" })} {user.firstName}! + toggleMegaMenu(false)} > - - {title} + + {title}
diff --git a/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx b/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx index ada415172..0eafafa29 100644 --- a/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx +++ b/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx @@ -35,10 +35,12 @@ export default function LanguageSwitcherContainer({ onClick={() => toggleDropdown(position)} > - - {intl.formatMessage({ - id: "Main menu", - })} + + + {intl.formatMessage({ + id: "Main menu", + })} +
@@ -53,7 +55,7 @@ export default function LanguageSwitcherContainer({ })} onClick={() => toggleDropdown(position)} > - + ) : null} diff --git a/components/TempDesignSystem/Form/Phone/index.tsx b/components/TempDesignSystem/Form/Phone/index.tsx index d27e31986..ffe66a67e 100644 --- a/components/TempDesignSystem/Form/Phone/index.tsx +++ b/components/TempDesignSystem/Form/Phone/index.tsx @@ -91,7 +91,7 @@ export default function Phone({ -
+ {props.children} -
+
)} /> diff --git a/components/TempDesignSystem/Select/index.tsx b/components/TempDesignSystem/Select/index.tsx index 64f3fc335..d506c146c 100644 --- a/components/TempDesignSystem/Select/index.tsx +++ b/components/TempDesignSystem/Select/index.tsx @@ -60,12 +60,12 @@ export default function Select({ > From 32c875a56c9cb61cacfe040db1b7570e53733a33 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 23 Oct 2024 14:59:31 +0200 Subject: [PATCH 03/49] feat(SW-281): create hotellisting component --- .../HotelListing/hotelListing.module.css | 49 ++++++++++++++ .../HotelListing/hotelListingTempData.json | 8 +++ components/Blocks/HotelListing/index.tsx | 64 +++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 components/Blocks/HotelListing/hotelListing.module.css create mode 100644 components/Blocks/HotelListing/hotelListingTempData.json create mode 100644 components/Blocks/HotelListing/index.tsx diff --git a/components/Blocks/HotelListing/hotelListing.module.css b/components/Blocks/HotelListing/hotelListing.module.css new file mode 100644 index 000000000..8e2ffb59e --- /dev/null +++ b/components/Blocks/HotelListing/hotelListing.module.css @@ -0,0 +1,49 @@ +.container { + background-color: var(--Base-Surface-Primary-light-Normal); + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Medium); + overflow: hidden; +} + +.image { + width: 100%; + object-fit: cover; +} + +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding: var(--Spacing-x2) var(--Spacing-x3); +} + +.intro { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); +} + +.dividerContainer { + padding: 0 var(--Spacing-x1); +} + +.captions { + display: flex; + flex-direction: row; +} + +@media screen and (min-width: 768px) { + .container { + display: grid; + grid-template-columns: 1fr 2fr; + } + + .image { + height: 100%; + } + + .button { + width: 100%; + max-width: 200px; + } +} diff --git a/components/Blocks/HotelListing/hotelListingTempData.json b/components/Blocks/HotelListing/hotelListingTempData.json new file mode 100644 index 000000000..4b1bc0039 --- /dev/null +++ b/components/Blocks/HotelListing/hotelListingTempData.json @@ -0,0 +1,8 @@ +{ + "title": "Downtown Camper", + "image": "https://test3.scandichotels.com/imagevault/publishedmedia/ehdsd3e3ceoe4ezin6cs/downtown-camper-by-scandic-lobby-reception-desk-ch.jpg", + "adress": "Brunkebergstorg 9", + "distance": "0", + "description": "There is plenty of space for meetings, conferences and parties in our large, modern event room. We offer the ideal conditions for a grand function, good transport possibilities for the event guests, and rooms with large capacity.", + "button": "Book now" +} diff --git a/components/Blocks/HotelListing/index.tsx b/components/Blocks/HotelListing/index.tsx new file mode 100644 index 000000000..d75c5d6eb --- /dev/null +++ b/components/Blocks/HotelListing/index.tsx @@ -0,0 +1,64 @@ +import { ScandicLogoIcon } from "@/components/Icons" +import Image from "@/components/Image" +import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import hotelListingTempData from "./hotelListingTempData.json" + +import styles from "./hotelListing.module.css" + +export default async function HotelListing() { + // TODO: Fetch and consume data from correct endpoint, when it is decided where the data shall come form. + const { adress, button, description, distance, title, image } = + hotelListingTempData + + const intl = await getIntl() + + return ( +
+
+ +
+
+
+ + + {title} + +
+ {adress} +
+ +
+ + {`${distance} ${intl.formatMessage({ id: "km to city center" })}`} + +
+
+ {description} + +
+
+ ) +} From 2428e09fe54bf9ee571d119dd3058ecfc10f046d Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 24 Oct 2024 13:41:39 +0200 Subject: [PATCH 04/49] feat(SW-281): use subtitle instead of title --- components/Blocks/HotelListing/index.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/components/Blocks/HotelListing/index.tsx b/components/Blocks/HotelListing/index.tsx index d75c5d6eb..9149b817b 100644 --- a/components/Blocks/HotelListing/index.tsx +++ b/components/Blocks/HotelListing/index.tsx @@ -5,7 +5,7 @@ import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" -import Title from "@/components/TempDesignSystem/Text/Title" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIntl } from "@/i18n" import hotelListingTempData from "./hotelListingTempData.json" @@ -33,9 +33,7 @@ export default async function HotelListing() {
- - {title} - + {title}
{adress}
From b841604e5eedae2a763af9e14fba3290357d5197 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 24 Oct 2024 15:08:23 +0200 Subject: [PATCH 05/49] feat(SW-281): refactor --- .../HotelListing/hotelListingTempData.json | 8 --- components/Blocks/HotelListing/index.tsx | 62 ----------------- .../HotelListing/hotelListing.module.css | 0 .../ContentPage/HotelListing/index.tsx | 66 +++++++++++++++++++ types/components/contentPage/hotelListing.ts | 9 +++ 5 files changed, 75 insertions(+), 70 deletions(-) delete mode 100644 components/Blocks/HotelListing/hotelListingTempData.json delete mode 100644 components/Blocks/HotelListing/index.tsx rename components/{Blocks => ContentType/ContentPage}/HotelListing/hotelListing.module.css (100%) create mode 100644 components/ContentType/ContentPage/HotelListing/index.tsx create mode 100644 types/components/contentPage/hotelListing.ts diff --git a/components/Blocks/HotelListing/hotelListingTempData.json b/components/Blocks/HotelListing/hotelListingTempData.json deleted file mode 100644 index 4b1bc0039..000000000 --- a/components/Blocks/HotelListing/hotelListingTempData.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "title": "Downtown Camper", - "image": "https://test3.scandichotels.com/imagevault/publishedmedia/ehdsd3e3ceoe4ezin6cs/downtown-camper-by-scandic-lobby-reception-desk-ch.jpg", - "adress": "Brunkebergstorg 9", - "distance": "0", - "description": "There is plenty of space for meetings, conferences and parties in our large, modern event room. We offer the ideal conditions for a grand function, good transport possibilities for the event guests, and rooms with large capacity.", - "button": "Book now" -} diff --git a/components/Blocks/HotelListing/index.tsx b/components/Blocks/HotelListing/index.tsx deleted file mode 100644 index 9149b817b..000000000 --- a/components/Blocks/HotelListing/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { ScandicLogoIcon } from "@/components/Icons" -import Image from "@/components/Image" -import Button from "@/components/TempDesignSystem/Button" -import Divider from "@/components/TempDesignSystem/Divider" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { getIntl } from "@/i18n" - -import hotelListingTempData from "./hotelListingTempData.json" - -import styles from "./hotelListing.module.css" - -export default async function HotelListing() { - // TODO: Fetch and consume data from correct endpoint, when it is decided where the data shall come form. - const { adress, button, description, distance, title, image } = - hotelListingTempData - - const intl = await getIntl() - - return ( -
-
- -
-
-
- - {title} -
- {adress} -
- -
- - {`${distance} ${intl.formatMessage({ id: "km to city center" })}`} - -
-
- {description} - -
-
- ) -} diff --git a/components/Blocks/HotelListing/hotelListing.module.css b/components/ContentType/ContentPage/HotelListing/hotelListing.module.css similarity index 100% rename from components/Blocks/HotelListing/hotelListing.module.css rename to components/ContentType/ContentPage/HotelListing/hotelListing.module.css diff --git a/components/ContentType/ContentPage/HotelListing/index.tsx b/components/ContentType/ContentPage/HotelListing/index.tsx new file mode 100644 index 000000000..f206ba242 --- /dev/null +++ b/components/ContentType/ContentPage/HotelListing/index.tsx @@ -0,0 +1,66 @@ +import { ScandicLogoIcon } from "@/components/Icons" +import Image from "@/components/Image" +import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getIntl } from "@/i18n" + +import styles from "./hotelListing.module.css" + +import { HotelListingProps } from "@/types/components/contentPage/hotelListing" + +export default async function HotelListing({ + image, + name, + address, + distance, + description, +}: HotelListingProps) { + const intl = await getIntl() + + return ( +
  • +
    +
    + +
    +
    +
    + + {name} +
    + {address} +
    + +
    + + {`${distance} ${intl.formatMessage({ id: "km to city center" })}`} + +
    +
    + {description} + +
    +
    +
  • + ) +} diff --git a/types/components/contentPage/hotelListing.ts b/types/components/contentPage/hotelListing.ts new file mode 100644 index 000000000..2559137a4 --- /dev/null +++ b/types/components/contentPage/hotelListing.ts @@ -0,0 +1,9 @@ +import { Hotel } from "@/types/hotel" + +export type HotelListingProps = { + image: Hotel["hotelContent"]["images"]["imageSizes"]["large"] + name: Hotel["name"] + address: Hotel["address"]["streetAddress"] + distance: Hotel["location"]["distanceToCentre"] + description: Hotel["hotelContent"]["texts"]["descriptions"]["medium"] +} From a4e597dc14a83f1ae5f8204eabbf00f9fff748ca Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 24 Oct 2024 15:57:27 +0200 Subject: [PATCH 06/49] feat(SW-281): remove li --- .../ContentPage/HotelListing/index.tsx | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/components/ContentType/ContentPage/HotelListing/index.tsx b/components/ContentType/ContentPage/HotelListing/index.tsx index f206ba242..4c33e2568 100644 --- a/components/ContentType/ContentPage/HotelListing/index.tsx +++ b/components/ContentType/ContentPage/HotelListing/index.tsx @@ -10,7 +10,7 @@ import { getIntl } from "@/i18n" import styles from "./hotelListing.module.css" -import { HotelListingProps } from "@/types/components/contentPage/hotelListing" +import type { HotelListingProps } from "@/types/components/contentPage/hotelListing" export default async function HotelListing({ image, @@ -22,45 +22,43 @@ export default async function HotelListing({ const intl = await getIntl() return ( -
  • -
    -
    - -
    -
    -
    - - {name} -
    - {address} -
    - -
    - - {`${distance} ${intl.formatMessage({ id: "km to city center" })}`} - +
    +
    + +
    +
    +
    + + {name} +
    + {address} +
    +
    + + {`${distance} ${intl.formatMessage({ id: "km to city center" })}`} +
    - {description} - -
    -
    -
  • +
    + {description} + +
    + ) } From 97b4cad44384a2936d46f3baef2d93e459c828d6 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 24 Oct 2024 22:23:07 +0200 Subject: [PATCH 07/49] feat(SW-281): add alt text --- components/ContentType/ContentPage/HotelListing/index.tsx | 3 ++- types/components/contentPage/hotelListing.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/components/ContentType/ContentPage/HotelListing/index.tsx b/components/ContentType/ContentPage/HotelListing/index.tsx index 4c33e2568..2be79d45b 100644 --- a/components/ContentType/ContentPage/HotelListing/index.tsx +++ b/components/ContentType/ContentPage/HotelListing/index.tsx @@ -14,6 +14,7 @@ import type { HotelListingProps } from "@/types/components/contentPage/hotelList export default async function HotelListing({ image, + altText, name, address, distance, @@ -26,7 +27,7 @@ export default async function HotelListing({
    Date: Fri, 25 Oct 2024 09:58:14 +0200 Subject: [PATCH 08/49] feat(SW-281): use distanceToCenter translation --- components/ContentType/ContentPage/HotelListing/index.tsx | 7 +++++-- types/components/contentPage/hotelListing.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/components/ContentType/ContentPage/HotelListing/index.tsx b/components/ContentType/ContentPage/HotelListing/index.tsx index 2be79d45b..0d18e731e 100644 --- a/components/ContentType/ContentPage/HotelListing/index.tsx +++ b/components/ContentType/ContentPage/HotelListing/index.tsx @@ -17,7 +17,7 @@ export default async function HotelListing({ altText, name, address, - distance, + distanceToCentre, description, }: HotelListingProps) { const intl = await getIntl() @@ -43,7 +43,10 @@ export default async function HotelListing({ - {`${distance} ${intl.formatMessage({ id: "km to city center" })}`} + {intl.formatMessage( + { id: "Distance to city centre" }, + { number: distanceToCentre } + )} diff --git a/types/components/contentPage/hotelListing.ts b/types/components/contentPage/hotelListing.ts index bb7a59fdb..9bc4d34f0 100644 --- a/types/components/contentPage/hotelListing.ts +++ b/types/components/contentPage/hotelListing.ts @@ -5,6 +5,6 @@ export type HotelListingProps = { altText: Hotel["hotelContent"]["images"]["metaData"]["altText"] name: Hotel["name"] address: Hotel["address"]["streetAddress"] - distance: Hotel["location"]["distanceToCentre"] + distanceToCentre: Hotel["location"]["distanceToCentre"] description: Hotel["hotelContent"]["texts"]["descriptions"]["medium"] } From 720dc4c26b77e430c4431b79cf85f3cc9cb0ad91 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 25 Oct 2024 13:28:46 +0200 Subject: [PATCH 09/49] feat(SW-281): change requests --- .../HotelListing/hotelListing.module.css | 8 +++--- .../ContentPage/HotelListing/index.tsx | 26 ++++++++++--------- types/components/contentPage/hotelListing.ts | 3 ++- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/components/ContentType/ContentPage/HotelListing/hotelListing.module.css b/components/ContentType/ContentPage/HotelListing/hotelListing.module.css index 8e2ffb59e..5a7bf6f7d 100644 --- a/components/ContentType/ContentPage/HotelListing/hotelListing.module.css +++ b/components/ContentType/ContentPage/HotelListing/hotelListing.module.css @@ -7,6 +7,7 @@ .image { width: 100%; + max-height: 200px; object-fit: cover; } @@ -29,21 +30,20 @@ .captions { display: flex; - flex-direction: row; } @media screen and (min-width: 768px) { .container { display: grid; - grid-template-columns: 1fr 2fr; + grid-template-columns: minmax(250px, 350px) auto; } .image { + max-height: none; height: 100%; } .button { - width: 100%; - max-width: 200px; + width: min(100%, 200px); } } diff --git a/components/ContentType/ContentPage/HotelListing/index.tsx b/components/ContentType/ContentPage/HotelListing/index.tsx index 0d18e731e..138e77d42 100644 --- a/components/ContentType/ContentPage/HotelListing/index.tsx +++ b/components/ContentType/ContentPage/HotelListing/index.tsx @@ -6,6 +6,7 @@ import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import styles from "./hotelListing.module.css" @@ -13,30 +14,31 @@ import styles from "./hotelListing.module.css" import type { HotelListingProps } from "@/types/components/contentPage/hotelListing" export default async function HotelListing({ - image, + imageUrl, altText, name, address, distanceToCentre, description, + link, }: HotelListingProps) { const intl = await getIntl() return (
    -
    - {altText} -
    + {altText}
    - {name} + + {name} +
    {address}
    @@ -58,7 +60,7 @@ export default async function HotelListing({ className={styles.button} asChild > - + {intl.formatMessage({ id: "See hotel details" })} diff --git a/types/components/contentPage/hotelListing.ts b/types/components/contentPage/hotelListing.ts index 9bc4d34f0..5463a0002 100644 --- a/types/components/contentPage/hotelListing.ts +++ b/types/components/contentPage/hotelListing.ts @@ -1,10 +1,11 @@ import type { Hotel } from "@/types/hotel" export type HotelListingProps = { - image: Hotel["hotelContent"]["images"]["imageSizes"]["large"] + imageUrl: Hotel["hotelContent"]["images"]["imageSizes"]["large"] altText: Hotel["hotelContent"]["images"]["metaData"]["altText"] name: Hotel["name"] address: Hotel["address"]["streetAddress"] distanceToCentre: Hotel["location"]["distanceToCentre"] description: Hotel["hotelContent"]["texts"]["descriptions"]["medium"] + link: string } From d8dacf022d8688045a7b73f932529a912fd4a452 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 28 Oct 2024 14:23:57 +0100 Subject: [PATCH 10/49] feat(SW-281): refactor type --- .../ContentType/ContentPage/HotelListing/index.tsx | 4 ++-- types/components/contentPage/hotelListing.ts | 11 ----------- types/components/contentPage/hotelListingItem.ts | 9 +++++++++ 3 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 types/components/contentPage/hotelListing.ts create mode 100644 types/components/contentPage/hotelListingItem.ts diff --git a/components/ContentType/ContentPage/HotelListing/index.tsx b/components/ContentType/ContentPage/HotelListing/index.tsx index 138e77d42..9ef600c7d 100644 --- a/components/ContentType/ContentPage/HotelListing/index.tsx +++ b/components/ContentType/ContentPage/HotelListing/index.tsx @@ -11,7 +11,7 @@ import { getIntl } from "@/i18n" import styles from "./hotelListing.module.css" -import type { HotelListingProps } from "@/types/components/contentPage/hotelListing" +import type { HotelListingItemProps } from "@/types/components/contentPage/hotelListingItem" export default async function HotelListing({ imageUrl, @@ -21,7 +21,7 @@ export default async function HotelListing({ distanceToCentre, description, link, -}: HotelListingProps) { +}: HotelListingItemProps) { const intl = await getIntl() return ( diff --git a/types/components/contentPage/hotelListing.ts b/types/components/contentPage/hotelListing.ts deleted file mode 100644 index 5463a0002..000000000 --- a/types/components/contentPage/hotelListing.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Hotel } from "@/types/hotel" - -export type HotelListingProps = { - imageUrl: Hotel["hotelContent"]["images"]["imageSizes"]["large"] - altText: Hotel["hotelContent"]["images"]["metaData"]["altText"] - name: Hotel["name"] - address: Hotel["address"]["streetAddress"] - distanceToCentre: Hotel["location"]["distanceToCentre"] - description: Hotel["hotelContent"]["texts"]["descriptions"]["medium"] - link: string -} diff --git a/types/components/contentPage/hotelListingItem.ts b/types/components/contentPage/hotelListingItem.ts new file mode 100644 index 000000000..1065d1c5f --- /dev/null +++ b/types/components/contentPage/hotelListingItem.ts @@ -0,0 +1,9 @@ +export type HotelListingItemProps = { + imageUrl: string + altText: string + name: string + address: string + distanceToCentre: number + description: string + link: string +} From 4e66a0297c814d09f93375ba0101228e85b0c684 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 28 Oct 2024 15:30:23 +0100 Subject: [PATCH 11/49] feat(SW-281): refactor file names --- .../hotelListingItem.module.css} | 0 .../ContentPage/{HotelListing => HotelListingItem}/index.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename components/ContentType/ContentPage/{HotelListing/hotelListing.module.css => HotelListingItem/hotelListingItem.module.css} (100%) rename components/ContentType/ContentPage/{HotelListing => HotelListingItem}/index.tsx (97%) diff --git a/components/ContentType/ContentPage/HotelListing/hotelListing.module.css b/components/ContentType/ContentPage/HotelListingItem/hotelListingItem.module.css similarity index 100% rename from components/ContentType/ContentPage/HotelListing/hotelListing.module.css rename to components/ContentType/ContentPage/HotelListingItem/hotelListingItem.module.css diff --git a/components/ContentType/ContentPage/HotelListing/index.tsx b/components/ContentType/ContentPage/HotelListingItem/index.tsx similarity index 97% rename from components/ContentType/ContentPage/HotelListing/index.tsx rename to components/ContentType/ContentPage/HotelListingItem/index.tsx index 9ef600c7d..d3f179d2c 100644 --- a/components/ContentType/ContentPage/HotelListing/index.tsx +++ b/components/ContentType/ContentPage/HotelListingItem/index.tsx @@ -9,7 +9,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" -import styles from "./hotelListing.module.css" +import styles from "./hotelListingItem.module.css" import type { HotelListingItemProps } from "@/types/components/contentPage/hotelListingItem" From 31da31b72d0f0bd5397dc2c6959e252be227452c Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 28 Oct 2024 15:49:34 +0100 Subject: [PATCH 12/49] feat(SW-281): change function name --- components/ContentType/ContentPage/HotelListingItem/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ContentType/ContentPage/HotelListingItem/index.tsx b/components/ContentType/ContentPage/HotelListingItem/index.tsx index d3f179d2c..2f2d0dd81 100644 --- a/components/ContentType/ContentPage/HotelListingItem/index.tsx +++ b/components/ContentType/ContentPage/HotelListingItem/index.tsx @@ -13,7 +13,7 @@ import styles from "./hotelListingItem.module.css" import type { HotelListingItemProps } from "@/types/components/contentPage/hotelListingItem" -export default async function HotelListing({ +export default async function HotelListingItem({ imageUrl, altText, name, From 05d353e22422819fb3df1e7d3c5eee048fd05c80 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Fri, 25 Oct 2024 08:50:23 +0200 Subject: [PATCH 13/49] sub-task/ SW-695 Prefill Guests data in booking widget --- app/[lang]/(live)/@bookingwidget/page.tsx | 6 +- components/BookingWidget/Client.tsx | 22 +- .../MobileToggleButton/index.tsx | 8 +- .../Forms/BookingWidget/FormContent/index.tsx | 7 +- components/Forms/BookingWidget/index.tsx | 2 +- components/Forms/BookingWidget/schema.ts | 2 +- .../GuestsRoomsPicker/AdultSelector/index.tsx | 6 +- .../ChildSelector/ChildInfoSelector.tsx | 14 +- .../GuestsRoomsPicker/ChildSelector/index.tsx | 22 +- .../Provider/GuestsRoomsProvider.tsx | 26 ++ components/GuestsRoomsPicker/index.tsx | 11 +- .../SelectRate/RoomSelection/utils.ts | 28 +- components/TempDesignSystem/Select/index.tsx | 2 + components/TempDesignSystem/Select/select.ts | 1 + stores/guests-rooms.ts | 319 +++++++++++------- .../bookingWidget/guestsRoomsPicker.ts | 2 +- types/components/bookingWidget/index.ts | 13 +- utils/url.ts | 33 ++ 18 files changed, 342 insertions(+), 182 deletions(-) create mode 100644 components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 16bc85731..2d43c7773 100644 --- a/app/[lang]/(live)/@bookingwidget/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -3,13 +3,11 @@ import { serverClient } from "@/lib/trpc/server" import BookingWidget, { preload } from "@/components/BookingWidget" -import { BookingWidgetSearchParams } from "@/types/components/bookingWidget" -import { LangParams, PageArgs } from "@/types/params" +import { BookingWidgetPageProps } from "@/types/components/bookingWidget" export default async function BookingWidgetPage({ - params, searchParams, -}: PageArgs) { +}: BookingWidgetPageProps) { if (env.HIDE_FOR_NEXT_RELEASE) { return null } diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index fda8683c9..6a594199e 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -9,6 +9,7 @@ import Form from "@/components/Forms/BookingWidget" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { CloseLargeIcon } from "@/components/Icons" import { debounce } from "@/utils/debounce" +import { getFormattedUrlQueryParams } from "@/utils/url" import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils" import MobileToggleButton from "./MobileToggleButton" @@ -18,6 +19,7 @@ import styles from "./bookingWidget.module.css" import type { BookingWidgetClientProps, BookingWidgetSchema, + BookingWidgetSearchParams, } from "@/types/components/bookingWidget" import type { Location } from "@/types/trpc/routers/hotel/locations" @@ -36,12 +38,14 @@ export default function BookingWidgetClient({ ? JSON.parse(sessionStorageSearchData) : undefined - const bookingWidgetSearchParams = searchParams - ? new URLSearchParams(searchParams) - : undefined - const bookingWidgetSearchData = bookingWidgetSearchParams - ? getHotelReservationQueryParams(bookingWidgetSearchParams) - : undefined + const bookingWidgetSearchData: BookingWidgetSearchParams | undefined = + searchParams + ? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), { + adults: "number", + age: "number", + bed: "number", + }) as BookingWidgetSearchParams) + : undefined const getLocationObj = (destination: string): Location | undefined => { if (destination) { @@ -83,7 +87,7 @@ export default function BookingWidgetClient({ // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 // This is specifically to handle timezones falling in different dates. fromDate: isDateParamValid - ? bookingWidgetSearchData?.fromDate.toString() + ? bookingWidgetSearchData?.fromDate?.toString() : dt().utc().format("YYYY-MM-DD"), toDate: isDateParamValid ? bookingWidgetSearchData?.toDate?.toString() @@ -92,10 +96,10 @@ export default function BookingWidgetClient({ bookingCode: "", redemption: false, voucher: false, - rooms: [ + rooms: bookingWidgetSearchData?.room ?? [ { adults: 1, - children: [], + child: [], }, ], }, diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index df84cd65d..3bc438c41 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -54,6 +54,12 @@ export default function MobileToggleButton({ } return acc }, 0) + const totalChildren = rooms.reduce((acc, room) => { + if (room.child) { + acc = acc + room.child.length + } + return acc + }, 0) return (
    @@ -62,7 +68,7 @@ export default function MobileToggleButton({ {`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage( { id: "booking.nights" }, { totalNights: nights } - )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} + )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.children" }, { totalChildren })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
    diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index eca1634d8..002edb44a 100644 --- a/components/Forms/BookingWidget/FormContent/index.tsx +++ b/components/Forms/BookingWidget/FormContent/index.tsx @@ -7,6 +7,7 @@ import { dt } from "@/lib/dt" import DatePicker from "@/components/DatePicker" import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker" +import GuestsRoomsProvider from "@/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider" import { SearchIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -29,6 +30,8 @@ export default function FormContent({ const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days") + const selectedGuests = useWatch({ name: "rooms" }) + return ( <>
    @@ -51,7 +54,9 @@ export default function FormContent({ {rooms} - + + +
    diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index 270a38b04..8c9ccb48e 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -42,7 +42,7 @@ export default function Form({ locations, type }: BookingWidgetFormProps) { data.rooms.forEach((room, index) => { bookingWidgetParams.set(`room[${index}].adults`, room.adults.toString()) - room.children.forEach((child, childIndex) => { + room.child.forEach((child, childIndex) => { bookingWidgetParams.set( `room[${index}].child[${childIndex}].age`, child.age.toString() diff --git a/components/Forms/BookingWidget/schema.ts b/components/Forms/BookingWidget/schema.ts index aa42b542d..973ab6ad6 100644 --- a/components/Forms/BookingWidget/schema.ts +++ b/components/Forms/BookingWidget/schema.ts @@ -4,7 +4,7 @@ import type { Location } from "@/types/trpc/routers/hotel/locations" export const guestRoomSchema = z.object({ adults: z.number().default(1), - children: z.array( + child: z.array( z.object({ age: z.number().nonnegative(), bed: z.number(), diff --git a/components/GuestsRoomsPicker/AdultSelector/index.tsx b/components/GuestsRoomsPicker/AdultSelector/index.tsx index 72d60ebaf..06dbd56c3 100644 --- a/components/GuestsRoomsPicker/AdultSelector/index.tsx +++ b/components/GuestsRoomsPicker/AdultSelector/index.tsx @@ -21,7 +21,7 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) { const intl = useIntl() const adultsLabel = intl.formatMessage({ id: "Adults" }) const { setValue } = useFormContext() - const { adults, children, childrenInAdultsBed } = useGuestsRoomsStore( + const { adults, child, childrenInAdultsBed } = useGuestsRoomsStore( (state) => state.rooms[roomIndex] ) const increaseAdults = useGuestsRoomsStore((state) => state.increaseAdults) @@ -39,13 +39,13 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) { decreaseAdults(roomIndex) setValue(`rooms.${roomIndex}.adults`, adults - 1) if (childrenInAdultsBed > adults) { - const toUpdateIndex = children.findIndex( + const toUpdateIndex = child.findIndex( (child: Child) => child.bed == BedTypeEnum.IN_ADULTS_BED ) if (toUpdateIndex != -1) { setValue( `rooms.${roomIndex}.children.${toUpdateIndex}.bed`, - children[toUpdateIndex].age < 3 + child[toUpdateIndex].age < 3 ? BedTypeEnum.IN_CRIB : BedTypeEnum.IN_EXTRA_BED ) diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index 107bfd8b6..f219293ab 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -26,7 +26,7 @@ export default function ChildInfoSelector({ const ageLabel = intl.formatMessage({ id: "Age" }) const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" }) const bedLabel = intl.formatMessage({ id: "Bed" }) - const { setValue, trigger } = useFormContext() + const { setValue } = useFormContext() const { adults, childrenInAdultsBed } = useGuestsRoomsStore( (state) => state.rooms[roomIndex] ) @@ -51,10 +51,11 @@ export default function ChildInfoSelector({ function updateSelectedAge(age: number) { updateChildAge(age, roomIndex, index) - setValue(`rooms.${roomIndex}.children.${index}.age`, age) + setValue(`rooms.${roomIndex}.child.${index}.age`, age, { + shouldValidate: true, + }) const availableBedTypes = getAvailableBeds(age) updateSelectedBed(availableBedTypes[0].value) - trigger("rooms") } function updateSelectedBed(bed: number) { @@ -64,7 +65,7 @@ export default function ChildInfoSelector({ decreaseChildInAdultsBed(roomIndex) } updateChildBed(bed, roomIndex, index) - setValue(`rooms.${roomIndex}.children.${index}.bed`, bed) + setValue(`rooms.${roomIndex}.child.${index}.bed`, bed) } const allBedTypes: ChildBed[] = [ @@ -109,8 +110,9 @@ export default function ChildInfoSelector({ onSelect={(key) => { updateSelectedAge(key as number) }} - name={`rooms.${roomIndex}.children.${index}.age`} + name={`rooms.${roomIndex}.child.${index}.age`} placeholder={ageLabel} + maxHeight={150} />
    @@ -123,7 +125,7 @@ export default function ChildInfoSelector({ onSelect={(key) => { updateSelectedBed(key as number) }} - name={`rooms.${roomIndex}.children.${index}.age`} + name={`rooms.${roomIndex}.child.${index}.age`} placeholder={bedLabel} /> ) : null} diff --git a/components/GuestsRoomsPicker/ChildSelector/index.tsx b/components/GuestsRoomsPicker/ChildSelector/index.tsx index 827bcd2e6..22d594397 100644 --- a/components/GuestsRoomsPicker/ChildSelector/index.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/index.tsx @@ -19,9 +19,7 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) { const intl = useIntl() const childrenLabel = intl.formatMessage({ id: "Children" }) const { setValue, trigger } = useFormContext() - const children = useGuestsRoomsStore( - (state) => state.rooms[roomIndex].children - ) + const children = useGuestsRoomsStore((state) => state.rooms[roomIndex].child) const increaseChildren = useGuestsRoomsStore( (state) => state.increaseChildren ) @@ -32,18 +30,22 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) { function increaseChildrenCount(roomIndex: number) { if (children.length < 5) { increaseChildren(roomIndex) - setValue(`rooms.${roomIndex}.children.${children.length}`, { - age: -1, - bed: -1, - }) - trigger("rooms") + setValue( + `rooms.${roomIndex}.child.${children.length}`, + { + age: -1, + bed: -1, + }, + { shouldValidate: true } + ) } } function decreaseChildrenCount(roomIndex: number) { if (children.length > 0) { const newChildrenList = decreaseChildren(roomIndex) - setValue(`rooms.${roomIndex}.children`, newChildrenList) - trigger("rooms") + setValue(`rooms.${roomIndex}.child`, newChildrenList, { + shouldValidate: true, + }) } } diff --git a/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx b/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx new file mode 100644 index 000000000..5a85102e7 --- /dev/null +++ b/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx @@ -0,0 +1,26 @@ +"use client" +import { PropsWithChildren, useRef } from "react" + +import { + GuestsRoomsContext, + type GuestsRoomsStore, + initGuestsRoomsState, +} from "@/stores/guests-rooms" + +import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" + +export default function GuestsRoomsProvider({ + selectedGuests, + children, +}: PropsWithChildren<{ selectedGuests?: GuestsRoom[] }>) { + const initialStore = useRef() + if (!initialStore.current) { + initialStore.current = initGuestsRoomsState(selectedGuests) + } + + return ( + + {children} + + ) +} diff --git a/components/GuestsRoomsPicker/index.tsx b/components/GuestsRoomsPicker/index.tsx index 8533f79bd..090fc3803 100644 --- a/components/GuestsRoomsPicker/index.tsx +++ b/components/GuestsRoomsPicker/index.tsx @@ -1,6 +1,7 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" +import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" import { useGuestsRoomsStore } from "@/stores/guests-rooms" @@ -12,9 +13,14 @@ import GuestsRoomsPicker from "./GuestsRoomsPicker" import styles from "./guests-rooms-picker.module.css" -export default function GuestsRoomsPickerForm() { +export default function GuestsRoomsPickerForm({ + name = "rooms", +}: { + name: string +}) { const intl = useIntl() const [isOpen, setIsOpen] = useState(false) + const { setValue } = useFormContext() const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore( (state) => ({ rooms: state.rooms, @@ -32,10 +38,11 @@ export default function GuestsRoomsPickerForm() { if (guestRoomsValidData.success) { setIsOpen(false) setIsValidated(false) + setValue(name, guestRoomsValidData.data, { shouldValidate: true }) } else { setIsValidated(true) } - }, [rooms, setIsValidated, setIsOpen]) + }, [rooms, name, setValue, setIsValidated, setIsOpen]) useEffect(() => { function handleClickOutside(evt: Event) { diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index e47a0da70..0b1ab884a 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -1,28 +1,12 @@ +import { getFormattedUrlQueryParams } from "@/utils/url" + import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" function getHotelReservationQueryParams(searchParams: URLSearchParams) { - const searchParamsObject: Record = Array.from( - searchParams.entries() - ).reduce>( - (acc, [key, value]) => { - const keys = key.replace(/\]/g, "").split(/\[|\./) // Split keys by '[' or '.' - keys.reduce((nestedAcc, k, i) => { - if (i === keys.length - 1) { - // Convert value to number if the key is 'adults' or 'age' - ;(nestedAcc as Record)[k] = - k === "adults" || k === "age" ? Number(value) : value - } else { - if (!nestedAcc[k]) { - nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as object or array - } - } - return nestedAcc[k] as Record - }, acc) - return acc - }, - {} as Record - ) - return searchParamsObject as SelectRateSearchParams + return getFormattedUrlQueryParams(searchParams, { + adults: "number", + age: "number", + }) as SelectRateSearchParams } export default getHotelReservationQueryParams diff --git a/components/TempDesignSystem/Select/index.tsx b/components/TempDesignSystem/Select/index.tsx index d506c146c..5575dbd33 100644 --- a/components/TempDesignSystem/Select/index.tsx +++ b/components/TempDesignSystem/Select/index.tsx @@ -34,6 +34,7 @@ export default function Select({ required = false, tabIndex, value, + maxHeight, }: SelectProps) { const [rootDiv, setRootDiv] = useState(undefined) @@ -81,6 +82,7 @@ export default function Select({ * on the container as well as to not overflow it at any time. */ UNSTABLE_portalContainer={rootDiv} + maxHeight={maxHeight} > {items.map((item) => ( diff --git a/components/TempDesignSystem/Select/select.ts b/components/TempDesignSystem/Select/select.ts index cac1e69cc..706ed71fd 100644 --- a/components/TempDesignSystem/Select/select.ts +++ b/components/TempDesignSystem/Select/select.ts @@ -9,6 +9,7 @@ export interface SelectProps onSelect: (key: Key) => void placeholder?: string value?: string | number + maxHeight?: number } export type SelectPortalContainer = HTMLDivElement | undefined diff --git a/stores/guests-rooms.ts b/stores/guests-rooms.ts index 2866184b0..bc305db61 100644 --- a/stores/guests-rooms.ts +++ b/stores/guests-rooms.ts @@ -1,22 +1,28 @@ "use client" import { produce } from "immer" -import { create } from "zustand" +import { createContext, useContext } from "react" +import { create, useStore } from "zustand" import { BedTypeEnum } from "@/types/components/bookingWidget/enums" -import { Child } from "@/types/components/bookingWidget/guestsRoomsPicker" +import { + Child, + GuestsRoom, +} from "@/types/components/bookingWidget/guestsRoomsPicker" -interface GuestsRooms { - rooms: [ - { - adults: number - children: Child[] - childrenInAdultsBed: number - }, - ] +const SESSION_STORAGE_KEY = "guests_rooms" + +interface extendedGuestsRoom extends GuestsRoom { + childrenInAdultsBed: number +} +interface GuestsRoomsState { + rooms: extendedGuestsRoom[] adultCount: number childCount: number isValidated: boolean +} + +interface GuestsRoomsStoreState extends GuestsRoomsState { increaseAdults: (roomIndex: number) => void decreaseAdults: (roomIndex: number) => void increaseChildren: (roomIndex: number) => void @@ -30,115 +36,192 @@ interface GuestsRooms { setIsValidated: (isValidated: boolean) => void } -export const useGuestsRoomsStore = create((set, get) => ({ - rooms: [ - { - adults: 1, - children: [], - childrenInAdultsBed: 0, - }, - ], - adultCount: 1, - childCount: 0, - isValidated: false, - increaseAdults: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1 - state.adultCount = state.adultCount + 1 - }) - ), - decreaseAdults: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1 - state.adultCount = state.adultCount - 1 - if ( - state.rooms[roomIndex].childrenInAdultsBed > - state.rooms[roomIndex].adults - ) { - const toUpdateIndex = state.rooms[roomIndex].children.findIndex( - (child) => child.bed == BedTypeEnum.IN_ADULTS_BED - ) - if (toUpdateIndex != -1) { - state.rooms[roomIndex].children[toUpdateIndex].bed = - state.rooms[roomIndex].children[toUpdateIndex].age < 3 - ? BedTypeEnum.IN_CRIB - : BedTypeEnum.IN_EXTRA_BED - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].adults - } - } - }) - ), - increaseChildren: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].children.push({ - age: -1, - bed: -1, +export function validateBedTypes(data: extendedGuestsRoom[]) { + data.forEach((room) => { + room.child.forEach((child) => { + const allowedBedTypes: number[] = [] + if (child.age <= 5 && room.adults >= room.childrenInAdultsBed) { + allowedBedTypes.push(BedTypeEnum.IN_ADULTS_BED) + } else if (child.age <= 5) { + room.childrenInAdultsBed = room.childrenInAdultsBed - 1 + } + if (child.age < 3) { + allowedBedTypes.push(BedTypeEnum.IN_CRIB) + } + if (child.age > 2) { + allowedBedTypes.push(BedTypeEnum.IN_EXTRA_BED) + } + if (!allowedBedTypes.includes(child.bed)) { + child.bed = allowedBedTypes[0] + } + }) + }) +} + +export function initGuestsRoomsState(initData?: GuestsRoom[]) { + const isBrowser = typeof window !== "undefined" + const sessionData = isBrowser + ? sessionStorage.getItem(SESSION_STORAGE_KEY) + : null + + const defaultGuestsData: extendedGuestsRoom = { + adults: 1, + child: [], + childrenInAdultsBed: 0, + } + const defaultData: GuestsRoomsState = { + rooms: [defaultGuestsData], + adultCount: 1, + childCount: 0, + isValidated: false, + } + + let inputData: GuestsRoomsState = defaultData + if (sessionData) { + inputData = JSON.parse(sessionData) + } + if (initData) { + inputData.rooms = initData.map((room) => { + const childrenInAdultsBed = room.child + ? room.child.reduce((acc, child) => { + acc = acc + (child.bed == BedTypeEnum.IN_ADULTS_BED ? 1 : 0) + return acc + }, 0) + : 0 + return { ...defaultGuestsData, ...room, childrenInAdultsBed } + }) as extendedGuestsRoom[] + + inputData.adultCount = initData.reduce((acc, room) => { + acc = acc + room.adults + return acc + }, 0) + inputData.childCount = initData.reduce((acc, room) => { + acc = acc + room.child?.length + return acc + }, 0) + validateBedTypes(inputData.rooms) + } + + return create()((set, get) => ({ + ...inputData, + increaseAdults: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1 + state.adultCount = state.adultCount + 1 }) - state.childCount = state.childCount + 1 - }) - ), - decreaseChildren: (roomIndex) => { - set( - produce((state: GuestsRooms) => { - const roomChildren = state.rooms[roomIndex].children - if ( - roomChildren.length && - roomChildren[roomChildren.length - 1].bed == BedTypeEnum.IN_ADULTS_BED - ) { + ), + decreaseAdults: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1 + state.adultCount = state.adultCount - 1 + if ( + state.rooms[roomIndex].childrenInAdultsBed > + state.rooms[roomIndex].adults + ) { + const toUpdateIndex = state.rooms[roomIndex].child.findIndex( + (child) => child.bed == BedTypeEnum.IN_ADULTS_BED + ) + if (toUpdateIndex != -1) { + state.rooms[roomIndex].child[toUpdateIndex].bed = + state.rooms[roomIndex].child[toUpdateIndex].age < 3 + ? BedTypeEnum.IN_CRIB + : BedTypeEnum.IN_EXTRA_BED + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].adults + } + } + }) + ), + increaseChildren: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].child.push({ + age: -1, + bed: -1, + }) + state.childCount = state.childCount + 1 + }) + ), + decreaseChildren: (roomIndex) => { + set( + produce((state: GuestsRoomsState) => { + const roomChildren = state.rooms[roomIndex].child + if ( + roomChildren.length && + roomChildren[roomChildren.length - 1].bed == + BedTypeEnum.IN_ADULTS_BED + ) { + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].childrenInAdultsBed - 1 + } + state.rooms[roomIndex].child.pop() + state.childCount = state.childCount - 1 + }) + ) + return get().rooms[roomIndex].child + }, + updateChildAge: (age, roomIndex, childIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].child[childIndex].age = age + }) + ), + updateChildBed: (bed, roomIndex, childIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].child[childIndex].bed = bed + }) + ), + increaseChildInAdultsBed: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].childrenInAdultsBed + 1 + }) + ), + decreaseChildInAdultsBed: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { state.rooms[roomIndex].childrenInAdultsBed = state.rooms[roomIndex].childrenInAdultsBed - 1 - } - state.rooms[roomIndex].children.pop() - state.childCount = state.childCount - 1 - }) - ) - return get().rooms[roomIndex].children - }, - updateChildAge: (age, roomIndex, childIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].children[childIndex].age = age - }) - ), - updateChildBed: (bed, roomIndex, childIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].children[childIndex].bed = bed - }) - ), - increaseChildInAdultsBed: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].childrenInAdultsBed + 1 - }) - ), - decreaseChildInAdultsBed: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].childrenInAdultsBed - 1 - }) - ), - increaseRoom: () => - set( - produce((state: GuestsRooms) => { - state.rooms.push({ - adults: 1, - children: [], - childrenInAdultsBed: 0, }) - }) - ), - decreaseRoom: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms.splice(roomIndex, 1) - }) - ), - setIsValidated: (isValidated) => set(() => ({ isValidated })), -})) + ), + increaseRoom: () => + set( + produce((state: GuestsRoomsState) => { + state.rooms.push({ + adults: 1, + child: [], + childrenInAdultsBed: 0, + }) + }) + ), + decreaseRoom: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms.splice(roomIndex, 1) + }) + ), + setIsValidated: (isValidated) => set(() => ({ isValidated })), + })) +} + +export type GuestsRoomsStore = ReturnType + +export const GuestsRoomsContext = createContext(null) + +export const useGuestsRoomsStore = ( + selector: (store: GuestsRoomsStoreState) => T +): T => { + const guestsRoomsContextStore = useContext(GuestsRoomsContext) + + if (!guestsRoomsContextStore) { + throw new Error( + `guestsRoomsContextStore must be used within GuestsRoomsContextProvider` + ) + } + + return useStore(guestsRoomsContextStore, selector) +} diff --git a/types/components/bookingWidget/guestsRoomsPicker.ts b/types/components/bookingWidget/guestsRoomsPicker.ts index 5b3b7b3e2..61e8f7d7a 100644 --- a/types/components/bookingWidget/guestsRoomsPicker.ts +++ b/types/components/bookingWidget/guestsRoomsPicker.ts @@ -10,7 +10,7 @@ export type Child = { export type GuestsRoom = { adults: number - children: Child[] + child: Child[] } export interface GuestsRoomsPickerProps { diff --git a/types/components/bookingWidget/index.ts b/types/components/bookingWidget/index.ts index 8652b63da..a6230b0b2 100644 --- a/types/components/bookingWidget/index.ts +++ b/types/components/bookingWidget/index.ts @@ -4,6 +4,8 @@ import { z } from "zod" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants" +import { GuestsRoom } from "./guestsRoomsPicker" + import type { Locations } from "@/types/trpc/routers/hotel/locations" export type BookingWidgetSchema = z.output @@ -13,22 +15,27 @@ export type BookingWidgetSearchParams = { hotel?: string fromDate?: string toDate?: string - room?: string + room?: GuestsRoom[] + [key: string]: string | string[] | GuestsRoom[] | undefined } export type BookingWidgetType = VariantProps< typeof bookingWidgetVariants >["type"] +export interface BookingWidgetPageProps { + searchParams?: URLSearchParams +} + export interface BookingWidgetProps { type?: BookingWidgetType - searchParams?: BookingWidgetSearchParams + searchParams?: URLSearchParams } export interface BookingWidgetClientProps { locations: Locations type?: BookingWidgetType - searchParams?: BookingWidgetSearchParams + searchParams?: URLSearchParams } export interface BookingWidgetToggleButtonProps { diff --git a/utils/url.ts b/utils/url.ts index d9be2c491..4366fa181 100644 --- a/utils/url.ts +++ b/utils/url.ts @@ -9,3 +9,36 @@ export function removeTrailingSlash(pathname: string) { } return pathname } + +export function getFormattedUrlQueryParams( + searchParams: URLSearchParams, + dataTypes: Record +) { + const searchParamsObject: Record = Array.from( + searchParams.entries() + ).reduce>( + (acc, [key, value]) => { + const keys = key.replace(/\]/g, "").split(/\[|\./) // Split keys by '[' or '.' + keys.reduce((nestedAcc, k, i) => { + if (i === keys.length - 1) { + if (dataTypes[k] == "number") { + ;(nestedAcc as Record)[k] = Number(value) + } else if (dataTypes[k] == "boolean") { + ;(nestedAcc as Record)[k] = + value.toLowerCase() === "true" + } else { + ;(nestedAcc as Record)[k] = value + } + } else { + if (!nestedAcc[k]) { + nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as object or array + } + } + return nestedAcc[k] as Record + }, acc) + return acc + }, + {} as Record + ) + return searchParamsObject +} From 24384c08f3aa332a1c8aeda08b31d42b29aaa844 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Mon, 28 Oct 2024 12:04:17 +0100 Subject: [PATCH 14/49] sub-task: SW-685 Optimized params --- app/[lang]/(live)/@bookingwidget/page.tsx | 4 ++-- server/routers/contentstack/bookingwidget/query.ts | 2 +- types/components/bookingWidget/index.ts | 5 ----- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 2d43c7773..6c944ae69 100644 --- a/app/[lang]/(live)/@bookingwidget/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -3,11 +3,11 @@ import { serverClient } from "@/lib/trpc/server" import BookingWidget, { preload } from "@/components/BookingWidget" -import { BookingWidgetPageProps } from "@/types/components/bookingWidget" +import { PageArgs } from "@/types/params" export default async function BookingWidgetPage({ searchParams, -}: BookingWidgetPageProps) { +}: PageArgs<{}, URLSearchParams>) { if (env.HIDE_FOR_NEXT_RELEASE) { return null } diff --git a/server/routers/contentstack/bookingwidget/query.ts b/server/routers/contentstack/bookingwidget/query.ts index c8a64f20e..f87899965 100644 --- a/server/routers/contentstack/bookingwidget/query.ts +++ b/server/routers/contentstack/bookingwidget/query.ts @@ -26,7 +26,7 @@ export const bookingwidgetQueryRouter = router({ const failedResponse = { hideBookingWidget: false } const { contentType, uid, lang } = ctx - // This condition is to handle 404 page case + // This condition is to handle 404 page case and booking flow if (!contentType || !uid) { console.log("No proper params defined: ", contentType, uid) return failedResponse diff --git a/types/components/bookingWidget/index.ts b/types/components/bookingWidget/index.ts index a6230b0b2..c668617da 100644 --- a/types/components/bookingWidget/index.ts +++ b/types/components/bookingWidget/index.ts @@ -16,17 +16,12 @@ export type BookingWidgetSearchParams = { fromDate?: string toDate?: string room?: GuestsRoom[] - [key: string]: string | string[] | GuestsRoom[] | undefined } export type BookingWidgetType = VariantProps< typeof bookingWidgetVariants >["type"] -export interface BookingWidgetPageProps { - searchParams?: URLSearchParams -} - export interface BookingWidgetProps { type?: BookingWidgetType searchParams?: URLSearchParams From 4bd089ab2a8791452f0ec9dfbcbaf76d4cc88779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= Date: Tue, 29 Oct 2024 09:20:27 +0100 Subject: [PATCH 15/49] feat(698): fix correct order of blocks from CS --- .../Query/ContentPage/ContentPage.graphql | 3 ++ .../routers/contentstack/contentPage/query.ts | 35 ++++++++++++++----- .../trpc/routers/contentstack/contentPage.ts | 3 +- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index 20f910aa9..d98771450 100644 --- a/lib/graphql/Query/ContentPage/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage/ContentPage.graphql @@ -26,6 +26,9 @@ query GetContentPage($locale: String!, $uid: String!) { preamble ...NavigationLinks } + blocks { + __typename + } sidebar { __typename ...ContentSidebar_ContentPage diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index dc7659dc8..e7ec7e24c 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -19,7 +19,11 @@ import { TrackingChannelEnum, type TrackingSDKPageData, } from "@/types/components/tracking" -import type { GetContentPageSchema } from "@/types/trpc/routers/contentstack/contentPage" +import { ContentPageEnum } from "@/types/enums/contentPage" +import type { + GetBlock, + GetContentPageSchema, +} from "@/types/trpc/routers/contentstack/contentPage" export const contentPageQueryRouter = router({ get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { @@ -79,21 +83,36 @@ export const contentPageQueryRouter = router({ ), ]) + const blocksOrder = mainResponse.data.content_page.blocks?.map( + (block) => block.__typename + ) + + let sortedBlocks + if (blocksOrder) { + const blocks = [ + blocksResponse1.data.content_page.blocks, + blocksResponse2.data.content_page.blocks, + ] + .flat(2) + .filter((obj) => !(obj && Object.keys(obj).length < 2)) + // Remove empty objects and objects with only typename + + sortedBlocks = blocksOrder + .map((typename: ContentPageEnum.ContentStack.blocks) => + blocks.find((block) => block?.__typename === typename) + ) + .filter((block): block is GetBlock => !!block) + } + const responseData = { ...mainResponse.data, content_page: { ...mainResponse.data.content_page, - blocks: [ - blocksResponse1.data.content_page.blocks, - blocksResponse2.data.content_page.blocks, - ] - .flat(2) - .filter((obj) => !(obj && Object.keys(obj).length < 2)), // Remove empty objects and objects with only typename + blocks: sortedBlocks, }, } const contentPage = contentPageSchema.safeParse(responseData) - if (!contentPage.success) { console.error( `Failed to validate Contentpage Data - (lang: ${lang}, uid: ${uid})` diff --git a/types/trpc/routers/contentstack/contentPage.ts b/types/trpc/routers/contentstack/contentPage.ts index e3f0f99bb..e1b71bff5 100644 --- a/types/trpc/routers/contentstack/contentPage.ts +++ b/types/trpc/routers/contentstack/contentPage.ts @@ -4,7 +4,6 @@ import { blocksSchema, contentPageRefsSchema, contentPageSchema, - contentPageSchemaBlocks, sidebarSchema, } from "@/server/routers/contentstack/contentPage/output" @@ -21,4 +20,6 @@ export interface ContentPage extends z.output {} export type Block = z.output +export type GetBlock = z.input + export type SidebarBlock = z.output From bc43573d43fbe9939b606ea44efa2cad0ae40668 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Fri, 25 Oct 2024 16:37:41 +0200 Subject: [PATCH 16/49] fix: remove ids from svgs to prevent duplicate ids when rendering icon multiple times --- components/Icons/Ac.tsx | 21 +---- components/Icons/Accesories.tsx | 21 +---- components/Icons/Accessibility.tsx | 21 +---- components/Icons/Air.tsx | 21 +---- components/Icons/Airplane.tsx | 21 +---- components/Icons/ArrowRight.tsx | 21 +---- components/Icons/Bar.tsx | 21 +---- components/Icons/Bathtub.tsx | 21 +---- components/Icons/BedDouble.tsx | 21 +---- components/Icons/Biking.tsx | 21 +---- components/Icons/Breakfast.tsx | 21 +---- components/Icons/Business.tsx | 21 +---- components/Icons/Calendar.tsx | 21 +---- components/Icons/Camera.tsx | 21 +---- components/Icons/Cellphone.tsx | 21 +---- components/Icons/Chair.tsx | 21 +---- components/Icons/Check.tsx | 21 +---- components/Icons/CheckCircle.tsx | 21 +---- components/Icons/ChevronDown.tsx | 21 +---- components/Icons/ChevronLeft.tsx | 21 +---- components/Icons/ChevronRight.tsx | 21 +---- components/Icons/City.tsx | 21 +---- components/Icons/Close.tsx | 21 +---- components/Icons/CloseLarge.tsx | 21 +---- components/Icons/CoffeeAlt.tsx | 21 +---- components/Icons/Concierge.tsx | 21 +---- components/Icons/ConvenienceStore24h.tsx | 21 +---- components/Icons/Cool.tsx | 21 +---- components/Icons/Copy.tsx | 21 +---- components/Icons/CreditCard.tsx | 21 +---- components/Icons/CrossCircle.tsx | 21 +---- components/Icons/Cultural.tsx | 21 +---- components/Icons/Delete.tsx | 21 +---- components/Icons/Desk.tsx | 21 +---- components/Icons/DoorOpen.tsx | 21 +---- components/Icons/Download.tsx | 21 +---- components/Icons/Dresser.tsx | 21 +---- components/Icons/Edit.tsx | 21 +---- components/Icons/ElectricBike.tsx | 21 +---- components/Icons/ElectricCar.tsx | 21 +---- components/Icons/Email.tsx | 21 +---- components/Icons/ErrorCircle.tsx | 21 +---- components/Icons/EyeHide.tsx | 21 +---- components/Icons/EyeShow.tsx | 21 +---- components/Icons/Fan.tsx | 21 +---- components/Icons/Fitness.tsx | 21 +---- components/Icons/Footstool.tsx | 21 +---- components/Icons/Gallery.tsx | 21 +---- components/Icons/Garage.tsx | 21 +---- components/Icons/Gift.tsx | 21 +---- components/Icons/Globe.tsx | 21 +---- components/Icons/Golf.tsx | 21 +---- components/Icons/Groceries.tsx | 21 +---- components/Icons/Hairdryer.tsx | 57 +++++------- components/Icons/HandSoap.tsx | 32 ++----- components/Icons/Hanger.tsx | 21 +---- components/Icons/HangerAlt.tsx | 21 +---- components/Icons/Heart.tsx | 21 +---- components/Icons/Heat.tsx | 21 +---- components/Icons/House.tsx | 28 ++---- components/Icons/Image.tsx | 21 +---- components/Icons/InfoCircle.tsx | 21 +---- components/Icons/Iron.tsx | 21 +---- components/Icons/Kayaking.tsx | 21 +---- components/Icons/Kettle.tsx | 21 +---- components/Icons/KingBed.tsx | 13 ++- components/Icons/Lamp.tsx | 21 +---- components/Icons/LaundryMachine.tsx | 21 +---- components/Icons/LocalBar.tsx | 21 +---- components/Icons/Location.tsx | 21 +---- components/Icons/Lock.tsx | 21 +---- components/Icons/Map.tsx | 21 +---- components/Icons/Museum.tsx | 21 +---- components/Icons/Nature.tsx | 21 +---- components/Icons/Nightlife.tsx | 21 +---- components/Icons/NoBreakfast.tsx | 21 +---- components/Icons/NoSmoking.tsx | 21 +---- components/Icons/OutdoorFurniture.tsx | 21 +---- components/Icons/Parking.tsx | 21 +---- components/Icons/People2.tsx | 21 +---- components/Icons/Person.tsx | 21 +---- components/Icons/Pets.tsx | 21 +---- components/Icons/Phone.tsx | 21 +---- components/Icons/PlusCircle.tsx | 21 +---- components/Icons/PriceTag.tsx | 21 +---- components/Icons/Printer.tsx | 21 +---- components/Icons/Restaurant.tsx | 21 +---- components/Icons/RoomService.tsx | 21 +---- components/Icons/SafetyBox.tsx | 33 +++---- components/Icons/Sauna.tsx | 21 +---- components/Icons/Search.tsx | 21 +---- components/Icons/Service.tsx | 21 +---- components/Icons/Shopping.tsx | 21 +---- components/Icons/Shower.tsx | 21 +---- components/Icons/Skateboarding.tsx | 21 +---- components/Icons/Smoking.tsx | 21 +---- components/Icons/Spa.tsx | 21 +---- components/Icons/StarFilled.tsx | 21 +---- components/Icons/Street.tsx | 21 +---- components/Icons/Swim.tsx | 21 +---- components/Icons/Thermostat.tsx | 21 +---- components/Icons/Train.tsx | 21 +---- components/Icons/TripAdvisor.tsx | 25 ++---- components/Icons/Tshirt.tsx | 21 +---- components/Icons/TshirtWash.tsx | 21 +---- components/Icons/TvCasting.tsx | 21 +---- components/Icons/WarningTriangle.tsx | 21 +---- components/Icons/Wifi.tsx | 21 +---- components/Icons/WindowCurtainsAlt.tsx | 21 +---- components/Icons/WindowNotAvailable.tsx | 45 ++++------ components/Icons/WineBar.tsx | 21 +---- components/Icons/WoodFloor.tsx | 109 ++++++++++------------- components/Icons/Yard.tsx | 21 +---- 113 files changed, 539 insertions(+), 2008 deletions(-) diff --git a/components/Icons/Ac.tsx b/components/Icons/Ac.tsx index 5449112ca..b8b5a781a 100644 --- a/components/Icons/Ac.tsx +++ b/components/Icons/Ac.tsx @@ -14,23 +14,10 @@ export default function AcIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Accesories.tsx b/components/Icons/Accesories.tsx index 9aaf0c894..4ca631cee 100644 --- a/components/Icons/Accesories.tsx +++ b/components/Icons/Accesories.tsx @@ -18,23 +18,10 @@ export default function AccesoriesIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Accessibility.tsx b/components/Icons/Accessibility.tsx index 60d9a79d4..bf711678d 100644 --- a/components/Icons/Accessibility.tsx +++ b/components/Icons/Accessibility.tsx @@ -18,23 +18,10 @@ export default function AccessibilityIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Air.tsx b/components/Icons/Air.tsx index 239566515..75b19a56c 100644 --- a/components/Icons/Air.tsx +++ b/components/Icons/Air.tsx @@ -14,23 +14,10 @@ export default function AirIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Airplane.tsx b/components/Icons/Airplane.tsx index c68c37ce1..e68a911b2 100644 --- a/components/Icons/Airplane.tsx +++ b/components/Icons/Airplane.tsx @@ -18,23 +18,10 @@ export default function AirplaneIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/ArrowRight.tsx b/components/Icons/ArrowRight.tsx index 5f05e5f09..d21fb311c 100644 --- a/components/Icons/ArrowRight.tsx +++ b/components/Icons/ArrowRight.tsx @@ -20,23 +20,10 @@ export default function ArrowRightIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Bar.tsx b/components/Icons/Bar.tsx index 45796be66..8ab06dcd7 100644 --- a/components/Icons/Bar.tsx +++ b/components/Icons/Bar.tsx @@ -14,23 +14,10 @@ export default function BarIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Bathtub.tsx b/components/Icons/Bathtub.tsx index 26ab4ed54..52c8a0265 100644 --- a/components/Icons/Bathtub.tsx +++ b/components/Icons/Bathtub.tsx @@ -14,23 +14,10 @@ export default function BathtubIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/BedDouble.tsx b/components/Icons/BedDouble.tsx index 6eb59d7b0..be6e87ac8 100644 --- a/components/Icons/BedDouble.tsx +++ b/components/Icons/BedDouble.tsx @@ -18,23 +18,10 @@ export default function BedDoubleIcon({ className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/Biking.tsx b/components/Icons/Biking.tsx index 1cc9b3b9f..2ed9143b5 100644 --- a/components/Icons/Biking.tsx +++ b/components/Icons/Biking.tsx @@ -14,23 +14,10 @@ export default function BikingIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Breakfast.tsx b/components/Icons/Breakfast.tsx index dccfc0c39..6bd0e3705 100644 --- a/components/Icons/Breakfast.tsx +++ b/components/Icons/Breakfast.tsx @@ -18,23 +18,10 @@ export default function BreakfastIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Business.tsx b/components/Icons/Business.tsx index cb6ded53d..57e0e7f9f 100644 --- a/components/Icons/Business.tsx +++ b/components/Icons/Business.tsx @@ -18,23 +18,10 @@ export default function BusinessIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Calendar.tsx b/components/Icons/Calendar.tsx index eaa8f42ac..82f4ba811 100644 --- a/components/Icons/Calendar.tsx +++ b/components/Icons/Calendar.tsx @@ -18,23 +18,10 @@ export default function CalendarIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Camera.tsx b/components/Icons/Camera.tsx index 728a9e1c1..a41729cb6 100644 --- a/components/Icons/Camera.tsx +++ b/components/Icons/Camera.tsx @@ -14,23 +14,10 @@ export default function CameraIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Cellphone.tsx b/components/Icons/Cellphone.tsx index be9d5c0af..d40fdbc8d 100644 --- a/components/Icons/Cellphone.tsx +++ b/components/Icons/Cellphone.tsx @@ -18,23 +18,10 @@ export default function CellphoneIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Chair.tsx b/components/Icons/Chair.tsx index d7cdc81a2..0696cea8f 100644 --- a/components/Icons/Chair.tsx +++ b/components/Icons/Chair.tsx @@ -14,23 +14,10 @@ export default function ChairIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Check.tsx b/components/Icons/Check.tsx index 47c5067a0..b9b266bb0 100644 --- a/components/Icons/Check.tsx +++ b/components/Icons/Check.tsx @@ -14,23 +14,10 @@ export default function CheckIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/CheckCircle.tsx b/components/Icons/CheckCircle.tsx index f8278019b..55f100ceb 100644 --- a/components/Icons/CheckCircle.tsx +++ b/components/Icons/CheckCircle.tsx @@ -18,23 +18,10 @@ export default function CheckCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/ChevronDown.tsx b/components/Icons/ChevronDown.tsx index 1bf541d43..b1370284c 100644 --- a/components/Icons/ChevronDown.tsx +++ b/components/Icons/ChevronDown.tsx @@ -18,23 +18,10 @@ export default function ChevronDownIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/ChevronLeft.tsx b/components/Icons/ChevronLeft.tsx index eb14d07dd..9676480a0 100644 --- a/components/Icons/ChevronLeft.tsx +++ b/components/Icons/ChevronLeft.tsx @@ -18,23 +18,10 @@ export default function ChevronLeftIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/ChevronRight.tsx b/components/Icons/ChevronRight.tsx index 9930ac095..98c1a57a5 100644 --- a/components/Icons/ChevronRight.tsx +++ b/components/Icons/ChevronRight.tsx @@ -18,23 +18,10 @@ export default function ChevronRightIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/City.tsx b/components/Icons/City.tsx index 791242ade..9f101a27d 100644 --- a/components/Icons/City.tsx +++ b/components/Icons/City.tsx @@ -14,23 +14,10 @@ export default function CityIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/Close.tsx b/components/Icons/Close.tsx index 6fdb13a67..4cd21c489 100644 --- a/components/Icons/Close.tsx +++ b/components/Icons/Close.tsx @@ -14,23 +14,10 @@ export default function CloseIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/CloseLarge.tsx b/components/Icons/CloseLarge.tsx index e6692f914..2ea89eb5b 100644 --- a/components/Icons/CloseLarge.tsx +++ b/components/Icons/CloseLarge.tsx @@ -18,23 +18,10 @@ export default function CloseLargeIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/CoffeeAlt.tsx b/components/Icons/CoffeeAlt.tsx index e76da5126..1f3051176 100644 --- a/components/Icons/CoffeeAlt.tsx +++ b/components/Icons/CoffeeAlt.tsx @@ -18,23 +18,10 @@ export default function CoffeeAltIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Concierge.tsx b/components/Icons/Concierge.tsx index 69119a969..24362e8c3 100644 --- a/components/Icons/Concierge.tsx +++ b/components/Icons/Concierge.tsx @@ -18,23 +18,10 @@ export default function ConciergeIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/ConvenienceStore24h.tsx b/components/Icons/ConvenienceStore24h.tsx index ace46579c..1c7696aa9 100644 --- a/components/Icons/ConvenienceStore24h.tsx +++ b/components/Icons/ConvenienceStore24h.tsx @@ -18,23 +18,10 @@ export default function ConvenienceStore24hIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Cool.tsx b/components/Icons/Cool.tsx index efa5ae67f..2deb081ad 100644 --- a/components/Icons/Cool.tsx +++ b/components/Icons/Cool.tsx @@ -14,23 +14,10 @@ export default function CoolIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Copy.tsx b/components/Icons/Copy.tsx index 05a5268a5..738456695 100644 --- a/components/Icons/Copy.tsx +++ b/components/Icons/Copy.tsx @@ -14,23 +14,10 @@ export default function CopyIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/CreditCard.tsx b/components/Icons/CreditCard.tsx index ae74314bb..d81ce95f2 100644 --- a/components/Icons/CreditCard.tsx +++ b/components/Icons/CreditCard.tsx @@ -18,23 +18,10 @@ export default function CreditCardIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/CrossCircle.tsx b/components/Icons/CrossCircle.tsx index f4c4c258a..f2c188a75 100644 --- a/components/Icons/CrossCircle.tsx +++ b/components/Icons/CrossCircle.tsx @@ -18,23 +18,10 @@ export default function CrossCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Cultural.tsx b/components/Icons/Cultural.tsx index ede6800b6..af3b9894f 100644 --- a/components/Icons/Cultural.tsx +++ b/components/Icons/Cultural.tsx @@ -18,23 +18,10 @@ export default function CulturalIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Delete.tsx b/components/Icons/Delete.tsx index c91d632a8..2a4150666 100644 --- a/components/Icons/Delete.tsx +++ b/components/Icons/Delete.tsx @@ -14,23 +14,10 @@ export default function DeleteIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Desk.tsx b/components/Icons/Desk.tsx index de9d79ac6..289a227a8 100644 --- a/components/Icons/Desk.tsx +++ b/components/Icons/Desk.tsx @@ -14,23 +14,10 @@ export default function DeskIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/DoorOpen.tsx b/components/Icons/DoorOpen.tsx index 93bc2caf4..27957f2bd 100644 --- a/components/Icons/DoorOpen.tsx +++ b/components/Icons/DoorOpen.tsx @@ -18,23 +18,10 @@ export default function DoorOpenIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Download.tsx b/components/Icons/Download.tsx index 7c1e9017a..3e47c8a5a 100644 --- a/components/Icons/Download.tsx +++ b/components/Icons/Download.tsx @@ -18,23 +18,10 @@ export default function DownloadIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Dresser.tsx b/components/Icons/Dresser.tsx index b81f2bece..2b7d06f1c 100644 --- a/components/Icons/Dresser.tsx +++ b/components/Icons/Dresser.tsx @@ -14,23 +14,10 @@ export default function DresserIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Edit.tsx b/components/Icons/Edit.tsx index 209e16883..b0fa6141e 100644 --- a/components/Icons/Edit.tsx +++ b/components/Icons/Edit.tsx @@ -14,23 +14,10 @@ export default function EditIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/ElectricBike.tsx b/components/Icons/ElectricBike.tsx index 44e796cd8..1e0787c63 100644 --- a/components/Icons/ElectricBike.tsx +++ b/components/Icons/ElectricBike.tsx @@ -18,23 +18,10 @@ export default function ElectricBikeIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/ElectricCar.tsx b/components/Icons/ElectricCar.tsx index 1f9500b64..1af91af32 100644 --- a/components/Icons/ElectricCar.tsx +++ b/components/Icons/ElectricCar.tsx @@ -18,23 +18,10 @@ export default function ElectricCarIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Email.tsx b/components/Icons/Email.tsx index 8915e3991..7f781781f 100644 --- a/components/Icons/Email.tsx +++ b/components/Icons/Email.tsx @@ -20,23 +20,10 @@ export default function EmailIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/ErrorCircle.tsx b/components/Icons/ErrorCircle.tsx index 87b1514d3..4ba0fcde8 100644 --- a/components/Icons/ErrorCircle.tsx +++ b/components/Icons/ErrorCircle.tsx @@ -18,23 +18,10 @@ export default function ErrorCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/EyeHide.tsx b/components/Icons/EyeHide.tsx index cc1db5926..c3da6d258 100644 --- a/components/Icons/EyeHide.tsx +++ b/components/Icons/EyeHide.tsx @@ -14,23 +14,10 @@ export default function EyeHideIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/EyeShow.tsx b/components/Icons/EyeShow.tsx index c3fdaf17d..fc308c352 100644 --- a/components/Icons/EyeShow.tsx +++ b/components/Icons/EyeShow.tsx @@ -14,23 +14,10 @@ export default function EyeShowIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Fan.tsx b/components/Icons/Fan.tsx index 128200104..394d013f2 100644 --- a/components/Icons/Fan.tsx +++ b/components/Icons/Fan.tsx @@ -14,23 +14,10 @@ export default function FanIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Fitness.tsx b/components/Icons/Fitness.tsx index 69b965a7b..113a90fb0 100644 --- a/components/Icons/Fitness.tsx +++ b/components/Icons/Fitness.tsx @@ -14,23 +14,10 @@ export default function FitnessIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Footstool.tsx b/components/Icons/Footstool.tsx index becb0fd80..6d7c6cc42 100644 --- a/components/Icons/Footstool.tsx +++ b/components/Icons/Footstool.tsx @@ -18,23 +18,10 @@ export default function FootstoolIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Gallery.tsx b/components/Icons/Gallery.tsx index 5bf1bc8b6..005f539f2 100644 --- a/components/Icons/Gallery.tsx +++ b/components/Icons/Gallery.tsx @@ -14,23 +14,10 @@ export default function GalleryIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Garage.tsx b/components/Icons/Garage.tsx index b715532cc..597c3e90b 100644 --- a/components/Icons/Garage.tsx +++ b/components/Icons/Garage.tsx @@ -14,23 +14,10 @@ export default function GarageIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Gift.tsx b/components/Icons/Gift.tsx index b07015db5..09914ab54 100644 --- a/components/Icons/Gift.tsx +++ b/components/Icons/Gift.tsx @@ -14,23 +14,10 @@ export default function GiftIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Globe.tsx b/components/Icons/Globe.tsx index 7ea19bf87..16bda388e 100644 --- a/components/Icons/Globe.tsx +++ b/components/Icons/Globe.tsx @@ -14,23 +14,10 @@ export default function GlobeIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Golf.tsx b/components/Icons/Golf.tsx index 027c906d7..a685ea440 100644 --- a/components/Icons/Golf.tsx +++ b/components/Icons/Golf.tsx @@ -14,23 +14,10 @@ export default function GolfIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Groceries.tsx b/components/Icons/Groceries.tsx index 404f09f7b..69410ed29 100644 --- a/components/Icons/Groceries.tsx +++ b/components/Icons/Groceries.tsx @@ -18,23 +18,10 @@ export default function GroceriesIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Hairdryer.tsx b/components/Icons/Hairdryer.tsx index 4d6ca72b2..bca9ef2e7 100644 --- a/components/Icons/Hairdryer.tsx +++ b/components/Icons/Hairdryer.tsx @@ -18,41 +18,28 @@ export default function HairdryerIcon({ className={classNames} {...props} > - - - - - - - - - - + + + + + ) } diff --git a/components/Icons/HandSoap.tsx b/components/Icons/HandSoap.tsx index a6850e4fe..70efe77f0 100644 --- a/components/Icons/HandSoap.tsx +++ b/components/Icons/HandSoap.tsx @@ -18,32 +18,12 @@ export default function HandSoapIcon({ className={classNames} {...props} > - - - - - - - - - - - - - + ) } diff --git a/components/Icons/Hanger.tsx b/components/Icons/Hanger.tsx index 63c869619..e75ac5489 100644 --- a/components/Icons/Hanger.tsx +++ b/components/Icons/Hanger.tsx @@ -14,23 +14,10 @@ export default function HangerIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/HangerAlt.tsx b/components/Icons/HangerAlt.tsx index 732656e60..67167cd8c 100644 --- a/components/Icons/HangerAlt.tsx +++ b/components/Icons/HangerAlt.tsx @@ -18,23 +18,10 @@ export default function HangerAltIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Heart.tsx b/components/Icons/Heart.tsx index 49cbb1171..3f191732e 100644 --- a/components/Icons/Heart.tsx +++ b/components/Icons/Heart.tsx @@ -14,23 +14,10 @@ export default function HeartIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Heat.tsx b/components/Icons/Heat.tsx index ef093e955..f2c2a4318 100644 --- a/components/Icons/Heat.tsx +++ b/components/Icons/Heat.tsx @@ -14,23 +14,10 @@ export default function HeatIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/House.tsx b/components/Icons/House.tsx index 1ddcf0f74..2fa747d57 100644 --- a/components/Icons/House.tsx +++ b/components/Icons/House.tsx @@ -14,28 +14,12 @@ export default function HouseIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - - - + ) } diff --git a/components/Icons/Image.tsx b/components/Icons/Image.tsx index 9fcfe4a71..aa0b875df 100644 --- a/components/Icons/Image.tsx +++ b/components/Icons/Image.tsx @@ -14,23 +14,10 @@ export default function ImageIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/InfoCircle.tsx b/components/Icons/InfoCircle.tsx index f87f5d663..3d9bc82c1 100644 --- a/components/Icons/InfoCircle.tsx +++ b/components/Icons/InfoCircle.tsx @@ -18,23 +18,10 @@ export default function InfoCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Iron.tsx b/components/Icons/Iron.tsx index ac2ac4882..4f5964a90 100644 --- a/components/Icons/Iron.tsx +++ b/components/Icons/Iron.tsx @@ -14,23 +14,10 @@ export default function IronIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/Kayaking.tsx b/components/Icons/Kayaking.tsx index 1d4061a3a..1c5e61304 100644 --- a/components/Icons/Kayaking.tsx +++ b/components/Icons/Kayaking.tsx @@ -18,23 +18,10 @@ export default function KayakingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Kettle.tsx b/components/Icons/Kettle.tsx index 28713719a..c75521398 100644 --- a/components/Icons/Kettle.tsx +++ b/components/Icons/Kettle.tsx @@ -14,23 +14,10 @@ export default function KettleIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/KingBed.tsx b/components/Icons/KingBed.tsx index d4df0f225..5e0f0615d 100644 --- a/components/Icons/KingBed.tsx +++ b/components/Icons/KingBed.tsx @@ -14,14 +14,11 @@ export default function KingBedIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - + ) } diff --git a/components/Icons/Lamp.tsx b/components/Icons/Lamp.tsx index bde8af3dd..785a09dce 100644 --- a/components/Icons/Lamp.tsx +++ b/components/Icons/Lamp.tsx @@ -14,23 +14,10 @@ export default function LampIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/LaundryMachine.tsx b/components/Icons/LaundryMachine.tsx index b4225197d..85f0bbb17 100644 --- a/components/Icons/LaundryMachine.tsx +++ b/components/Icons/LaundryMachine.tsx @@ -18,23 +18,10 @@ export default function LaundryMachineIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/LocalBar.tsx b/components/Icons/LocalBar.tsx index 7a5b3bb51..cd6b2f5dc 100644 --- a/components/Icons/LocalBar.tsx +++ b/components/Icons/LocalBar.tsx @@ -18,23 +18,10 @@ export default function LocalBarIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Location.tsx b/components/Icons/Location.tsx index b5d1d0e64..1b4eba611 100644 --- a/components/Icons/Location.tsx +++ b/components/Icons/Location.tsx @@ -18,23 +18,10 @@ export default function LocationIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Lock.tsx b/components/Icons/Lock.tsx index ce38caa46..5a29f5f3b 100644 --- a/components/Icons/Lock.tsx +++ b/components/Icons/Lock.tsx @@ -14,23 +14,10 @@ export default function LockIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Map.tsx b/components/Icons/Map.tsx index 9571db13b..e8b1b5727 100644 --- a/components/Icons/Map.tsx +++ b/components/Icons/Map.tsx @@ -14,23 +14,10 @@ export default function MapIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Museum.tsx b/components/Icons/Museum.tsx index d9a774908..5cbada97a 100644 --- a/components/Icons/Museum.tsx +++ b/components/Icons/Museum.tsx @@ -14,23 +14,10 @@ export default function MuseumIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Nature.tsx b/components/Icons/Nature.tsx index 4c48c377f..592620ab9 100644 --- a/components/Icons/Nature.tsx +++ b/components/Icons/Nature.tsx @@ -14,23 +14,10 @@ export default function NatureIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Nightlife.tsx b/components/Icons/Nightlife.tsx index 093099cb9..817f2ed5d 100644 --- a/components/Icons/Nightlife.tsx +++ b/components/Icons/Nightlife.tsx @@ -18,23 +18,10 @@ export default function NightlifeIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/NoBreakfast.tsx b/components/Icons/NoBreakfast.tsx index c09af6616..063b5defe 100644 --- a/components/Icons/NoBreakfast.tsx +++ b/components/Icons/NoBreakfast.tsx @@ -18,23 +18,10 @@ export default function NoBreakfastIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/NoSmoking.tsx b/components/Icons/NoSmoking.tsx index bdaa7d3f3..9ca2be0de 100644 --- a/components/Icons/NoSmoking.tsx +++ b/components/Icons/NoSmoking.tsx @@ -18,23 +18,10 @@ export default function NoSmokingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/OutdoorFurniture.tsx b/components/Icons/OutdoorFurniture.tsx index 3ddac9f38..d0cdb78f3 100644 --- a/components/Icons/OutdoorFurniture.tsx +++ b/components/Icons/OutdoorFurniture.tsx @@ -18,23 +18,10 @@ export default function OutdoorFurnitureIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Parking.tsx b/components/Icons/Parking.tsx index 3680757e4..cbcf30927 100644 --- a/components/Icons/Parking.tsx +++ b/components/Icons/Parking.tsx @@ -14,23 +14,10 @@ export default function ParkingIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/People2.tsx b/components/Icons/People2.tsx index df5319f33..55f16de36 100644 --- a/components/Icons/People2.tsx +++ b/components/Icons/People2.tsx @@ -14,23 +14,10 @@ export default function People2Icon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Person.tsx b/components/Icons/Person.tsx index bc2452ac4..5f2671194 100644 --- a/components/Icons/Person.tsx +++ b/components/Icons/Person.tsx @@ -14,23 +14,10 @@ export default function PersonIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Pets.tsx b/components/Icons/Pets.tsx index db832a5af..679a3afee 100644 --- a/components/Icons/Pets.tsx +++ b/components/Icons/Pets.tsx @@ -14,23 +14,10 @@ export default function PetsIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Phone.tsx b/components/Icons/Phone.tsx index 1dd1c960e..a63adc769 100644 --- a/components/Icons/Phone.tsx +++ b/components/Icons/Phone.tsx @@ -20,23 +20,10 @@ export default function PhoneIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/PlusCircle.tsx b/components/Icons/PlusCircle.tsx index aa2c40afb..3f391672a 100644 --- a/components/Icons/PlusCircle.tsx +++ b/components/Icons/PlusCircle.tsx @@ -18,23 +18,10 @@ export default function PlusCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/PriceTag.tsx b/components/Icons/PriceTag.tsx index d91e28900..fd735a1d3 100644 --- a/components/Icons/PriceTag.tsx +++ b/components/Icons/PriceTag.tsx @@ -18,23 +18,10 @@ export default function PriceTagIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Printer.tsx b/components/Icons/Printer.tsx index d703940da..e82696d37 100644 --- a/components/Icons/Printer.tsx +++ b/components/Icons/Printer.tsx @@ -14,23 +14,10 @@ export default function PrinterIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Restaurant.tsx b/components/Icons/Restaurant.tsx index 4ac06eb32..8fe36e0e6 100644 --- a/components/Icons/Restaurant.tsx +++ b/components/Icons/Restaurant.tsx @@ -18,23 +18,10 @@ export default function RestaurantIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/RoomService.tsx b/components/Icons/RoomService.tsx index 00aadd7d0..b472c7e2a 100644 --- a/components/Icons/RoomService.tsx +++ b/components/Icons/RoomService.tsx @@ -18,23 +18,10 @@ export default function RoomServiceIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/SafetyBox.tsx b/components/Icons/SafetyBox.tsx index 2b79a063f..7dc9e9590 100644 --- a/components/Icons/SafetyBox.tsx +++ b/components/Icons/SafetyBox.tsx @@ -18,29 +18,16 @@ export default function SafetyBoxIcon({ className={classNames} {...props} > - - - - - - - + + ) } diff --git a/components/Icons/Sauna.tsx b/components/Icons/Sauna.tsx index 13288f6d9..f5535c270 100644 --- a/components/Icons/Sauna.tsx +++ b/components/Icons/Sauna.tsx @@ -14,23 +14,10 @@ export default function SaunaIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Search.tsx b/components/Icons/Search.tsx index aa9f15e52..a849ffcbe 100644 --- a/components/Icons/Search.tsx +++ b/components/Icons/Search.tsx @@ -14,23 +14,10 @@ export default function SearchIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Service.tsx b/components/Icons/Service.tsx index 1f91f7cd8..9ebb97d6a 100644 --- a/components/Icons/Service.tsx +++ b/components/Icons/Service.tsx @@ -14,23 +14,10 @@ export default function ServiceIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Shopping.tsx b/components/Icons/Shopping.tsx index 87b1da6b2..d427f3a38 100644 --- a/components/Icons/Shopping.tsx +++ b/components/Icons/Shopping.tsx @@ -18,23 +18,10 @@ export default function ShoppingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Shower.tsx b/components/Icons/Shower.tsx index 432b5f346..0f54c4829 100644 --- a/components/Icons/Shower.tsx +++ b/components/Icons/Shower.tsx @@ -14,23 +14,10 @@ export default function ShowerIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/Skateboarding.tsx b/components/Icons/Skateboarding.tsx index 6c0106ff3..a94dd0ff2 100644 --- a/components/Icons/Skateboarding.tsx +++ b/components/Icons/Skateboarding.tsx @@ -18,23 +18,10 @@ export default function SkateboardingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Smoking.tsx b/components/Icons/Smoking.tsx index 58c30abce..e382ccc34 100644 --- a/components/Icons/Smoking.tsx +++ b/components/Icons/Smoking.tsx @@ -14,23 +14,10 @@ export default function SmokingIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Spa.tsx b/components/Icons/Spa.tsx index f3141a4e4..7cc26f820 100644 --- a/components/Icons/Spa.tsx +++ b/components/Icons/Spa.tsx @@ -14,23 +14,10 @@ export default function SpaIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/StarFilled.tsx b/components/Icons/StarFilled.tsx index 4e96f47d9..300924a37 100644 --- a/components/Icons/StarFilled.tsx +++ b/components/Icons/StarFilled.tsx @@ -18,23 +18,10 @@ export default function StarFilledIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Street.tsx b/components/Icons/Street.tsx index 414df197c..b2b0c2dfb 100644 --- a/components/Icons/Street.tsx +++ b/components/Icons/Street.tsx @@ -14,23 +14,10 @@ export default function StreetIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Swim.tsx b/components/Icons/Swim.tsx index abd2bd29a..534bb2068 100644 --- a/components/Icons/Swim.tsx +++ b/components/Icons/Swim.tsx @@ -14,23 +14,10 @@ export default function SwimIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Thermostat.tsx b/components/Icons/Thermostat.tsx index 2fd3ebe97..490f2f042 100644 --- a/components/Icons/Thermostat.tsx +++ b/components/Icons/Thermostat.tsx @@ -18,23 +18,10 @@ export default function ThermostatIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Train.tsx b/components/Icons/Train.tsx index 79fae85f4..b892a9064 100644 --- a/components/Icons/Train.tsx +++ b/components/Icons/Train.tsx @@ -14,23 +14,10 @@ export default function TrainIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/TripAdvisor.tsx b/components/Icons/TripAdvisor.tsx index a1fc6d416..0b58ca839 100644 --- a/components/Icons/TripAdvisor.tsx +++ b/components/Icons/TripAdvisor.tsx @@ -18,25 +18,12 @@ export default function TripAdvisorIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Tshirt.tsx b/components/Icons/Tshirt.tsx index e6643725b..4d92d2418 100644 --- a/components/Icons/Tshirt.tsx +++ b/components/Icons/Tshirt.tsx @@ -14,23 +14,10 @@ export default function TshirtIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/TshirtWash.tsx b/components/Icons/TshirtWash.tsx index 7b952490f..58dea52a8 100644 --- a/components/Icons/TshirtWash.tsx +++ b/components/Icons/TshirtWash.tsx @@ -18,23 +18,10 @@ export default function TshirtWashIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/TvCasting.tsx b/components/Icons/TvCasting.tsx index ea6e7b90d..5169f41bf 100644 --- a/components/Icons/TvCasting.tsx +++ b/components/Icons/TvCasting.tsx @@ -18,23 +18,10 @@ export default function TvCastingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/WarningTriangle.tsx b/components/Icons/WarningTriangle.tsx index 78d47d60a..cc4ebb766 100644 --- a/components/Icons/WarningTriangle.tsx +++ b/components/Icons/WarningTriangle.tsx @@ -18,23 +18,10 @@ export default function WarningTriangleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Wifi.tsx b/components/Icons/Wifi.tsx index 80d2d1000..594bbe660 100644 --- a/components/Icons/Wifi.tsx +++ b/components/Icons/Wifi.tsx @@ -14,23 +14,10 @@ export default function BarIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/WindowCurtainsAlt.tsx b/components/Icons/WindowCurtainsAlt.tsx index f32529566..6fe6dce4c 100644 --- a/components/Icons/WindowCurtainsAlt.tsx +++ b/components/Icons/WindowCurtainsAlt.tsx @@ -18,23 +18,10 @@ export default function WindowCurtainsAltIcon({ className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/WindowNotAvailable.tsx b/components/Icons/WindowNotAvailable.tsx index f064e146d..9e938c1e9 100644 --- a/components/Icons/WindowNotAvailable.tsx +++ b/components/Icons/WindowNotAvailable.tsx @@ -18,35 +18,22 @@ export default function WindowNotAvailableIcon({ className={classNames} {...props} > - - - - - - - - + + + ) } diff --git a/components/Icons/WineBar.tsx b/components/Icons/WineBar.tsx index a0ae499d1..1140d4ee6 100644 --- a/components/Icons/WineBar.tsx +++ b/components/Icons/WineBar.tsx @@ -14,23 +14,10 @@ export default function WineBarIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/WoodFloor.tsx b/components/Icons/WoodFloor.tsx index e7eaa4f35..1f57c69ad 100644 --- a/components/Icons/WoodFloor.tsx +++ b/components/Icons/WoodFloor.tsx @@ -18,67 +18,54 @@ export default function WoodFloorIcon({ className={classNames} {...props} > - - - - - - - - - - - - - - - - - + + + + + + + + + + + + ) } diff --git a/components/Icons/Yard.tsx b/components/Icons/Yard.tsx index 9e853b4b0..19c0bd09b 100644 --- a/components/Icons/Yard.tsx +++ b/components/Icons/Yard.tsx @@ -14,23 +14,10 @@ export default function YardIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } From 2fddc503c59808d06252bebdb7ceda516eacc590 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 25 Oct 2024 13:55:08 +0200 Subject: [PATCH 17/49] feat(SW-667): read more section to join loyalty sidebar --- .../Sidebar/JoinLoyalty/ReadMore/index.tsx | 49 +++++++++++++++++++ .../JoinLoyalty/ReadMore/readMore.module.css | 27 ++++++++++ components/Sidebar/JoinLoyalty/index.tsx | 8 +-- .../JoinLoyalty/joinLoyalty.module.css | 7 ++- constants/currentWebHrefs.ts | 18 +++++++ 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 components/Sidebar/JoinLoyalty/ReadMore/index.tsx create mode 100644 components/Sidebar/JoinLoyalty/ReadMore/readMore.module.css diff --git a/components/Sidebar/JoinLoyalty/ReadMore/index.tsx b/components/Sidebar/JoinLoyalty/ReadMore/index.tsx new file mode 100644 index 000000000..8fba3390e --- /dev/null +++ b/components/Sidebar/JoinLoyalty/ReadMore/index.tsx @@ -0,0 +1,49 @@ +import { faq, membershipTermsAndConditions } from "@/constants/currentWebHrefs" + +import ArrowRight from "@/components/Icons/ArrowRight" +import Link from "@/components/TempDesignSystem/Link" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + +import styles from "./readMore.module.css" + +export default async function ReadMore() { + const [intl, lang] = await Promise.all([getIntl(), getLang()]) + + const links = [ + { href: faq[lang], translationId: "FAQ" }, + { + href: membershipTermsAndConditions[lang], + translationId: "Membership terms and conditions", + }, + ] + + return ( +
    + {intl.formatMessage({ id: "Read more" })} +
    + {links.map((link) => { + const translatedText = intl.formatMessage({ id: link.translationId }) + return ( + + + {translatedText} + + ) + })} +
    +
    + ) +} diff --git a/components/Sidebar/JoinLoyalty/ReadMore/readMore.module.css b/components/Sidebar/JoinLoyalty/ReadMore/readMore.module.css new file mode 100644 index 000000000..c7b103996 --- /dev/null +++ b/components/Sidebar/JoinLoyalty/ReadMore/readMore.module.css @@ -0,0 +1,27 @@ +.wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--Spacing-x2); +} + +.links { + display: grid; + gap: var(--Spacing-x-one-and-half); + justify-items: center; +} + +.link { + display: flex; + align-items: center; +} + +@media screen and (min-width: 1367px) { + .wrapper { + align-items: start; + } + + .links { + justify-items: start; + } +} diff --git a/components/Sidebar/JoinLoyalty/index.tsx b/components/Sidebar/JoinLoyalty/index.tsx index 13b3a91c5..3e353551f 100644 --- a/components/Sidebar/JoinLoyalty/index.tsx +++ b/components/Sidebar/JoinLoyalty/index.tsx @@ -10,6 +10,7 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import Contact from "./Contact" +import ReadMore from "./ReadMore" import styles from "./joinLoyalty.module.css" @@ -18,15 +19,15 @@ import type { JoinLoyaltyContactProps } from "@/types/components/sidebar/joinLoy export default async function JoinLoyaltyContact({ block, }: JoinLoyaltyContactProps) { - const intl = await getIntl() - const user = await getName() + const [intl, user] = await Promise.all([getIntl(), getName()]) // Check if we have user, that means we are logged in. if (user) { return null } + return ( -
    +
    {block.title} @@ -70,6 +71,7 @@ export default async function JoinLoyaltyContact({ </section> </article> {block.contact ? <Contact contactBlock={block.contact} /> : null} + <ReadMore /> </section> ) } diff --git a/components/Sidebar/JoinLoyalty/joinLoyalty.module.css b/components/Sidebar/JoinLoyalty/joinLoyalty.module.css index 8ff913bce..5b7129ebf 100644 --- a/components/Sidebar/JoinLoyalty/joinLoyalty.module.css +++ b/components/Sidebar/JoinLoyalty/joinLoyalty.module.css @@ -1,7 +1,12 @@ +.joinLoyaltyContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x5); +} .wrapper { display: grid; gap: var(--Spacing-x3); - padding-bottom: var(--Spacing-x5); + /* padding-bottom: var(--Spacing-x5); */ padding-top: var(--Spacing-x4); justify-items: center; } diff --git a/constants/currentWebHrefs.ts b/constants/currentWebHrefs.ts index 7438ee13a..751e875de 100644 --- a/constants/currentWebHrefs.ts +++ b/constants/currentWebHrefs.ts @@ -18,6 +18,24 @@ export const bookingTermsAndConditions: LangRoute = { sv: `${baseUrl.sv}/kundservice/priser-och-bokningsregler/bokningsregler`, } +export const membershipTermsAndConditions: LangRoute = { + da: `${baseUrl.da}/kundeservice/priser-og-bookingvilkar/vilkar-betingelser-for-medlemsskab`, + de: `${baseUrl.de}/kundenbetreuung/preise-und-richtlinien/scandic-friends-allgemeine-geschaftsbedingungen`, + en: `${baseUrl.en}/customer-service/rates-and-policies/scandic-friends-terms-conditions`, + fi: `${baseUrl.fi}/asiakaspalvelu/hinnat-ja-varausehdot/jasenyyden-ehdot`, + no: `${baseUrl.no}/kundeservice/priser-og-bestillingsvilkar/betingelser-for-medlemskap`, + sv: `${baseUrl.sv}/kundservice/priser-och-bokningsregler/medlemsvillkor`, +} + +export const faq: LangRoute = { + da: `${baseUrl.da}/scandic-friends/hjalp-og-service/ofte-stillede-sporgsmal`, + de: `${baseUrl.de}/scandic-friends/hilfe-und-service/haufig-gestellte-fragen`, + en: `${baseUrl.en}/scandic-friends/help-service/faq`, + fi: `${baseUrl.fi}/scandic-friends/apua-ongelmatilanteissa/usein-kysyttya`, + no: `${baseUrl.no}/scandic-friends/hjelp-og-medlemsservice/ofte-stilte-sporsmal`, + sv: `${baseUrl.sv}/scandic-friends/hjalp-och-service/vanliga-fragor`, +} + export const privacyPolicy: LangRoute = { da: `${baseUrl.da}/kundeservice/priser-og-bookingvilkar/persondatapolitik`, de: `${baseUrl.de}/kundenbetreuung/preise-und-richtlinien/datenschutzrichtlinie`, From 8219ff1931ae5d747b191e640859442ab9c8bf20 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Fri, 25 Oct 2024 15:56:01 +0200 Subject: [PATCH 18/49] fix(SW-667): proper contact padding on mobile + translations --- components/Sidebar/JoinLoyalty/Contact/contact.module.css | 3 ++- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + 7 files changed, 8 insertions(+), 1 deletion(-) diff --git a/components/Sidebar/JoinLoyalty/Contact/contact.module.css b/components/Sidebar/JoinLoyalty/Contact/contact.module.css index 72127e707..59827194c 100644 --- a/components/Sidebar/JoinLoyalty/Contact/contact.module.css +++ b/components/Sidebar/JoinLoyalty/Contact/contact.module.css @@ -4,8 +4,8 @@ flex-direction: column; gap: var(--Spacing-x2); justify-content: center; - padding-top: var(--Spacing-x2); align-items: center; + padding-top: var(--Spacing-x5); } .contact { @@ -21,6 +21,7 @@ @media screen and (min-width: 1367px) { .contactContainer { align-items: start; + padding-top: var(--Spacing-x2); } .contact > div { diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index ee5954d7d..889555466 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -173,6 +173,7 @@ "Membership ID": "Medlems-id", "Membership ID copied to clipboard": "Medlems-ID kopieret til udklipsholder", "Membership cards": "Medlemskort", + "Membership terms and conditions": "Medlemsvilkår og -betingelser", "Menu": "Menu", "Modify": "Ændre", "Month": "Måned", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 9eb8bd1ec..e092862df 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -173,6 +173,7 @@ "Membership ID": "Mitglieds-ID", "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", "Membership cards": "Mitgliedskarten", + "Membership terms and conditions": "Mitgliedschaftsbedingungen", "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index f40909ac9..f52e13d8e 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -184,6 +184,7 @@ "Membership ID": "Membership ID", "Membership ID copied to clipboard": "Membership ID copied to clipboard", "Membership cards": "Membership cards", + "Membership terms and conditions": "Membership terms and conditions", "Menu": "Menu", "Modify": "Modify", "Month": "Month", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index b44c6755d..323dff85f 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -173,6 +173,7 @@ "Membership ID": "Jäsentunnus", "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", "Membership cards": "Jäsenkortit", + "Membership terms and conditions": "Jäsenyysehdot", "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index ab4143986..eb09018fb 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -171,6 +171,7 @@ "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopiert til utklippstavlen", "Membership cards": "Medlemskort", + "Membership terms and conditions": "Medlemsvilkår og -betingelser", "Menu": "Menu", "Modify": "Endre", "Month": "Måned", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 9d7c3b95d..be22979a5 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -171,6 +171,7 @@ "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopierat till urklipp", "Membership cards": "Medlemskort", + "Membership terms and conditions": "Medlemsvillkor", "Menu": "Meny", "Modify": "Ändra", "Month": "Månad", From d15e76de469e6a8bbb4492b2a6665479ea49f792 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Fri, 25 Oct 2024 16:00:18 +0200 Subject: [PATCH 19/49] chore(SW-667): remove unused css --- components/Sidebar/JoinLoyalty/joinLoyalty.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/components/Sidebar/JoinLoyalty/joinLoyalty.module.css b/components/Sidebar/JoinLoyalty/joinLoyalty.module.css index 5b7129ebf..7e85c29bd 100644 --- a/components/Sidebar/JoinLoyalty/joinLoyalty.module.css +++ b/components/Sidebar/JoinLoyalty/joinLoyalty.module.css @@ -6,7 +6,6 @@ .wrapper { display: grid; gap: var(--Spacing-x3); - /* padding-bottom: var(--Spacing-x5); */ padding-top: var(--Spacing-x4); justify-items: center; } From 365cb15f90ec02d3301355756de3b2bd019f6068 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Mon, 28 Oct 2024 15:57:33 +0100 Subject: [PATCH 20/49] chore(SW-667): move formatMessage call to array --- .../Sidebar/JoinLoyalty/ReadMore/index.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/components/Sidebar/JoinLoyalty/ReadMore/index.tsx b/components/Sidebar/JoinLoyalty/ReadMore/index.tsx index 8fba3390e..8890c4172 100644 --- a/components/Sidebar/JoinLoyalty/ReadMore/index.tsx +++ b/components/Sidebar/JoinLoyalty/ReadMore/index.tsx @@ -12,10 +12,13 @@ export default async function ReadMore() { const [intl, lang] = await Promise.all([getIntl(), getLang()]) const links = [ - { href: faq[lang], translationId: "FAQ" }, + { + href: faq[lang], + text: intl.formatMessage({ id: "FAQ" }), + }, { href: membershipTermsAndConditions[lang], - translationId: "Membership terms and conditions", + text: intl.formatMessage({ id: "Membership terms and conditions" }), }, ] @@ -23,26 +26,23 @@ export default async function ReadMore() { <article className={styles.wrapper}> <Subtitle>{intl.formatMessage({ id: "Read more" })}</Subtitle> <div className={styles.links}> - {links.map((link) => { - const translatedText = intl.formatMessage({ id: link.translationId }) - return ( - <Link - key={link.translationId} - size="small" - className={styles.link} + {links.map((link) => ( + <Link + key={link.text} + size="small" + className={styles.link} + color="burgundy" + href={link.href} + > + <ArrowRight color="burgundy" - href={link.href} - > - <ArrowRight - color="burgundy" - className={styles.icon} - height="20" - width="20" - /> - {translatedText} - </Link> - ) - })} + className={styles.icon} + height="20" + width="20" + /> + {link.text} + </Link> + ))} </div> </article> ) From 6c56fbc3134129baf9ece6652e550486e037f4b1 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Mon, 28 Oct 2024 16:30:19 +0100 Subject: [PATCH 21/49] fix(SW-667): update translations based on content team requirements --- i18n/dictionaries/fi.json | 4 ++-- i18n/dictionaries/no.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 323dff85f..afe70ea1b 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -101,7 +101,7 @@ "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "Explore nearby": "Tutustu lähialueeseen", "Extras to your booking": "Varauksessa lisäpalveluita", - "FAQ": "UKK", + "FAQ": "Usein kysytyt kysymykset", "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Fair": "Messukeskus", "Find booking": "Etsi varaus", @@ -173,7 +173,7 @@ "Membership ID": "Jäsentunnus", "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", "Membership cards": "Jäsenkortit", - "Membership terms and conditions": "Jäsenyysehdot", + "Membership terms and conditions": "Jäsenehdot ja -säännöt", "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index eb09018fb..fab5ce670 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -100,7 +100,7 @@ "Explore all levels and benefits": "Utforsk alle nivåer og fordeler", "Explore nearby": "Utforsk i nærheten", "Extras to your booking": "Tilvalg til bestillingen din", - "FAQ": "FAQ", + "FAQ": "Ofte stilte spørsmål", "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Fair": "Messe", "Find booking": "Finn booking", From aea963740b7be8dd65b23d1c75f790a7240ef2e2 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Thu, 17 Oct 2024 13:47:37 +0200 Subject: [PATCH 22/49] feat(sw-453): Added selected types count --- .../(standard)/select-rate/page.tsx | 10 ++++-- .../SelectRate/RoomFilter/index.tsx | 34 +++++++++++++++++++ .../RoomFilter/roomFilter.module.css | 5 +++ .../SelectRate/RoomSelection/index.tsx | 6 ++-- i18n/dictionaries/da.json | 2 +- i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 6 ++-- i18n/dictionaries/fi.json | 4 ++- i18n/dictionaries/no.json | 4 ++- i18n/dictionaries/sv.json | 4 ++- .../hotelReservation/selectRate/roomFilter.ts | 3 ++ .../selectRate/roomSelection.ts | 2 +- 12 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 components/HotelReservation/SelectRate/RoomFilter/index.tsx create mode 100644 components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css create mode 100644 types/components/hotelReservation/selectRate/roomFilter.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 2d80356ac..8ef2b4cd4 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -2,6 +2,7 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" +import RoomFilter from "@/components/HotelReservation/SelectRate/RoomFilter" import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" @@ -23,7 +24,7 @@ export default async function SelectRatePage({ const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms - const [hotelData, roomConfigurations, user] = await Promise.all([ + const [hotelData, roomsAvailability, user] = await Promise.all([ serverClient().hotel.hotelData.get({ hotelId: searchParams.hotel, language: params.lang, @@ -39,7 +40,7 @@ export default async function SelectRatePage({ getProfileSafely(), ]) - if (!roomConfigurations) { + if (!roomsAvailability) { return "No rooms found" // TODO: Add a proper error message } @@ -54,8 +55,11 @@ export default async function SelectRatePage({ <HotelInfoCard hotelData={hotelData} /> <div className={styles.content}> <div className={styles.main}> + <RoomFilter + numberOfRooms={roomsAvailability.roomConfigurations.length} + /> <RoomSelection - roomConfigurations={roomConfigurations} + roomsAvailability={roomsAvailability} roomCategories={roomCategories ?? []} user={user} /> diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx new file mode 100644 index 000000000..ff3edc852 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -0,0 +1,34 @@ +"use client" + +import { useIntl } from "react-intl" + +import Checkbox from "@/components/TempDesignSystem/Checkbox" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./roomFilter.module.css" + +import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter" + +function RoomFilter({ numberOfRooms }: RoomFilterProps) { + const intl = useIntl() + return ( + <div className={styles.container}> + <Body color="uiTextHighContrast"> + {numberOfRooms}{" "} + {intl.formatMessage( + { id: "Room types available" }, + { numberOfRooms: numberOfRooms } + )} + </Body> + <div className={styles.roomsFilter}> + <div> + <Checkbox name="accessibilty" /> + <Caption color="uiTextHighContrast">Accessibility room</Caption> + </div> + </div> + </div> + ) +} + +export default RoomFilter diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css new file mode 100644 index 000000000..06caf5149 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -0,0 +1,5 @@ +.container { + display: flex; + flex-direction: row; + justify-content: space-between; +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index c4c5e2e87..8592e64ea 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -12,7 +12,7 @@ import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRa import { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" export default function RoomSelection({ - roomConfigurations, + roomsAvailability, roomCategories, user, }: RoomSelectionProps) { @@ -54,10 +54,10 @@ export default function RoomSelection({ onSubmit={handleSubmit} > <ul className={styles.roomList}> - {roomConfigurations.roomConfigurations.map((roomConfiguration) => ( + {roomsAvailability.roomConfigurations.map((roomConfiguration) => ( <li key={roomConfiguration.roomType}> <RoomCard - rateDefinitions={roomConfigurations.rateDefinitions} + rateDefinitions={roomsAvailability.rateDefinitions} roomConfiguration={roomConfiguration} roomCategories={roomCategories} handleSelectRate={setRateSummary} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 889555466..b9c1622c4 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -241,9 +241,9 @@ "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Gentag den nye adgangskode", - "Room": "Værelse", "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", + "Room types available": "værelse {numberOfRooms, plural, one {# type} other {# types}} tilgængelig", "Rooms": "Værelser", "Rooms & Guests": "Værelser & gæster", "Sauna and gym": "Sauna and gym", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index e092862df..a4df8d378 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -244,6 +244,7 @@ "Room": "Zimmer", "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", + "Room types available": "zimmer {numberOfRooms, plural, one {# type} other {# types}} verfügbar", "Rooms": "Räume", "Rooms & Guests": "Zimmer & Gäste", "Sauna and gym": "Sauna and gym", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index f52e13d8e..cf5551d18 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -52,8 +52,6 @@ "Check in": "Check in", "Check out": "Check out", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.", - "Check-in": "Check-in", - "Check-out": "Check-out", "Child age is required": "Child age is required", "Children": "Children", "Choose room": "Choose room", @@ -256,9 +254,9 @@ "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Retype new password", - "Room": "Room", "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", + "Room types available": "room {numberOfRooms, plural, one {# type} other {# types}} available", "Rooms": "Rooms", "Rooms & Guests": "Rooms & Guests", "Sauna and gym": "Sauna and gym", @@ -396,6 +394,8 @@ "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", + "type": "type", + "types": "types", "uppercase letter": "uppercase letter", "{amount} out of {total}": "{amount} out of {total}", "{amount} {currency}": "{amount} {currency}", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index afe70ea1b..e8f430f16 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -241,9 +241,9 @@ "Restaurant & Bar": "Ravintola & Baari", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Kirjoita uusi salasana uudelleen", - "Room": "Huone", "Room & Terms": "Huone & Ehdot", "Room facilities": "Huoneen varustelu", + "Room types available": "huoneen {numberOfRooms, plural, one {# type} other {# types}} saatavilla", "Rooms": "Huoneet", "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", @@ -377,6 +377,8 @@ "to": "to", "uppercase letter": "iso kirjain", "{amount} out of {total}": "{amount}/{total}", + "type": "tyyppi", + "types": "tyypit", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index fab5ce670..0cdbf99e9 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -239,9 +239,9 @@ "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Skriv inn nytt passord på nytt", - "Room": "Rom", "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", + "Room types available": "romtyper {numberOfRooms, plural, one {# type} other {# types}} tilgjengelig", "Rooms": "Rom", "Rooms & Guests": "Rom og gjester", "Sauna and gym": "Sauna and gym", @@ -373,6 +373,8 @@ "to": "til", "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", + "type": "type", + "types": "typer", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index be22979a5..c88675fe9 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -239,9 +239,9 @@ "Restaurant & Bar": "Restaurang & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Upprepa nytt lösenord", - "Room": "Rum", "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", + "Room types available": "rumstyper {numberOfRooms, plural, one {# type} other {# types}} tillgängliga", "Rooms": "Rum", "Rooms & Guests": "Rum och gäster", "Sauna and gym": "Sauna and gym", @@ -374,6 +374,8 @@ "to": "till", "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", + "type": "typ", + "types": "typer", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts new file mode 100644 index 000000000..3ac0c6be5 --- /dev/null +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -0,0 +1,3 @@ +export interface RoomFilterProps { + numberOfRooms: number +} diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 9e944f5d8..fd633fa50 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -4,7 +4,7 @@ import { RoomData } from "@/types/hotel" import { SafeUser } from "@/types/user" export interface RoomSelectionProps { - roomConfigurations: RoomsAvailability + roomsAvailability: RoomsAvailability roomCategories: RoomData[] user: SafeUser } From fc63a8e5ca251add468f7118fb077ed321cadb93 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Thu, 24 Oct 2024 08:35:34 +0200 Subject: [PATCH 23/49] feat(SW-453) added filter options --- .../(standard)/select-rate/page.tsx | 4 +- .../SelectRate/RoomFilter/index.tsx | 87 ++++++++++++++++--- .../RoomFilter/roomFilter.module.css | 6 ++ .../Checkbox/checkbox.module.css | 40 --------- .../TempDesignSystem/Checkbox/checkbox.ts | 7 -- .../TempDesignSystem/Checkbox/index.tsx | 49 ----------- .../Form/Checkbox/checkbox.module.css | 2 +- i18n/dictionaries/da.json | 8 +- i18n/dictionaries/de.json | 8 +- i18n/dictionaries/en.json | 10 ++- i18n/dictionaries/fi.json | 8 +- i18n/dictionaries/no.json | 8 +- i18n/dictionaries/sv.json | 8 +- server/routers/hotels/query.ts | 1 + server/routers/hotels/schemas/room.ts | 6 ++ .../hotelReservation/selectRate/roomFilter.ts | 6 ++ 16 files changed, 138 insertions(+), 120 deletions(-) delete mode 100644 components/TempDesignSystem/Checkbox/checkbox.module.css delete mode 100644 components/TempDesignSystem/Checkbox/checkbox.ts delete mode 100644 components/TempDesignSystem/Checkbox/index.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 8ef2b4cd4..05103acf3 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -21,8 +21,8 @@ export default async function SelectRatePage({ const selectRoomParams = new URLSearchParams(searchParams) const selectRoomParamsObject = getHotelReservationQueryParams(selectRoomParams) - const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms + const adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms + const children = selectRoomParamsObject.room?.[0].child?.length // TODO: Handle multiple rooms const [hotelData, roomsAvailability, user] = await Promise.all([ serverClient().hotel.hotelData.get({ diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index ff3edc852..d775f8f46 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -1,32 +1,93 @@ "use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { useRef } from "react" +import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import Checkbox from "@/components/TempDesignSystem/Checkbox" +import { roomFilterSchema } from "@/server/routers/hotels/schemas/room" + +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import styles from "./roomFilter.module.css" -import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { + RoomFilterFormData, + RoomFilterProps, +} from "@/types/components/hotelReservation/selectRate/roomFilter" function RoomFilter({ numberOfRooms }: RoomFilterProps) { const intl = useIntl() + const methods = useForm<RoomFilterFormData>({ + defaultValues: { + allergyFriendly: false, + petFriendly: false, + accessibility: false, + }, + mode: "all", + reValidateMode: "onChange", + resolver: zodResolver(roomFilterSchema), + }) + + const formRef = useRef<HTMLFormElement | null>(null) + const { watch, setValue } = methods + const petFriendly = watch("petFriendly") + const allergyFriendly = watch("allergyFriendly") + + const onSubmit = (data: RoomFilterFormData) => { + if (data.petFriendly) { + setValue("allergyFriendly", false) + } else if (data.allergyFriendly) { + setValue("petFriendly", false) + } + console.log("Form submitted with data:", data) + } + return ( <div className={styles.container}> <Body color="uiTextHighContrast"> - {numberOfRooms}{" "} - {intl.formatMessage( - { id: "Room types available" }, - { numberOfRooms: numberOfRooms } - )} + {intl.formatMessage({ id: "Room types available" }, { numberOfRooms })} </Body> - <div className={styles.roomsFilter}> - <div> - <Checkbox name="accessibilty" /> - <Caption color="uiTextHighContrast">Accessibility room</Caption> - </div> - </div> + <FormProvider {...methods}> + <form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}> + <div className={styles.roomsFilter}> + <Checkbox + name="accessibility" + onChange={() => formRef.current?.requestSubmit()} + > + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: "Accessibility room" })} + </Caption> + </Checkbox> + <Checkbox + name="petFriendly" + onChange={() => { + setValue("petFriendly", !petFriendly) + formRef.current?.requestSubmit() + }} + registerOptions={{ disabled: allergyFriendly }} + > + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: "Pet room" })} + </Caption> + </Checkbox> + <Checkbox + name="allergyFriendly" + onChange={() => { + setValue("allergyFriendly", !allergyFriendly) + formRef.current?.requestSubmit() + }} + registerOptions={{ disabled: petFriendly }} + > + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: "Allergy room" })} + </Caption> + </Checkbox> + </div> + </form> + </FormProvider> </div> ) } diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css index 06caf5149..3c715f5c3 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -3,3 +3,9 @@ flex-direction: row; justify-content: space-between; } + +.roomsFilter { + display: flex; + flex-direction: row; + gap: var(--Spacing-x3); +} diff --git a/components/TempDesignSystem/Checkbox/checkbox.module.css b/components/TempDesignSystem/Checkbox/checkbox.module.css deleted file mode 100644 index c831ba525..000000000 --- a/components/TempDesignSystem/Checkbox/checkbox.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.container { - display: flex; - flex-direction: column; - color: var(--text-color); -} - -.container[data-selected] .checkbox { - border: none; - background: var(--UI-Input-Controls-Fill-Selected); -} - -.checkboxContainer { - display: flex; - align-items: flex-start; - gap: var(--Spacing-x-one-and-half); -} - -.checkbox { - width: 24px; - height: 24px; - min-width: 24px; - background-color: var(--UI-Input-Controls-Surface-Normal); - border: 2px solid var(--UI-Input-Controls-Border-Normal); - border-radius: var(--Corner-radius-Small); - transition: all 200ms; - display: flex; - align-items: center; - justify-content: center; - transition: all 200ms; - forced-color-adjust: none; - cursor: pointer; -} - -.error { - align-items: center; - color: var(--Scandic-Red-60); - display: flex; - gap: var(--Spacing-x-half); - margin-top: var(--Spacing-x1); -} diff --git a/components/TempDesignSystem/Checkbox/checkbox.ts b/components/TempDesignSystem/Checkbox/checkbox.ts deleted file mode 100644 index 8588b7401..000000000 --- a/components/TempDesignSystem/Checkbox/checkbox.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { RegisterOptions } from "react-hook-form" - -export interface CheckboxProps - extends React.InputHTMLAttributes<HTMLInputElement> { - name: string - registerOptions?: RegisterOptions -} diff --git a/components/TempDesignSystem/Checkbox/index.tsx b/components/TempDesignSystem/Checkbox/index.tsx deleted file mode 100644 index fde1742ff..000000000 --- a/components/TempDesignSystem/Checkbox/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Checkbox as AriaCheckbox } from "react-aria-components" -import { useController, useFormContext } from "react-hook-form" - -import { InfoCircleIcon } from "@/components/Icons" -import CheckIcon from "@/components/Icons/Check" -import Caption from "@/components/TempDesignSystem/Text/Caption" - -import { CheckboxProps } from "./checkbox" - -import styles from "./checkbox.module.css" - -export default function Checkbox({ - name, - children, - registerOptions, -}: React.PropsWithChildren<CheckboxProps>) { - const { control } = useFormContext() - const { field, fieldState } = useController({ - control, - name, - rules: registerOptions, - }) - - return ( - <AriaCheckbox - className={styles.container} - isSelected={field.value} - onChange={field.onChange} - data-testid={name} - > - {({ isSelected }) => ( - <> - <div className={styles.checkboxContainer}> - <div className={styles.checkbox}> - {isSelected && <CheckIcon color="white" />} - </div> - {children} - </div> - {children && fieldState.error ? ( - <Caption className={styles.error}> - <InfoCircleIcon color="red" /> - {fieldState.error.message} - </Caption> - ) : null} - </> - )} - </AriaCheckbox> - ) -} diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css index 99077e212..2e924b226 100644 --- a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css +++ b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css @@ -16,7 +16,7 @@ .checkboxContainer { display: flex; - align-items: flex-start; + align-items: center; gap: var(--Spacing-x-one-and-half); } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index b9c1622c4..ae660a5fe 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -5,13 +5,16 @@ "A photo of the room": "Et foto af værelset", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", + "Accessibility room": "Handicapvenligt værelse", "Activities": "Aktiviteter", "Add code": "Tilføj kode", "Add new card": "Tilføj nyt kort", + "Add room": "Tilføj værelse", "Address": "Adresse", "Adults": "voksne", "Airport": "Lufthavn", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.", + "Allergy room": "Allergivenligt værelse", "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", "Amusement park": "Forlystelsespark", @@ -214,6 +217,7 @@ "Pay later": "Betal senere", "Pay now": "Betal nu", "Payment info": "Betalingsoplysninger", + "Pet room": "Kæledyrsrum", "Phone": "Telefon", "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", @@ -243,7 +247,7 @@ "Retype new password": "Gentag den nye adgangskode", "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", - "Room types available": "værelse {numberOfRooms, plural, one {# type} other {# types}} tilgængelig", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} tilgængelig", "Rooms": "Værelser", "Rooms & Guests": "Værelser & gæster", "Sauna and gym": "Sauna and gym", @@ -376,6 +380,8 @@ "to": "til", "uppercase letter": "stort bogstav", "{amount} out of {total}": "{amount} ud af {total}", + "room type": "værelsestype", + "room types": "værelsestyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index a4df8d378..e69b84d40 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -5,13 +5,16 @@ "A photo of the room": "Ein Foto des Zimmers", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Über das Hotel", + "Accessibility room": "Barrierefreies Zimmer", "Activities": "Aktivitäten", "Add code": "Code hinzufügen", "Add new card": "Neue Karte hinzufügen", + "Add room": "Zimmer hinzufügen", "Address": "Adresse", "Adults": "Erwachsene", "Airport": "Flughafen", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.", + "Allergy room": "Allergiefreundliches Zimmer", "Already a friend?": "Sind wir schon Freunde?", "Amenities": "Annehmlichkeiten", "Amusement park": "Vergnügungspark", @@ -214,6 +217,7 @@ "Pay later": "Später bezahlen", "Pay now": "Jetzt bezahlen", "Payment info": "Zahlungsinformationen", + "Pet room": "Haustierfreundliches Zimmer", "Phone": "Telefon", "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", @@ -244,7 +248,7 @@ "Room": "Zimmer", "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", - "Room types available": "zimmer {numberOfRooms, plural, one {# type} other {# types}} verfügbar", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} verfügbar", "Rooms": "Räume", "Rooms & Guests": "Zimmer & Gäste", "Sauna and gym": "Sauna and gym", @@ -377,6 +381,8 @@ "to": "zu", "uppercase letter": "großbuchstabe", "{amount} out of {total}": "{amount} von {total}", + "room type": "zimmerart", + "room types": "zimmerarten", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index cf5551d18..59762c705 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -5,16 +5,19 @@ "A photo of the room": "A photo of the room", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", + "Accessibility room": "Accessibility room", "Activities": "Activities", "Add Room": "Add room", "Add code": "Add code", "Add new card": "Add new card", "Add to calendar": "Add to calendar", + "Add room": "Add room", "Address": "Address", "Adults": "Adults", "Age": "Age", "Airport": "Airport", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", + "Allergy room": "Allergy room", "Already a friend?": "Already a friend?", "Amenities": "Amenities", "Amusement park": "Amusement park", @@ -224,6 +227,7 @@ "Pay now": "Pay now", "Payment info": "Payment info", "Payment received": "Payment received", + "Pet room": "Pet room", "Phone": "Phone", "Phone is required": "Phone is required", "Phone number": "Phone number", @@ -256,7 +260,7 @@ "Retype new password": "Retype new password", "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", - "Room types available": "room {numberOfRooms, plural, one {# type} other {# types}} available", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available", "Rooms": "Rooms", "Rooms & Guests": "Rooms & Guests", "Sauna and gym": "Sauna and gym", @@ -391,11 +395,11 @@ "number": "number", "or": "or", "points": "Points", + "room type": "room type", + "room types": "room types", "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", - "type": "type", - "types": "types", "uppercase letter": "uppercase letter", "{amount} out of {total}": "{amount} out of {total}", "{amount} {currency}": "{amount} {currency}", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index e8f430f16..12fb36018 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -5,13 +5,16 @@ "A photo of the room": "Kuva huoneesta", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Tietoja hotellista", + "Accessibility room": "Esteettömyyshuone", "Activities": "Aktiviteetit", "Add code": "Lisää koodi", "Add new card": "Lisää uusi kortti", + "Add room": "Lisää huone", "Address": "Osoite", "Adults": "Aikuista", "Airport": "Lentokenttä", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.", + "Allergy room": "Allergiahuone", "Already a friend?": "Oletko jo ystävä?", "Amenities": "Mukavuudet", "Amusement park": "Huvipuisto", @@ -214,6 +217,7 @@ "Pay later": "Maksa myöhemmin", "Pay now": "Maksa nyt", "Payment info": "Maksutiedot", + "Pet room": "Lemmikkihuone", "Phone": "Puhelin", "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", @@ -243,7 +247,7 @@ "Retype new password": "Kirjoita uusi salasana uudelleen", "Room & Terms": "Huone & Ehdot", "Room facilities": "Huoneen varustelu", - "Room types available": "huoneen {numberOfRooms, plural, one {# type} other {# types}} saatavilla", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} saatavilla", "Rooms": "Huoneet", "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", @@ -379,6 +383,8 @@ "{amount} out of {total}": "{amount}/{total}", "type": "tyyppi", "types": "tyypit", + "room type": "huonetyyppi", + "room types": "huonetyypit", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 0cdbf99e9..fa02a3b96 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -5,13 +5,16 @@ "A photo of the room": "Et bilde av rommet", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accessibility room": "Tilgjengelighetsrom", "Activities": "Aktiviteter", "Add code": "Legg til kode", "Add new card": "Legg til nytt kort", + "Add room": "Legg til rom", "Address": "Adresse", "Adults": "Voksne", "Airport": "Flyplass", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.", + "Allergy room": "Allergihus", "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", "Amusement park": "Tivoli", @@ -212,6 +215,7 @@ "Pay later": "Betal senere", "Pay now": "Betal nå", "Payment info": "Betalingsinformasjon", + "Pet room": "Kjæledyrrom", "Phone": "Telefon", "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", @@ -241,7 +245,7 @@ "Retype new password": "Skriv inn nytt passord på nytt", "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", - "Room types available": "romtyper {numberOfRooms, plural, one {# type} other {# types}} tilgjengelig", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} tilgjengelig", "Rooms": "Rom", "Rooms & Guests": "Rom og gjester", "Sauna and gym": "Sauna and gym", @@ -375,6 +379,8 @@ "{amount} out of {total}": "{amount} av {total}", "type": "type", "types": "typer", + "room type": "romtype", + "room types": "romtyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index c88675fe9..5ee85ccde 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -5,13 +5,16 @@ "A photo of the room": "Ett foto av rummet", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accessibility room": "Tillgänglighetsrum", "Activities": "Aktiviteter", "Add code": "Lägg till kod", "Add new card": "Lägg till nytt kort", + "Add room": "Lägg till rum", "Address": "Adress", "Adults": "Vuxna", "Airport": "Flygplats", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.", + "Allergy room": "Allergirum", "Already a friend?": "Är du redan en vän?", "Amenities": "Bekvämligheter", "Amusement park": "Nöjespark", @@ -212,6 +215,7 @@ "Pay later": "Betala senare", "Pay now": "Betala nu", "Payment info": "Betalningsinformation", + "Pet room": "Husdjursrum", "Phone": "Telefon", "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", @@ -241,7 +245,7 @@ "Retype new password": "Upprepa nytt lösenord", "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", - "Room types available": "rumstyper {numberOfRooms, plural, one {# type} other {# types}} tillgängliga", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} tillgängliga", "Rooms": "Rum", "Rooms & Guests": "Rum och gäster", "Sauna and gym": "Sauna and gym", @@ -376,6 +380,8 @@ "{amount} out of {total}": "{amount} av {total}", "type": "typ", "types": "typer", + "room type": "rumtyp", + "room types": "rumstyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index b5b5ff34b..289429ca7 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -441,6 +441,7 @@ export const hotelQueryRouter = router({ }, params ) + if (!apiResponse.ok) { const text = await apiResponse.text() roomsAvailabilityFailCounter.add(1, { diff --git a/server/routers/hotels/schemas/room.ts b/server/routers/hotels/schemas/room.ts index 19f922db0..af3a3e8bc 100644 --- a/server/routers/hotels/schemas/room.ts +++ b/server/routers/hotels/schemas/room.ts @@ -92,3 +92,9 @@ export const roomSchema = z roomFacilities: data.attributes.roomFacilities, } }) + +export const roomFilterSchema = z.object({ + accessibility: z.boolean(), + petFriendly: z.boolean(), + allergyFriendly: z.boolean(), +}) diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index 3ac0c6be5..ac31cfdbd 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -1,3 +1,9 @@ +import { z } from "zod" + +import { roomFilterSchema } from "@/server/routers/hotels/schemas/room" + export interface RoomFilterProps { numberOfRooms: number } + +export interface RoomFilterFormData extends z.output<typeof roomFilterSchema> {} From 260c9096f60ce38ac63b81efee2a5ead3348571d Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Thu, 24 Oct 2024 09:26:50 +0200 Subject: [PATCH 24/49] feat(sw-453): fixed lang files --- i18n/dictionaries/da.json | 7 ++----- i18n/dictionaries/de.json | 7 ++----- i18n/dictionaries/en.json | 2 +- i18n/dictionaries/fi.json | 9 ++------- i18n/dictionaries/no.json | 9 ++------- 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index ae660a5fe..adf12745e 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -351,14 +351,12 @@ "Zoom in": "Zoom ind", "Zoom out": "Zoom ud", "as of today": "pr. dags dato", - "booking.accommodatesUpTo": "Plads til {nrOfGuests, plural, one {# person} other {op til # personer}}", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.children": "{totalChildren, plural, one {# barn} other {# børn}}", "booking.guests": "Maks {nrOfGuests, plural, one {# gæst} other {# gæster}}", "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", - "booking.thisRoomIsEquippedWith": "Dette værelse er udstyret med", "by": "inden", "characters": "tegn", "guest": "gæst", @@ -375,13 +373,12 @@ "number": "nummer", "or": "eller", "points": "Point", + "room type": "værelsestype", + "room types": "værelsestyper", "special character": "speciel karakter", "spendable points expiring by": "{points} Brugbare point udløber den {date}", "to": "til", "uppercase letter": "stort bogstav", - "{amount} out of {total}": "{amount} ud af {total}", - "room type": "værelsestype", - "room types": "værelsestyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index e69b84d40..e7c3ee819 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -352,14 +352,12 @@ "Zoom in": "Vergrößern", "Zoom out": "Verkleinern", "as of today": "Stand heute", - "booking.accommodatesUpTo": "Bietet Platz für {nrOfGuests, plural, one {# Person } other {bis zu # Personen}}", "booking.adults": "{totalAdults, plural, one {# erwachsene} other {# erwachsene}}", "booking.children": "{totalChildren, plural, one {# kind} other {# kinder}}", "booking.guests": "Max {nrOfGuests, plural, one {# gast} other {# gäste}}", "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", "booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", - "booking.thisRoomIsEquippedWith": "Dieses Zimmer ist ausgestattet mit", "by": "bis", "characters": "figuren", "guest": "gast", @@ -376,13 +374,12 @@ "number": "nummer", "or": "oder", "points": "Punkte", + "room type": "zimmerart", + "room types": "zimmerarten", "special character": "sonderzeichen", "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", "to": "zu", "uppercase letter": "großbuchstabe", - "{amount} out of {total}": "{amount} von {total}", - "room type": "zimmerart", - "room types": "zimmerarten", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 59762c705..cb2f91ecc 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -10,8 +10,8 @@ "Add Room": "Add room", "Add code": "Add code", "Add new card": "Add new card", - "Add to calendar": "Add to calendar", "Add room": "Add room", + "Add to calendar": "Add to calendar", "Address": "Address", "Adults": "Adults", "Age": "Age", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 12fb36018..9307bd321 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -352,14 +352,12 @@ "Zoom in": "Lähennä", "Zoom out": "Loitonna", "as of today": "tänään", - "booking.accommodatesUpTo": "Huoneeseen {nrOfGuests, plural, one {# person} other {mahtuu 2 henkilöä}}", "booking.adults": "{totalAdults, plural, one {# aikuinen} other {# aikuiset}}", "booking.children": "{totalChildren, plural, one {# lapsi} other {# lasta}}", "booking.guests": "Max {nrOfGuests, plural, one {# vieras} other {# vieraita}}", "booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}", "booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}", "booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", - "booking.thisRoomIsEquippedWith": "Tämä huone on varustettu", "by": "mennessä", "characters": "hahmoja", "guest": "Vieras", @@ -376,15 +374,12 @@ "number": "määrä", "or": "tai", "points": "pistettä", + "room type": "huonetyyppi", + "room types": "huonetyypit", "special character": "erikoishahmo", "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "to": "to", "uppercase letter": "iso kirjain", - "{amount} out of {total}": "{amount}/{total}", - "type": "tyyppi", - "types": "tyypit", - "room type": "huonetyyppi", - "room types": "huonetyypit", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index fa02a3b96..e7b95db27 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -349,13 +349,11 @@ "Zoom in": "Zoom inn", "Zoom out": "Zoom ut", "as of today": "per i dag", - "booking.accommodatesUpTo": "Plass til {nrOfGuests, plural, one {# person} other {opptil # personer}}", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.children": "{totalChildren, plural, one {# barn} other {# barn}}", "booking.guests": "Maks {nrOfGuests, plural, one {# gjest} other {# gjester}}", "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", - "booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med", "by": "innen", "characters": "tegn", "guest": "gjest", @@ -372,15 +370,12 @@ "number": "antall", "or": "eller", "points": "poeng", + "room type": "romtype", + "room types": "romtyper", "special character": "spesiell karakter", "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "to": "til", "uppercase letter": "stor bokstav", - "{amount} out of {total}": "{amount} av {total}", - "type": "type", - "types": "typer", - "room type": "romtype", - "room types": "romtyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" From 7b36139684d6ebc5978e3e3e87f334c4bfaad34e Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Fri, 25 Oct 2024 12:55:13 +0200 Subject: [PATCH 25/49] feat(sw-453): implemented filter from packages --- .../(standard)/select-rate/page.module.css | 24 ---- .../(standard)/select-rate/page.tsx | 42 ++++--- .../EnterDetails/Payment/index.tsx | 2 +- .../SelectRate/RoomFilter/index.tsx | 115 ++++++++---------- .../SelectRate/Rooms/index.tsx | 53 ++++++++ .../SelectRate/Rooms/rooms.module.css | 8 ++ i18n/dictionaries/da.json | 6 +- i18n/dictionaries/de.json | 6 +- i18n/dictionaries/en.json | 7 +- i18n/dictionaries/fi.json | 6 +- i18n/dictionaries/no.json | 6 +- i18n/dictionaries/sv.json | 6 +- lib/api/endpoints.ts | 1 + lib/api/index.ts | 2 +- server/routers/hotels/query.ts | 97 +++++++++++++++ server/routers/hotels/schemas/packages.ts | 59 +++++++++ server/routers/hotels/schemas/room.ts | 6 - server/tokenManager.ts | 2 +- .../hotelReservation/selectRate/room.ts | 6 + .../hotelReservation/selectRate/roomFilter.ts | 9 +- 20 files changed, 330 insertions(+), 133 deletions(-) delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css create mode 100644 components/HotelReservation/SelectRate/Rooms/index.tsx create mode 100644 components/HotelReservation/SelectRate/Rooms/rooms.module.css create mode 100644 server/routers/hotels/schemas/packages.ts create mode 100644 types/components/hotelReservation/selectRate/room.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css deleted file mode 100644 index 464c8ce65..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.page { - min-height: 100dvh; - padding-top: var(--Spacing-x6); - padding-left: var(--Spacing-x2); - padding-right: var(--Spacing-x2); - background-color: var(--Scandic-Brand-Warm-White); -} - -.content { - max-width: var(--max-width); - margin: 0 auto; - display: flex; - flex-direction: column; - gap: var(--Spacing-x7); - padding: var(--Spacing-x2); -} - -.main { - flex-grow: 1; -} - -.summary { - max-width: 340px; -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 05103acf3..d43330adb 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -1,14 +1,12 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" +import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" -import RoomFilter from "@/components/HotelReservation/SelectRate/RoomFilter" -import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" +import Rooms from "@/components/HotelReservation/SelectRate/Rooms" import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" -import styles from "./page.module.css" - import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" @@ -24,7 +22,7 @@ export default async function SelectRatePage({ const adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms const children = selectRoomParamsObject.room?.[0].child?.length // TODO: Handle multiple rooms - const [hotelData, roomsAvailability, user] = await Promise.all([ + const [hotelData, roomsAvailability, packages, user] = await Promise.all([ serverClient().hotel.hotelData.get({ hotelId: searchParams.hotel, language: params.lang, @@ -37,6 +35,18 @@ export default async function SelectRatePage({ adults, children, }), + serverClient().hotel.packages.get({ + hotelId: searchParams.hotel, + startDate: searchParams.fromDate, + endDate: searchParams.toDate, + adults: adults, + children: children, + packageCodes: [ + RoomPackageCode.ACCE, + RoomPackageCode.PETR, + RoomPackageCode.ALLG, + ], + }), getProfileSafely(), ]) @@ -51,20 +61,14 @@ export default async function SelectRatePage({ const roomCategories = hotelData?.included return ( - <div> + <> <HotelInfoCard hotelData={hotelData} /> - <div className={styles.content}> - <div className={styles.main}> - <RoomFilter - numberOfRooms={roomsAvailability.roomConfigurations.length} - /> - <RoomSelection - roomsAvailability={roomsAvailability} - roomCategories={roomCategories ?? []} - user={user} - /> - </div> - </div> - </div> + <Rooms + roomsAvailability={roomsAvailability} + roomCategories={roomCategories ?? []} + user={user} + packages={packages ?? []} + /> + </> ) } diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 0da2b79e2..ba126c1f2 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -22,7 +22,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details" import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" -import Checkbox from "@/components/TempDesignSystem/Checkbox" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index d775f8f46..3c3586994 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -1,49 +1,60 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useRef } from "react" +import { useCallback, useEffect, useMemo } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" +import { z } from "zod" -import { roomFilterSchema } from "@/server/routers/hotels/schemas/room" +import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages" +import Chip from "@/components/TempDesignSystem/Chip" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import styles from "./roomFilter.module.css" -import { - RoomFilterFormData, - RoomFilterProps, -} from "@/types/components/hotelReservation/selectRate/roomFilter" +import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter" + +export default function RoomFilter({ + numberOfRooms, + onFilter, + filterOptions, +}: RoomFilterProps) { + const initialFilterValues = useMemo( + () => + filterOptions.reduce( + (acc, option) => { + acc[option.code] = false + return acc + }, + {} as Record<string, boolean | undefined> + ), + [filterOptions] + ) -function RoomFilter({ numberOfRooms }: RoomFilterProps) { const intl = useIntl() - const methods = useForm<RoomFilterFormData>({ - defaultValues: { - allergyFriendly: false, - petFriendly: false, - accessibility: false, - }, + const methods = useForm<Record<string, boolean | undefined>>({ + defaultValues: initialFilterValues, mode: "all", reValidateMode: "onChange", - resolver: zodResolver(roomFilterSchema), + resolver: zodResolver(z.object({})), }) - const formRef = useRef<HTMLFormElement | null>(null) - const { watch, setValue } = methods - const petFriendly = watch("petFriendly") - const allergyFriendly = watch("allergyFriendly") + const { watch, getValues, handleSubmit } = methods + const petFriendly = watch(RoomPackageCode.PETR) + const allergyFriendly = watch(RoomPackageCode.ALLG) - const onSubmit = (data: RoomFilterFormData) => { - if (data.petFriendly) { - setValue("allergyFriendly", false) - } else if (data.allergyFriendly) { - setValue("petFriendly", false) - } - console.log("Form submitted with data:", data) - } + const submitFilter = useCallback(() => { + const data = getValues() + onFilter(data) + }, [onFilter, getValues]) + + useEffect(() => { + const subscription = watch(() => handleSubmit(submitFilter)()) + return () => subscription.unsubscribe() + }, [handleSubmit, watch, submitFilter]) return ( <div className={styles.container}> @@ -51,45 +62,27 @@ function RoomFilter({ numberOfRooms }: RoomFilterProps) { {intl.formatMessage({ id: "Room types available" }, { numberOfRooms })} </Body> <FormProvider {...methods}> - <form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}> + <form onSubmit={handleSubmit(submitFilter)}> <div className={styles.roomsFilter}> - <Checkbox - name="accessibility" - onChange={() => formRef.current?.requestSubmit()} - > - <Caption color="uiTextHighContrast"> - {intl.formatMessage({ id: "Accessibility room" })} - </Caption> - </Checkbox> - <Checkbox - name="petFriendly" - onChange={() => { - setValue("petFriendly", !petFriendly) - formRef.current?.requestSubmit() - }} - registerOptions={{ disabled: allergyFriendly }} - > - <Caption color="uiTextHighContrast"> - {intl.formatMessage({ id: "Pet room" })} - </Caption> - </Checkbox> - <Checkbox - name="allergyFriendly" - onChange={() => { - setValue("allergyFriendly", !allergyFriendly) - formRef.current?.requestSubmit() - }} - registerOptions={{ disabled: petFriendly }} - > - <Caption color="uiTextHighContrast"> - {intl.formatMessage({ id: "Allergy room" })} - </Caption> - </Checkbox> + {filterOptions.map((option) => ( + <Checkbox + name={option.code} + key={option.code} + registerOptions={{ + required: false, + disabled: + (option.code === RoomPackageCode.PETR && allergyFriendly) || + (option.code === RoomPackageCode.ALLG && petFriendly), + }} + > + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: option.description })} + </Caption> + </Checkbox> + ))} </div> </form> </FormProvider> </div> ) } - -export default RoomFilter diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx new file mode 100644 index 000000000..963f266d4 --- /dev/null +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -0,0 +1,53 @@ +"use client" + +import { useState } from "react" + +import { RoomsAvailability } from "@/server/routers/hotels/output" + +import RoomFilter from "../RoomFilter" +import RoomSelection from "../RoomSelection" + +import styles from "./rooms.module.css" + +import { RoomProps } from "@/types/components/hotelReservation/selectRate/room" +import { RoomPackageCodes } from "@/types/components/hotelReservation/selectRate/roomFilter" + +export default function Rooms({ + roomsAvailability, + roomCategories = [], + user, + packages, +}: RoomProps) { + const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability) + + function handleFilter(filter: Record<string, boolean | undefined>) { + const selectedCodes = Object.keys(filter).filter((key) => filter[key]) + + if (selectedCodes.length === 0) { + setRooms(roomsAvailability) + return + } + + const filteredRooms = roomsAvailability.roomConfigurations.filter((room) => + room.features.some((feature) => + selectedCodes.includes(feature.code as RoomPackageCodes) + ) + ) + setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) + } + + return ( + <div className={styles.content}> + <RoomFilter + numberOfRooms={rooms.roomConfigurations.length} + onFilter={handleFilter} + filterOptions={packages} + /> + <RoomSelection + roomsAvailability={rooms} + roomCategories={roomCategories} + user={user} + /> + </div> + ) +} diff --git a/components/HotelReservation/SelectRate/Rooms/rooms.module.css b/components/HotelReservation/SelectRate/Rooms/rooms.module.css new file mode 100644 index 000000000..a8e573530 --- /dev/null +++ b/components/HotelReservation/SelectRate/Rooms/rooms.module.css @@ -0,0 +1,8 @@ +.content { + max-width: var(--max-width); + margin: 0 auto; + display: flex; + flex-direction: column; + gap: var(--Spacing-x7); + padding: var(--Spacing-x2); +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index adf12745e..af4c667b8 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -5,7 +5,7 @@ "A photo of the room": "Et foto af værelset", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", - "Accessibility room": "Handicapvenligt værelse", + "Accessible Room": "Tilgængelighedsrum", "Activities": "Aktiviteter", "Add code": "Tilføj kode", "Add new card": "Tilføj nyt kort", @@ -14,7 +14,7 @@ "Adults": "voksne", "Airport": "Lufthavn", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.", - "Allergy room": "Allergivenligt værelse", + "Allergy Room": "Allergirum", "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", "Amusement park": "Forlystelsespark", @@ -217,7 +217,7 @@ "Pay later": "Betal senere", "Pay now": "Betal nu", "Payment info": "Betalingsoplysninger", - "Pet room": "Kæledyrsrum", + "Pet Room": "Kæledyrsrum", "Phone": "Telefon", "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index e7c3ee819..c9483e349 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -5,7 +5,7 @@ "A photo of the room": "Ein Foto des Zimmers", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Über das Hotel", - "Accessibility room": "Barrierefreies Zimmer", + "Accessible Room": "Barrierefreies Zimmer", "Activities": "Aktivitäten", "Add code": "Code hinzufügen", "Add new card": "Neue Karte hinzufügen", @@ -14,7 +14,7 @@ "Adults": "Erwachsene", "Airport": "Flughafen", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.", - "Allergy room": "Allergiefreundliches Zimmer", + "Allergy Room": "Allergikerzimmer", "Already a friend?": "Sind wir schon Freunde?", "Amenities": "Annehmlichkeiten", "Amusement park": "Vergnügungspark", @@ -217,7 +217,7 @@ "Pay later": "Später bezahlen", "Pay now": "Jetzt bezahlen", "Payment info": "Zahlungsinformationen", - "Pet room": "Haustierfreundliches Zimmer", + "Pet Room": "Haustierzimmer", "Phone": "Telefon", "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index cb2f91ecc..0511cf5a9 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -5,7 +5,7 @@ "A photo of the room": "A photo of the room", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", - "Accessibility room": "Accessibility room", + "Accessible Room": "Accessibility room", "Activities": "Activities", "Add Room": "Add room", "Add code": "Add code", @@ -17,7 +17,7 @@ "Age": "Age", "Airport": "Airport", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", - "Allergy room": "Allergy room", + "Allergy Room": "Allergy room", "Already a friend?": "Already a friend?", "Amenities": "Amenities", "Amusement park": "Amusement park", @@ -227,7 +227,7 @@ "Pay now": "Pay now", "Payment info": "Payment info", "Payment received": "Payment received", - "Pet room": "Pet room", + "Pet Room": "Pet room", "Phone": "Phone", "Phone is required": "Phone is required", "Phone number": "Phone number", @@ -258,6 +258,7 @@ "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Retype new password", + "Room": "Room", "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 9307bd321..c8427a306 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -5,7 +5,7 @@ "A photo of the room": "Kuva huoneesta", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Tietoja hotellista", - "Accessibility room": "Esteettömyyshuone", + "Accessible Room": "Esteetön huone", "Activities": "Aktiviteetit", "Add code": "Lisää koodi", "Add new card": "Lisää uusi kortti", @@ -14,7 +14,7 @@ "Adults": "Aikuista", "Airport": "Lentokenttä", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.", - "Allergy room": "Allergiahuone", + "Allergy Room": "Allergiahuone", "Already a friend?": "Oletko jo ystävä?", "Amenities": "Mukavuudet", "Amusement park": "Huvipuisto", @@ -217,7 +217,7 @@ "Pay later": "Maksa myöhemmin", "Pay now": "Maksa nyt", "Payment info": "Maksutiedot", - "Pet room": "Lemmikkihuone", + "Pet Room": "Lemmikkihuone", "Phone": "Puhelin", "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index e7b95db27..69a1fbec4 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -5,7 +5,7 @@ "A photo of the room": "Et bilde av rommet", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", - "Accessibility room": "Tilgjengelighetsrom", + "Accessible Room": "Tilgjengelighetsrom", "Activities": "Aktiviteter", "Add code": "Legg til kode", "Add new card": "Legg til nytt kort", @@ -14,7 +14,7 @@ "Adults": "Voksne", "Airport": "Flyplass", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.", - "Allergy room": "Allergihus", + "Allergy Room": "Allergirom", "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", "Amusement park": "Tivoli", @@ -215,7 +215,7 @@ "Pay later": "Betal senere", "Pay now": "Betal nå", "Payment info": "Betalingsinformasjon", - "Pet room": "Kjæledyrrom", + "Pet Room": "Kjæledyrsrom", "Phone": "Telefon", "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 5ee85ccde..c2f90d520 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -5,7 +5,7 @@ "A photo of the room": "Ett foto av rummet", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", - "Accessibility room": "Tillgänglighetsrum", + "Accessible Room": "Tillgänglighetsrum", "Activities": "Aktiviteter", "Add code": "Lägg till kod", "Add new card": "Lägg till nytt kort", @@ -14,7 +14,7 @@ "Adults": "Vuxna", "Airport": "Flygplats", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.", - "Allergy room": "Allergirum", + "Allergy Room": "Allergirum", "Already a friend?": "Är du redan en vän?", "Amenities": "Bekvämligheter", "Amusement park": "Nöjespark", @@ -215,7 +215,7 @@ "Pay later": "Betala senare", "Pay now": "Betala nu", "Payment info": "Betalningsinformation", - "Pet room": "Husdjursrum", + "Pet Room": "Husdjursrum", "Phone": "Telefon", "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 66bc36ec3..be1aee2fd 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -23,6 +23,7 @@ export namespace endpoints { rewards = `${profile}/reward`, tierRewards = `${profile}/TierRewards`, subscriberId = `${profile}/SubscriberId`, + packages = "package/v1/packages/hotel", } } diff --git a/lib/api/index.ts b/lib/api/index.ts index 9e32ac0cc..475e0da4e 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -37,7 +37,7 @@ export async function get( const searchParams = new URLSearchParams(params) if (searchParams.size) { searchParams.forEach((value, key) => { - url.searchParams.set(key, value) + url.searchParams.append(key, value) }) url.searchParams.sort() } diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 289429ca7..883b3a3ee 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -25,6 +25,10 @@ import { getHotelPageCounter, validateHotelPageRefs, } from "../contentstack/hotelPage/utils" +import { + getRoomPackagesInputSchema, + getRoomPackagesSchema, +} from "./schemas/packages" import { getHotelInputSchema, getHotelsAvailabilityInputSchema, @@ -57,6 +61,14 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get") const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success") const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail") +const getPackagesCounter = meter.createCounter("trpc.hotel.packages.get") +const getPackagesSuccessCounter = meter.createCounter( + "trpc.hotel.packages.get-success" +) +const getPackagesFailCounter = meter.createCounter( + "trpc.hotel.packages.get-fail" +) + const hotelsAvailabilityCounter = meter.createCounter( "trpc.hotel.availability.hotels" ) @@ -694,4 +706,89 @@ export const hotelQueryRouter = router({ return locations }), }), + packages: router({ + get: serviceProcedure + .input(getRoomPackagesInputSchema) + .query(async ({ input, ctx }) => { + const { hotelId, startDate, endDate, adults, children, packageCodes } = + input + + const searchParams = new URLSearchParams({ + startDate, + endDate, + adults: adults.toString(), + children: children.toString(), + }) + + packageCodes.forEach((code) => { + searchParams.append("packageCodes", code) + }) + + const params = searchParams.toString() + + getPackagesCounter.add(1, { + hotelId, + }) + console.info( + "api.hotels.packages start", + JSON.stringify({ query: { hotelId, params } }) + ) + + const apiResponse = await api.get( + `${api.endpoints.v1.packages}/${hotelId}`, + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + }, + params + ) + + if (!apiResponse.ok) { + getPackagesFailCounter.add(1, { + hotelId, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + }), + }) + console.error( + "api.hotels.packages error", + JSON.stringify({ query: { hotelId, params } }) + ) + throw serverErrorByStatus(apiResponse.status, apiResponse) + } + + const apiJson = await apiResponse.json() + const validatedPackagesData = getRoomPackagesSchema.safeParse(apiJson) + + if (!validatedPackagesData.success) { + getHotelFailCounter.add(1, { + hotelId, + error_type: "validation_error", + error: JSON.stringify(validatedPackagesData.error), + }) + + console.error( + "api.hotels.packages validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validatedPackagesData.error, + }) + ) + throw badRequestError() + } + + getPackagesSuccessCounter.add(1, { + hotelId, + }) + console.info( + "api.hotels.packages success", + JSON.stringify({ query: { hotelId, params: params } }) + ) + + return validatedPackagesData.data + }), + }), }) diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts new file mode 100644 index 000000000..d7ff6a4e1 --- /dev/null +++ b/server/routers/hotels/schemas/packages.ts @@ -0,0 +1,59 @@ +import { z } from "zod" + +export enum RoomPackageCode { + PETR = "PETR", + ALLG = "ALLG", + ACCE = "ACCE", +} + +export const getRoomPackagesInputSchema = z.object({ + hotelId: z.string(), + startDate: z.string(), + endDate: z.string(), + adults: z.number(), + children: z.number().optional().default(0), + packageCodes: z.array(z.string()).optional().default([]), +}) + +const packagesSchema = z.array( + z.object({ + code: z.enum([ + RoomPackageCode.PETR, + RoomPackageCode.ALLG, + RoomPackageCode.ACCE, + ]), + itemCode: z.string(), + description: z.string(), + currency: z.string(), + calculatedPrice: z.number(), + inventories: z.array( + z.object({ + date: z.string(), + total: z.number(), + available: z.number(), + }) + ), + }) +) + +export const getRoomPackagesSchema = z + .object({ + data: z.object({ + attributes: z.object({ + hotelId: z.number(), + packages: packagesSchema, + }), + relationships: z + .object({ + links: z.array( + z.object({ + url: z.string(), + type: z.string(), + }) + ), + }) + .optional(), + type: z.string(), + }), + }) + .transform((data) => data.data.attributes.packages) diff --git a/server/routers/hotels/schemas/room.ts b/server/routers/hotels/schemas/room.ts index af3a3e8bc..19f922db0 100644 --- a/server/routers/hotels/schemas/room.ts +++ b/server/routers/hotels/schemas/room.ts @@ -92,9 +92,3 @@ export const roomSchema = z roomFacilities: data.attributes.roomFacilities, } }) - -export const roomFilterSchema = z.object({ - accessibility: z.boolean(), - petFriendly: z.boolean(), - allergyFriendly: z.boolean(), -}) diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 24180d017..980ca071d 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -74,7 +74,7 @@ export async function getServiceToken() { if (env.HIDE_FOR_NEXT_RELEASE) { scopes = ["profile"] } else { - scopes = ["profile", "hotel", "booking"] + scopes = ["profile", "hotel", "booking", "package"] } const tag = generateServiceTokenTag(scopes) const getCachedJwt = unstable_cache( diff --git a/types/components/hotelReservation/selectRate/room.ts b/types/components/hotelReservation/selectRate/room.ts new file mode 100644 index 000000000..b84e5c667 --- /dev/null +++ b/types/components/hotelReservation/selectRate/room.ts @@ -0,0 +1,6 @@ +import { RoomPackageData } from "./roomFilter" +import { RoomSelectionProps } from "./roomSelection" + +export interface RoomProps extends RoomSelectionProps { + packages: RoomPackageData +} diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index ac31cfdbd..c70f8f1d0 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -1,9 +1,14 @@ import { z } from "zod" -import { roomFilterSchema } from "@/server/routers/hotels/schemas/room" +import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages" export interface RoomFilterProps { numberOfRooms: number + onFilter: (filter: Record<string, boolean | undefined>) => void + filterOptions: RoomPackageData } -export interface RoomFilterFormData extends z.output<typeof roomFilterSchema> {} +export interface RoomPackageData + extends z.output<typeof getRoomPackagesSchema> {} + +export type RoomPackageCodes = RoomPackageData[number]["code"] From 8da94fc25973fa516a5d1e25a80bd17ddecc7663 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 10:43:53 +0100 Subject: [PATCH 26/49] feat(SW-453): Fixed new filter buttons and updated price in summary --- .../(standard)/select-rate/page.tsx | 8 +-- .../SelectRate/RoomFilter/index.tsx | 49 +++++++++------- .../RoomFilter/roomFilter.module.css | 10 +++- .../RoomSelection/FlexibilityOption/index.tsx | 2 + .../RoomSelection/RateSummary/index.tsx | 23 ++++++++ .../RateSummary/rateSummary.module.css | 5 ++ .../RoomSelection/RoomCard/index.tsx | 43 +++++++++----- .../RoomCard/roomCard.module.css | 9 ++- .../SelectRate/RoomSelection/index.tsx | 2 + .../RoomSelection/roomSelection.module.css | 1 - .../SelectRate/Rooms/index.tsx | 5 +- .../SelectRate/Rooms/rooms.module.css | 2 +- .../HotelReservation/SelectRate/utils.ts | 19 +++++++ components/Icons/Allergy.tsx | 36 ++++++++++++ components/Icons/Pets.tsx | 2 +- components/Icons/Wheelchair.tsx | 40 +++++++++++++ components/Icons/icon.module.css | 5 ++ components/Icons/index.tsx | 2 + components/Icons/variants.ts | 1 + .../Form/FilterChip/Checkbox.tsx | 7 +++ .../Form/FilterChip/_Chip/chip.module.css | 27 +++++++++ .../Form/FilterChip/_Chip/index.tsx | 57 +++++++++++++++++++ .../Tooltip/tooltip.module.css | 9 ++- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + server/routers/hotels/output.ts | 12 +++- server/routers/hotels/schemas/packages.ts | 12 ++-- types/components/form/filterChip.ts | 16 ++++++ .../selectRate/flexibilityOption.ts | 1 + .../selectRate/rateSummary.ts | 2 + .../hotelReservation/selectRate/room.ts | 6 -- .../hotelReservation/selectRate/roomCard.ts | 1 + .../hotelReservation/selectRate/roomFilter.ts | 5 ++ .../selectRate/roomSelection.ts | 3 + .../hotelReservation/selectRate/selectRate.ts | 1 + 39 files changed, 367 insertions(+), 62 deletions(-) create mode 100644 components/HotelReservation/SelectRate/utils.ts create mode 100644 components/Icons/Allergy.tsx create mode 100644 components/Icons/Wheelchair.tsx create mode 100644 components/TempDesignSystem/Form/FilterChip/Checkbox.tsx create mode 100644 components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css create mode 100644 components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx create mode 100644 types/components/form/filterChip.ts delete mode 100644 types/components/hotelReservation/selectRate/room.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index d43330adb..603bbbf4a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -1,12 +1,12 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" -import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" import Rooms from "@/components/HotelReservation/SelectRate/Rooms" import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" @@ -42,9 +42,9 @@ export default async function SelectRatePage({ adults: adults, children: children, packageCodes: [ - RoomPackageCode.ACCE, - RoomPackageCode.PETR, - RoomPackageCode.ALLG, + RoomPackageCodeEnum.ACCE, + RoomPackageCodeEnum.PETR, + RoomPackageCodeEnum.ALLG, ], }), getProfileSafely(), diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index 3c3586994..836dd5b49 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -6,16 +6,19 @@ import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { z } from "zod" -import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages" - -import Chip from "@/components/TempDesignSystem/Chip" -import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import { InfoCircleIcon } from "@/components/Icons" +import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" +import { Tooltip } from "@/components/TempDesignSystem/Tooltip" + +import { getIconForFeatureCode } from "../utils" import styles from "./roomFilter.module.css" -import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { + RoomFilterProps, + RoomPackageCodeEnum, +} from "@/types/components/hotelReservation/selectRate/roomFilter" export default function RoomFilter({ numberOfRooms, @@ -43,8 +46,8 @@ export default function RoomFilter({ }) const { watch, getValues, handleSubmit } = methods - const petFriendly = watch(RoomPackageCode.PETR) - const allergyFriendly = watch(RoomPackageCode.ALLG) + const petFriendly = watch(RoomPackageCodeEnum.PETR) + const allergyFriendly = watch(RoomPackageCodeEnum.ALLG) const submitFilter = useCallback(() => { const data = getValues() @@ -65,21 +68,27 @@ export default function RoomFilter({ <form onSubmit={handleSubmit(submitFilter)}> <div className={styles.roomsFilter}> {filterOptions.map((option) => ( - <Checkbox + <CheckboxChip name={option.code} key={option.code} - registerOptions={{ - required: false, - disabled: - (option.code === RoomPackageCode.PETR && allergyFriendly) || - (option.code === RoomPackageCode.ALLG && petFriendly), - }} - > - <Caption color="uiTextHighContrast"> - {intl.formatMessage({ id: option.description })} - </Caption> - </Checkbox> + label={intl.formatMessage({ id: option.description })} + disabled={ + (option.code === RoomPackageCodeEnum.ALLG && petFriendly) || + (option.code === RoomPackageCodeEnum.PETR && allergyFriendly) + } + selected={getValues(option.code)} + Icon={getIconForFeatureCode(option.code)} + /> ))} + <Tooltip + text={intl.formatMessage({ + id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", + })} + position="bottom" + arrow="right" + > + <InfoCircleIcon className={styles.infoIcon} /> + </Tooltip> </div> </form> </FormProvider> diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css index 3c715f5c3..c0eff095a 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -2,10 +2,18 @@ display: flex; flex-direction: row; justify-content: space-between; + align-items: center; } .roomsFilter { display: flex; flex-direction: row; - gap: var(--Spacing-x3); + gap: var(--Spacing-x1); + align-items: center; +} + +.roomsFilter .infoIcon, +.roomsFilter .infoIcon path { + stroke: var(--UI-Text-Medium-contrast); + fill: transparent; } diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index a523305ae..a0a92bb66 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -19,6 +19,7 @@ export default function FlexibilityOption({ priceInformation, roomType, roomTypeCode, + features, handleSelectRate, }: FlexibilityOptionProps) { const [rootDiv, setRootDiv] = useState<Element | undefined>(undefined) @@ -52,6 +53,7 @@ export default function FlexibilityOption({ priceName: name, public: publicPrice, member: memberPrice, + features, } handleSelectRate(rate) } diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index b929bfe76..98915efa6 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -7,15 +7,28 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./rateSummary.module.css" import { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" export default function RateSummary({ rateSummary, isUserLoggedIn, + packages, }: RateSummaryProps) { const intl = useIntl() const priceToShow = isUserLoggedIn ? rateSummary.member : rateSummary.public + const isPetRoomSelect = rateSummary.features.some( + (feature) => feature.code === RoomPackageCodeEnum.PETR + ) + + const petRoomPackage = packages.find( + (pkg) => pkg.code === RoomPackageCodeEnum.PETR + ) + + const petRoomPrice = petRoomPackage ? petRoomPackage.calculatedPrice : null + const petRoomCurrency = petRoomPackage ? petRoomPackage.currency : null + return ( <div className={styles.summary}> <div className={styles.summaryText}> @@ -34,6 +47,16 @@ export default function RateSummary({ {priceToShow?.requestedPrice?.currency} </Body> </div> + {isPetRoomSelect && ( + <div className={styles.petInfo}> + <Body color="uiTextHighContrast" textTransform="bold"> + + {petRoomPrice} {petRoomCurrency} + </Body> + <Body color="uiTextMediumContrast"> + {intl.formatMessage({ id: "Pet charge" })} + </Body> + </div> + )} <Button type="submit" theme="base"> {intl.formatMessage({ id: "Continue" })} </Button> diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css index c8352efb1..07e9841b4 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css @@ -15,3 +15,8 @@ display: flex; gap: var(--Spacing-x4); } + +.petInfo { + border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); + padding-left: var(--Spacing-x2); +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 1afec6119..3b161d8c7 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -1,5 +1,6 @@ "use client" +import { createElement } from "react" import { useIntl } from "react-intl" import { RateDefinition } from "@/server/routers/hotels/output" @@ -12,6 +13,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import ImageGallery from "../../ImageGallery" import RoomSidePeek from "../RoomSidePeek" +import { getIconForFeatureCode } from "../../utils" import styles from "./roomCard.module.css" @@ -47,16 +49,16 @@ export default function RoomCard({ : undefined } - function getPriceForRate( + function getPriceInformationForRate( rate: typeof saveRate | typeof changeRate | typeof flexRate ) { return rateDefinitions.find((def) => def.rateCode === rate?.rateCode) ?.generalTerms } + const selectedRoom = roomCategories.find( (room) => room.name === roomConfiguration.roomType ) - const roomSize = selectedRoom?.roomSize const occupancy = selectedRoom?.occupancy.total const roomDescription = selectedRoom?.descriptions.short @@ -68,7 +70,6 @@ export default function RoomCard({ <div className={styles.cardBody}> <div className={styles.specification}> <Caption color="uiTextMediumContrast" className={styles.guests}> - {/*TODO: Handle pluralisation*/} {intl.formatMessage( { id: "booking.guests", @@ -105,44 +106,58 @@ export default function RoomCard({ value="non-refundable" paymentTerm={intl.formatMessage({ id: "Pay now" })} product={findProductForRate(saveRate)} - priceInformation={getPriceForRate(saveRate)} + priceInformation={getPriceInformationForRate(saveRate)} handleSelectRate={handleSelectRate} roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} + features={roomConfiguration.features} /> <FlexibilityOption name={intl.formatMessage({ id: "Free rebooking" })} value="free-rebooking" paymentTerm={intl.formatMessage({ id: "Pay now" })} product={findProductForRate(changeRate)} - priceInformation={getPriceForRate(changeRate)} + priceInformation={getPriceInformationForRate(changeRate)} handleSelectRate={handleSelectRate} roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} + features={roomConfiguration.features} /> <FlexibilityOption name={intl.formatMessage({ id: "Free cancellation" })} value="free-cancellation" paymentTerm={intl.formatMessage({ id: "Pay later" })} product={findProductForRate(flexRate)} - priceInformation={getPriceForRate(flexRate)} + priceInformation={getPriceInformationForRate(flexRate)} handleSelectRate={handleSelectRate} roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} + features={roomConfiguration.features} /> </div> </div> </div> {mainImage && ( <div className={styles.imageContainer}> - {roomConfiguration.roomsLeft < 5 && ( - <span className={styles.roomsLeft}> - <Footnote - color="burgundy" - textTransform="uppercase" - >{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote> - </span> - )} + <div className={styles.chipContainer}> + {roomConfiguration.roomsLeft < 5 && ( + <span className={styles.chip}> + <Footnote + color="burgundy" + textTransform="uppercase" + >{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote> + </span> + )} + {roomConfiguration.features.map((feature) => ( + <span className={styles.chip} key={feature.code}> + {createElement(getIconForFeatureCode(feature.code), { + width: 16, + height: 16, + color: "burgundy", + })} + </span> + ))} + </div> {/*NOTE: images from the test API are hosted on test3.scandichotels.com, which can't be accessed unless on Scandic's Wifi or using Citrix. */} {images && ( diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index ef5d9b8fc..537c1b30a 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -64,10 +64,17 @@ gap: var(--Spacing-x2); } -.roomsLeft { +.chipContainer { position: absolute; + z-index: 1; top: 12px; left: 12px; + 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); diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 8592e64ea..351efd5b3 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -15,6 +15,7 @@ export default function RoomSelection({ roomsAvailability, roomCategories, user, + packages, }: RoomSelectionProps) { const [rateSummary, setRateSummary] = useState<Rate | null>(null) @@ -69,6 +70,7 @@ export default function RoomSelection({ <RateSummary rateSummary={rateSummary} isUserLoggedIn={isUserLoggedIn} + packages={packages} /> )} </form> diff --git a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css index 66a27302e..1dab63afb 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css @@ -3,7 +3,6 @@ } .roomList { - margin-top: var(--Spacing-x4); list-style: none; display: grid; grid-template-columns: 1fr; diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 963f266d4..0cd98156e 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -9,15 +9,15 @@ import RoomSelection from "../RoomSelection" import styles from "./rooms.module.css" -import { RoomProps } from "@/types/components/hotelReservation/selectRate/room" import { RoomPackageCodes } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" export default function Rooms({ roomsAvailability, roomCategories = [], user, packages, -}: RoomProps) { +}: RoomSelectionProps) { const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability) function handleFilter(filter: Record<string, boolean | undefined>) { @@ -47,6 +47,7 @@ export default function Rooms({ roomsAvailability={rooms} roomCategories={roomCategories} user={user} + packages={packages} /> </div> ) diff --git a/components/HotelReservation/SelectRate/Rooms/rooms.module.css b/components/HotelReservation/SelectRate/Rooms/rooms.module.css index a8e573530..5e2bca00b 100644 --- a/components/HotelReservation/SelectRate/Rooms/rooms.module.css +++ b/components/HotelReservation/SelectRate/Rooms/rooms.module.css @@ -3,6 +3,6 @@ margin: 0 auto; display: flex; flex-direction: column; - gap: var(--Spacing-x7); + gap: var(--Spacing-x2); padding: var(--Spacing-x2); } diff --git a/components/HotelReservation/SelectRate/utils.ts b/components/HotelReservation/SelectRate/utils.ts new file mode 100644 index 000000000..6002b0705 --- /dev/null +++ b/components/HotelReservation/SelectRate/utils.ts @@ -0,0 +1,19 @@ +import { AllergyIcon,PetsIcon, WheelchairIcon } from "@/components/Icons" + +import { + RoomPackageCodeEnum, + RoomPackageCodes, +} from "@/types/components/hotelReservation/selectRate/roomFilter" + +export function getIconForFeatureCode(featureCode: RoomPackageCodes) { + switch (featureCode) { + case RoomPackageCodeEnum.ACCE: + return WheelchairIcon + case RoomPackageCodeEnum.ALLG: + return AllergyIcon + case RoomPackageCodeEnum.PETR: + return PetsIcon + default: + return PetsIcon + } +} diff --git a/components/Icons/Allergy.tsx b/components/Icons/Allergy.tsx new file mode 100644 index 000000000..0fe399445 --- /dev/null +++ b/components/Icons/Allergy.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function AllergyIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + <svg + className={classNames} + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <mask + id="mask0_7488_25219" + style={{ maskType: "alpha" }} + maskUnits="userSpaceOnUse" + x="0" + y="0" + width="20" + height="20" + > + <rect width="20" height="20" fill="#D9D9D9" /> + </mask> + <g mask="url(#mask0_7488_1017)"> + <path + d="M8.83333 18.2812C8.14543 18.2812 7.62276 18.0729 7.26533 17.6562C6.90789 17.2396 6.72569 16.7708 6.71875 16.25C6.71875 15.9028 6.79688 15.5608 6.95312 15.224C7.10938 14.8872 7.35069 14.6111 7.67708 14.3958C7.98887 14.193 8.24043 13.9322 8.43175 13.6135C8.62308 13.2948 8.75694 12.9549 8.83333 12.5938C8.73611 12.5451 8.6441 12.4983 8.55729 12.4531C8.47049 12.408 8.38542 12.3542 8.30208 12.2917L6.35904 12.988C6.11968 13.0724 5.89075 13.1424 5.67225 13.1979C5.45375 13.2535 5.22842 13.2812 4.99627 13.2812C4.13592 13.2812 3.37458 12.9036 2.71225 12.1482C2.04992 11.3928 1.71875 10.2879 1.71875 8.83333C1.71875 8.14583 1.92361 7.62153 2.33333 7.26042C2.74306 6.89931 3.20673 6.71875 3.72435 6.71875C4.07852 6.71875 4.42587 6.79691 4.76642 6.95323C5.10697 7.10956 5.38622 7.35085 5.60417 7.67708C5.80699 7.98887 6.06776 8.24043 6.3865 8.43175C6.70522 8.62308 7.04514 8.75694 7.40625 8.83333C7.45486 8.73611 7.50174 8.6441 7.54688 8.55729C7.59201 8.47049 7.64583 8.38542 7.70833 8.30208L7.01042 6.35417C6.92708 6.1235 6.85764 5.89963 6.80208 5.68254C6.74653 5.46545 6.71875 5.24835 6.71875 5.03125C6.73264 4.16319 7.11534 3.3941 7.86685 2.72396C8.61837 2.05382 9.71831 1.71875 11.1667 1.71875C11.8542 1.71875 12.3785 1.92521 12.7396 2.33812C13.1007 2.75104 13.2812 3.21472 13.2812 3.72917C13.2812 4.07639 13.2031 4.42188 13.0469 4.76562C12.8906 5.10938 12.6493 5.38889 12.3229 5.60417C12.0111 5.80699 11.7596 6.06776 11.5682 6.3865C11.3769 6.70522 11.2431 7.04514 11.1667 7.40625C11.2639 7.45486 11.3559 7.50174 11.4427 7.54688C11.5295 7.59201 11.6146 7.64583 11.6979 7.70833L13.641 6.99115C13.8803 6.90677 14.1058 6.84028 14.3175 6.79167C14.5292 6.74306 14.7511 6.71875 14.9832 6.71875C16.0894 6.71875 16.9156 7.17875 17.4619 8.09875C18.0081 9.01876 18.2812 10.0414 18.2812 11.1667C18.2812 11.8546 18.0642 12.3772 17.6302 12.7347C17.1962 13.0921 16.7153 13.2743 16.1875 13.2812C15.8542 13.2812 15.5269 13.2031 15.2057 13.0468C14.8845 12.8904 14.6145 12.6492 14.3958 12.3229C14.193 12.0111 13.9322 11.7596 13.6135 11.5682C13.2948 11.3769 12.9549 11.2431 12.5938 11.1667C12.5451 11.2639 12.4983 11.3559 12.4531 11.4427C12.408 11.5295 12.3542 11.6146 12.2917 11.6979L12.9896 13.6458C13.066 13.8611 13.1337 14.0747 13.1927 14.2865C13.2517 14.4983 13.2812 14.7083 13.2812 14.9167C13.2743 15.7917 12.8959 16.5712 12.1461 17.2552C11.3963 17.9392 10.2921 18.2812 8.83333 18.2812ZM9.9994 11.3021C10.3609 11.3021 10.6684 11.1755 10.9219 10.9225C11.1753 10.6694 11.3021 10.3621 11.3021 10.0006C11.3021 9.63909 11.1755 9.3316 10.9225 9.07812C10.6694 8.82465 10.3621 8.69792 10.0006 8.69792C9.63909 8.69792 9.3316 8.82445 9.07812 9.07752C8.82465 9.33058 8.69792 9.63787 8.69792 9.9994C8.69792 10.3609 8.82445 10.6684 9.07752 10.9219C9.33058 11.1753 9.63787 11.3021 9.9994 11.3021ZM9.01042 7.3125C9.10764 7.27778 9.20733 7.25 9.30948 7.22917C9.41165 7.20833 9.50988 7.19097 9.60417 7.17708C9.71528 6.59375 9.92708 6.04861 10.2396 5.54167C10.5521 5.03472 10.9618 4.61806 11.4688 4.29167C11.5521 4.22917 11.6146 4.15327 11.6562 4.06398C11.6979 3.9747 11.7188 3.8631 11.7188 3.72917C11.7188 3.60343 11.6727 3.49735 11.5807 3.41092C11.4887 3.32447 11.3507 3.28125 11.1667 3.28125C10.6287 3.28125 10.0202 3.39938 9.34129 3.63565C8.66237 3.87191 8.30903 4.3337 8.28125 5.02102C8.28125 5.1499 8.29861 5.2716 8.33333 5.38615C8.36806 5.5007 8.39931 5.6081 8.42708 5.70833L9.01042 7.3125ZM5 11.7188C5.19444 11.7188 5.43056 11.6701 5.70833 11.5729L7.3125 10.9896C7.27778 10.8924 7.25 10.7927 7.22917 10.6905C7.20833 10.5884 7.19097 10.4901 7.17708 10.3958C6.59375 10.2847 6.04861 10.0729 5.54167 9.76042C5.03472 9.44792 4.61806 9.03819 4.29167 8.53125C4.22917 8.44792 4.14757 8.38542 4.04688 8.34375C3.94618 8.30208 3.84028 8.28125 3.72917 8.28125C3.58772 8.28125 3.47771 8.32726 3.39913 8.41927C3.32054 8.51128 3.28125 8.64931 3.28125 8.83333C3.28125 9.59713 3.42708 10.269 3.71875 10.8489C4.01042 11.4288 4.4375 11.7188 5 11.7188ZM8.83333 16.7188C9.49321 16.7188 10.1425 16.5851 10.7814 16.3177C11.4202 16.0503 11.7326 15.5868 11.7188 14.9271C11.7188 14.8084 11.7031 14.6971 11.6719 14.5933C11.6406 14.4894 11.6076 14.3889 11.5729 14.2917L10.9896 12.6875C10.8924 12.7222 10.7927 12.75 10.6905 12.7708C10.5884 12.7917 10.4901 12.809 10.3958 12.8229C10.2847 13.4062 10.0729 13.9514 9.76042 14.4583C9.44792 14.9653 9.03819 15.3819 8.53125 15.7083C8.44792 15.7708 8.38368 15.8524 8.33854 15.9531C8.2934 16.0538 8.27431 16.1562 8.28125 16.2604C8.29703 16.3826 8.34438 16.4896 8.42329 16.5813C8.50221 16.6729 8.63889 16.7188 8.83333 16.7188ZM16.1833 11.7188C16.325 11.7188 16.4497 11.6804 16.5573 11.6037C16.6649 11.527 16.7188 11.3814 16.7188 11.1667C16.7188 10.6292 16.6046 10.0174 16.3762 9.33146C16.1478 8.64546 15.6838 8.29539 14.9844 8.28125C14.8559 8.28125 14.7326 8.29514 14.6146 8.32292C14.4965 8.35069 14.3877 8.37894 14.2881 8.40765L12.6875 9.01042C12.7222 9.10764 12.75 9.20733 12.7708 9.30948C12.7917 9.41164 12.809 9.50988 12.8229 9.60417C13.4062 9.71528 13.9514 9.92708 14.4583 10.2396C14.9653 10.5521 15.3819 10.9618 15.7083 11.4688C15.7569 11.5451 15.8247 11.6059 15.9115 11.651C15.9983 11.6962 16.0889 11.7188 16.1833 11.7188Z" + fill="#787472" + /> + </g> + </svg> + ) +} diff --git a/components/Icons/Pets.tsx b/components/Icons/Pets.tsx index 679a3afee..e5e475b2e 100644 --- a/components/Icons/Pets.tsx +++ b/components/Icons/Pets.tsx @@ -16,7 +16,7 @@ export default function PetsIcon({ className, color, ...props }: IconProps) { > <path d="M4.6251 12.05C3.95843 12.05 3.39176 11.8166 2.9251 11.35C2.45843 10.8833 2.2251 10.3166 2.2251 9.64995C2.2251 8.98328 2.45843 8.41662 2.9251 7.94995C3.39176 7.48328 3.95843 7.24995 4.6251 7.24995C5.29176 7.24995 5.85843 7.48328 6.3251 7.94995C6.79176 8.41662 7.0251 8.98328 7.0251 9.64995C7.0251 10.3166 6.79176 10.8833 6.3251 11.35C5.85843 11.8166 5.29176 12.05 4.6251 12.05ZM9.0501 8.12495C8.38343 8.12495 7.81676 7.89162 7.3501 7.42495C6.88343 6.95828 6.6501 6.39162 6.6501 5.72495C6.6501 5.05828 6.88343 4.49162 7.3501 4.02495C7.81676 3.55828 8.38343 3.32495 9.0501 3.32495C9.71676 3.32495 10.2834 3.55828 10.7501 4.02495C11.2168 4.49162 11.4501 5.05828 11.4501 5.72495C11.4501 6.39162 11.2168 6.95828 10.7501 7.42495C10.2834 7.89162 9.71676 8.12495 9.0501 8.12495ZM14.9751 8.12495C14.3084 8.12495 13.7418 7.89162 13.2751 7.42495C12.8084 6.95828 12.5751 6.39162 12.5751 5.72495C12.5751 5.05828 12.8084 4.49162 13.2751 4.02495C13.7418 3.55828 14.3084 3.32495 14.9751 3.32495C15.6418 3.32495 16.2084 3.55828 16.6751 4.02495C17.1418 4.49162 17.3751 5.05828 17.3751 5.72495C17.3751 6.39162 17.1418 6.95828 16.6751 7.42495C16.2084 7.89162 15.6418 8.12495 14.9751 8.12495ZM19.4001 12.05C18.7334 12.05 18.1668 11.8166 17.7001 11.35C17.2334 10.8833 17.0001 10.3166 17.0001 9.64995C17.0001 8.98328 17.2334 8.41662 17.7001 7.94995C18.1668 7.48328 18.7334 7.24995 19.4001 7.24995C20.0668 7.24995 20.6334 7.48328 21.1001 7.94995C21.5668 8.41662 21.8001 8.98328 21.8001 9.64995C21.8001 10.3166 21.5668 10.8833 21.1001 11.35C20.6334 11.8166 20.0668 12.05 19.4001 12.05ZM6.7559 21.925C6.0187 21.925 5.40426 21.6474 4.9126 21.0922C4.42093 20.537 4.1751 19.8813 4.1751 19.125C4.1751 18.2666 4.4626 17.5187 5.0376 16.8812C5.6126 16.2437 6.19176 15.615 6.7751 14.995C7.25843 14.4893 7.6751 13.9387 8.0251 13.3432C8.3751 12.7477 8.78343 12.1833 9.2501 11.65C9.60843 11.2416 10.0188 10.8979 10.4813 10.6187C10.9438 10.3395 11.4506 10.2 12.0015 10.2C12.5524 10.2 13.0628 10.3333 13.5327 10.6C14.0026 10.8666 14.4168 11.2083 14.7751 11.625C15.2418 12.15 15.6522 12.7125 16.0063 13.3125C16.3605 13.9125 16.7755 14.4753 17.2513 15.001C17.8255 15.6253 18.4022 16.2541 18.9813 16.8875C19.5605 17.5208 19.8501 18.2666 19.8501 19.125C19.8501 19.8813 19.6043 20.537 19.1126 21.0922C18.6209 21.6474 18.0073 21.925 17.2718 21.925C16.3892 21.925 15.5147 21.85 14.6484 21.7C13.7822 21.55 12.9077 21.475 12.0251 21.475C11.1334 21.475 10.2535 21.55 9.38522 21.7C8.51697 21.85 7.64053 21.925 6.7559 21.925Z" - fill="#4D001B" + fill="#787472" /> </svg> ) diff --git a/components/Icons/Wheelchair.tsx b/components/Icons/Wheelchair.tsx new file mode 100644 index 000000000..991951761 --- /dev/null +++ b/components/Icons/Wheelchair.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function WheelchairIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + <svg + className={classNames} + width="16" + height="16" + viewBox="0 0 16 16" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <mask + id="mask0_7488_25219" + style={{ maskType: "alpha" }} + maskUnits="userSpaceOnUse" + x="0" + y="0" + width="16" + height="16" + > + <rect width="16" height="16" fill="#D9D9D9" /> + </mask> + <g mask="url(#mask0_7488_25219)"> + <path + d="M5.41602 14.5C4.51046 14.5 3.7424 14.1847 3.11185 13.5541C2.48129 12.9236 2.16602 12.1555 2.16602 11.25C2.16602 10.3444 2.48129 9.57636 3.11185 8.94581C3.7424 8.31525 4.51324 7.99998 5.42435 7.99998V9.24998C4.86324 9.24998 4.38824 9.44442 3.99935 9.83331C3.61046 10.2222 3.41602 10.6944 3.41602 11.25C3.41602 11.8055 3.61046 12.2778 3.99935 12.6666C4.38824 13.0555 4.86046 13.25 5.41602 13.25C5.97157 13.25 6.44379 13.0541 6.83268 12.6625C7.22157 12.2708 7.41602 11.7916 7.41602 11.225H8.66602C8.66602 12.1416 8.35074 12.9166 7.72018 13.55C7.08963 14.1833 6.32157 14.5 5.41602 14.5ZM7.38268 10.6C6.89379 10.6 6.52018 10.3944 6.26185 9.98331C6.00352 9.5722 5.97157 9.14442 6.16602 8.69998L7.36602 6.03331H5.84935L5.64935 6.54998C5.59379 6.70553 5.49102 6.82081 5.34102 6.89581C5.19102 6.97081 5.03546 6.98053 4.87435 6.92498C4.70213 6.86942 4.57574 6.76109 4.49518 6.59998C4.41463 6.43886 4.40768 6.2722 4.47435 6.09998L4.68268 5.54998C4.77713 5.30553 4.92852 5.11664 5.13685 4.98331C5.34518 4.84998 5.5799 4.78331 5.84102 4.78331H9.21601C9.69379 4.78331 10.0618 4.97914 10.3202 5.37081C10.5785 5.76248 10.6105 6.17775 10.416 6.61664L9.31602 9.03331H11.2827C11.6438 9.03331 11.9507 9.1597 12.2035 9.41248C12.4563 9.66525 12.5827 9.9722 12.5827 10.3333V13.2083C12.5827 13.3805 12.5216 13.5278 12.3993 13.65C12.2771 13.7722 12.1299 13.8333 11.9577 13.8333C11.7855 13.8333 11.6382 13.7722 11.516 13.65C11.3938 13.5278 11.3327 13.3805 11.3327 13.2083V10.6H7.38268ZM10.5493 4.44998C10.1882 4.44998 9.88129 4.32359 9.62852 4.07081C9.37574 3.81803 9.24935 3.51109 9.24935 3.14998C9.24935 2.78886 9.37574 2.48192 9.62852 2.22914C9.88129 1.97636 10.1882 1.84998 10.5493 1.84998C10.9105 1.84998 11.2174 1.97636 11.4702 2.22914C11.723 2.48192 11.8493 2.78886 11.8493 3.14998C11.8493 3.51109 11.723 3.81803 11.4702 4.07081C11.2174 4.32359 10.9105 4.44998 10.5493 4.44998Z" + fill="#787472" + /> + </g> + </svg> + ) +} diff --git a/components/Icons/icon.module.css b/components/Icons/icon.module.css index ec5a15fd4..68ace50b3 100644 --- a/components/Icons/icon.module.css +++ b/components/Icons/icon.module.css @@ -76,3 +76,8 @@ .baseButtonTextOnFillNormal * { fill: var(--Base-Button-Text-On-Fill-Normal); } + +.disabled, +.disabled * { + fill: var(--Base-Text-Disabled); +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index fc7407ce8..ce23296fe 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -4,6 +4,7 @@ export { default as AccessibilityIcon } from "./Accessibility" export { default as AccountCircleIcon } from "./AccountCircle" export { default as AirIcon } from "./Air" export { default as AirplaneIcon } from "./Airplane" +export { default as AllergyIcon } from "./Allergy" export { default as ArrowRightIcon } from "./ArrowRight" export { default as BarIcon } from "./Bar" export { default as BathtubIcon } from "./Bathtub" @@ -111,6 +112,7 @@ export { default as TshirtIcon } from "./Tshirt" export { default as TshirtWashIcon } from "./TshirtWash" export { default as TvCastingIcon } from "./TvCasting" export { default as WarningTriangle } from "./WarningTriangle" +export { default as WheelchairIcon } from "./Wheelchair" export { default as WifiIcon } from "./Wifi" export { default as WindowCurtainsAltIcon } from "./WindowCurtainsAlt" export { default as WindowNotAvailableIcon } from "./WindowNotAvailable" diff --git a/components/Icons/variants.ts b/components/Icons/variants.ts index d319c466e..09d69364f 100644 --- a/components/Icons/variants.ts +++ b/components/Icons/variants.ts @@ -20,6 +20,7 @@ const config = { white: styles.white, uiTextHighContrast: styles.uiTextHighContrast, uiTextMediumContrast: styles.uiTextMediumContrast, + disabled: styles.disabled, }, }, defaultVariants: { diff --git a/components/TempDesignSystem/Form/FilterChip/Checkbox.tsx b/components/TempDesignSystem/Form/FilterChip/Checkbox.tsx new file mode 100644 index 000000000..c2697d560 --- /dev/null +++ b/components/TempDesignSystem/Form/FilterChip/Checkbox.tsx @@ -0,0 +1,7 @@ +import Chip from "./_Chip" + +import type { FilterChipCheckboxProps } from "@/types/components/form/filterChip" + +export default function CheckboxChip(props: FilterChipCheckboxProps) { + return <Chip {...props} type="checkbox" /> +} diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css new file mode 100644 index 000000000..19e1db812 --- /dev/null +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css @@ -0,0 +1,27 @@ +.label { + display: flex; + align-items: center; + gap: var(--Spacing-x-half); + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Small); + background-color: var(--Base-Surface-Secondary-light-Normal); + cursor: pointer; +} + +.label[data-selected="true"], +.label[data-selected="true"]:hover { + background-color: var(--Primary-Light-Surface-Normal); + border-color: var(--Base-Border-Hover); +} + +.label:hover { + background-color: var(--Base-Surface-Primary-light-Hover-alt); + border-color: var(--Base-Border-Subtle); +} + +.label[data-disabled="true"] { + background-color: var(--Base-Button-Primary-Fill-Disabled); + border-color: var(--Base-Button-Primary-Fill-Disabled); + cursor: not-allowed; +} diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx new file mode 100644 index 000000000..c88f17d29 --- /dev/null +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx @@ -0,0 +1,57 @@ +import { useMemo } from "react" +import { useFormContext } from "react-hook-form" + +import { HeartIcon } from "@/components/Icons" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./chip.module.css" + +import { FilterChipProps } from "@/types/components/form/filterChip" + +export default function FilterChip({ + Icon = HeartIcon, + iconHeight = 20, + iconWidth = 20, + id, + name, + label, + type, + value, + selected, + disabled, +}: FilterChipProps) { + const { register } = useFormContext() + + const color = useMemo(() => { + if (selected) return "burgundy" + if (disabled) return "disabled" + return "uiTextPlaceholder" + }, [selected, disabled]) + + return ( + <label + className={styles.label} + data-selected={selected} + data-disabled={disabled} + > + <Icon + className={styles.icon} + color={color} + height={iconHeight} + width={iconWidth} + /> + <Caption type="bold" color={color}> + {label} + </Caption> + <input + aria-hidden + id={id || name} + hidden + type={type} + value={value} + disabled={disabled} + {...register(name)} + /> + </label> + ) +} diff --git a/components/TempDesignSystem/Tooltip/tooltip.module.css b/components/TempDesignSystem/Tooltip/tooltip.module.css index 2a6b00b43..da8e50cbd 100644 --- a/components/TempDesignSystem/Tooltip/tooltip.module.css +++ b/components/TempDesignSystem/Tooltip/tooltip.module.css @@ -15,6 +15,7 @@ opacity: 0; transition: opacity 0.3s; max-width: 200px; + min-width: 150px; } .tooltipContainer:hover .tooltip { @@ -31,11 +32,15 @@ } .top { - bottom: 100%; + bottom: calc(100% + 8px); } .bottom { - top: 100%; + top: calc(100% + 8px); +} + +.bottom.arrowRight { + right: 0; } .tooltip::before { diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index af4c667b8..74d3e165b 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -218,6 +218,7 @@ "Pay now": "Betal nu", "Payment info": "Betalingsoplysninger", "Pet Room": "Kæledyrsrum", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kæledyrsrum har en ekstra gebyr på 20 EUR per ophold", "Phone": "Telefon", "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index c9483e349..adb01b099 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -218,6 +218,7 @@ "Pay now": "Jetzt bezahlen", "Payment info": "Zahlungsinformationen", "Pet Room": "Haustierzimmer", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Haustierzimmer haben einen zusätzlichen Preis von 20 EUR pro Aufenthalt", "Phone": "Telefon", "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 0511cf5a9..2e92e936a 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -228,6 +228,7 @@ "Payment info": "Payment info", "Payment received": "Payment received", "Pet Room": "Pet room", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Pet-friendly rooms have an additional fee of 20 EUR per stay", "Phone": "Phone", "Phone is required": "Phone is required", "Phone number": "Phone number", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index c8427a306..5f464d30f 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -218,6 +218,7 @@ "Pay now": "Maksa nyt", "Payment info": "Maksutiedot", "Pet Room": "Lemmikkihuone", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Lemmikkihuoneen lisäkustannus on 20 EUR per majoitus", "Phone": "Puhelin", "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 69a1fbec4..8ef615923 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -216,6 +216,7 @@ "Pay now": "Betal nå", "Payment info": "Betalingsinformasjon", "Pet Room": "Kjæledyrsrom", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kjæledyrsrom har en tilleggsavgift på 20 EUR per opphold", "Phone": "Telefon", "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index c2f90d520..df8d576c8 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -216,6 +216,7 @@ "Pay now": "Betala nu", "Payment info": "Betalningsinformation", "Pet Room": "Husdjursrum", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Husdjursrum har en extra avgift på 20 EUR per vistelse", "Phone": "Telefon", "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 0f545d673..91032ffdc 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -7,6 +7,7 @@ import { imageMetaDataSchema, imageSizesSchema } from "./schemas/image" import { roomSchema } from "./schemas/room" import { getPoiGroupByCategoryName } from "./utils" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { AlertTypeEnum } from "@/types/enums/alert" import { FacilityEnum } from "@/types/enums/facilities" import { PointOfInterestCategoryNameEnum } from "@/types/hotel" @@ -545,7 +546,16 @@ const roomConfigurationSchema = z.object({ roomTypeCode: z.string().optional(), roomType: z.string(), roomsLeft: z.number(), - features: z.array(z.object({ inventory: z.number(), code: z.string() })), + features: z.array( + z.object({ + inventory: z.number(), + code: z.enum([ + RoomPackageCodeEnum.PETR, + RoomPackageCodeEnum.ALLG, + RoomPackageCodeEnum.ACCE, + ]), + }) + ), products: z.array(productSchema), }) diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts index d7ff6a4e1..a239c6b50 100644 --- a/server/routers/hotels/schemas/packages.ts +++ b/server/routers/hotels/schemas/packages.ts @@ -1,10 +1,6 @@ import { z } from "zod" -export enum RoomPackageCode { - PETR = "PETR", - ALLG = "ALLG", - ACCE = "ACCE", -} +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" export const getRoomPackagesInputSchema = z.object({ hotelId: z.string(), @@ -18,9 +14,9 @@ export const getRoomPackagesInputSchema = z.object({ const packagesSchema = z.array( z.object({ code: z.enum([ - RoomPackageCode.PETR, - RoomPackageCode.ALLG, - RoomPackageCode.ACCE, + RoomPackageCodeEnum.PETR, + RoomPackageCodeEnum.ALLG, + RoomPackageCodeEnum.ACCE, ]), itemCode: z.string(), description: z.string(), diff --git a/types/components/form/filterChip.ts b/types/components/form/filterChip.ts new file mode 100644 index 000000000..3ff40673d --- /dev/null +++ b/types/components/form/filterChip.ts @@ -0,0 +1,16 @@ +type FilterChipType = "checkbox" | "radio" + +export interface FilterChipProps { + Icon?: React.ElementType + iconHeight?: number + iconWidth?: number + id?: string + label: string + name: string + type: FilterChipType + value?: string + selected?: boolean + disabled?: boolean +} + +export type FilterChipCheckboxProps = Omit<FilterChipProps, "type"> diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index 1835c3b65..1a432dc32 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -18,6 +18,7 @@ export type FlexibilityOptionProps = { priceInformation?: Array<string> roomType: RoomConfiguration["roomType"] roomTypeCode: RoomConfiguration["roomTypeCode"] + features: RoomConfiguration["features"] handleSelectRate: (rate: Rate) => void } diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index 672df21dd..c9685f180 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -1,6 +1,8 @@ +import { RoomPackageData } from "./roomFilter" import { Rate } from "./selectRate" export interface RateSummaryProps { rateSummary: Rate isUserLoggedIn: boolean + packages: RoomPackageData } diff --git a/types/components/hotelReservation/selectRate/room.ts b/types/components/hotelReservation/selectRate/room.ts deleted file mode 100644 index b84e5c667..000000000 --- a/types/components/hotelReservation/selectRate/room.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { RoomPackageData } from "./roomFilter" -import { RoomSelectionProps } from "./roomSelection" - -export interface RoomProps extends RoomSelectionProps { - packages: RoomPackageData -} diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index a6ed91ac1..99552916d 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -3,6 +3,7 @@ import { RoomConfiguration, } from "@/server/routers/hotels/output" +import { RoomPackageCodes } from "./roomFilter" import { Rate } from "./selectRate" import { RoomData } from "@/types/hotel" diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index c70f8f1d0..aaf807508 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -2,6 +2,11 @@ import { z } from "zod" import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages" +export enum RoomPackageCodeEnum { + PETR = "PETR", + ALLG = "ALLG", + ACCE = "ACCE", +} export interface RoomFilterProps { numberOfRooms: number onFilter: (filter: Record<string, boolean | undefined>) => void diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index fd633fa50..03e84245e 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,5 +1,7 @@ import { RoomsAvailability } from "@/server/routers/hotels/output" +import { RoomPackageData } from "./roomFilter" + import { RoomData } from "@/types/hotel" import { SafeUser } from "@/types/user" @@ -7,4 +9,5 @@ export interface RoomSelectionProps { roomsAvailability: RoomsAvailability roomCategories: RoomData[] user: SafeUser + packages: RoomPackageData } diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index da8792133..b22c010c0 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -26,4 +26,5 @@ export interface Rate { priceName: string public: Product["productType"]["public"] member: Product["productType"]["member"] + features: RoomConfiguration["features"] } From 20efae18f664a348571256c774b3f268230f8314 Mon Sep 17 00:00:00 2001 From: Christian Andolf <christian@weahead.se> Date: Mon, 28 Oct 2024 17:18:40 +0100 Subject: [PATCH 27/49] fix: remove props that could be set as invalid attributes on svg --- components/DatePicker/Screen/Desktop.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/DatePicker/Screen/Desktop.tsx b/components/DatePicker/Screen/Desktop.tsx index d5040a4ae..d3b30dbf8 100644 --- a/components/DatePicker/Screen/Desktop.tsx +++ b/components/DatePicker/Screen/Desktop.tsx @@ -69,7 +69,13 @@ export default function DatePickerDesktop({ weekStartsOn={1} components={{ Chevron(props) { - return <ChevronLeftIcon {...props} height={20} width={20} /> + return ( + <ChevronLeftIcon + className={props.className} + height={20} + width={20} + /> + ) }, Footer(props) { return ( From 917f44f3238082e07893222c00ec9ff17615515c Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 13:11:24 +0100 Subject: [PATCH 28/49] feat(sw-453): fixed mobile view and some improvements --- .../(standard)/select-rate/page.tsx | 2 + .../MobileToggleButton/index.tsx | 9 +- .../SelectRate/RoomFilter/index.tsx | 45 +++++++-- .../RoomFilter/roomFilter.module.css | 24 +++++ .../FlexibilityOption/PriceList/index.tsx | 6 ++ .../PriceList/priceList.module.css | 5 + .../RoomSelection/RateSummary/index.tsx | 65 +++++++++++-- .../RateSummary/rateSummary.module.css | 42 +++++++- .../RoomSelection/RoomCard/index.tsx | 95 ++++++++----------- .../SelectRate/RoomSelection/index.tsx | 26 +++-- .../SelectRate/Rooms/index.tsx | 30 +++--- .../Form/FilterChip/_Chip/chip.module.css | 10 ++ .../Form/FilterChip/_Chip/index.tsx | 2 +- i18n/dictionaries/da.json | 5 + i18n/dictionaries/de.json | 5 + i18n/dictionaries/en.json | 5 + i18n/dictionaries/fi.json | 5 + i18n/dictionaries/no.json | 5 + i18n/dictionaries/sv.json | 13 ++- .../selectRate/rateSummary.ts | 3 + 20 files changed, 299 insertions(+), 103 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 603bbbf4a..48a0b30b1 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -58,6 +58,8 @@ export default async function SelectRatePage({ return "No hotel data found" // TODO: Add a proper error message } + console.log(selectRoomParamsObject) + const roomCategories = hotelData?.included return ( diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index 3bc438c41..a58bdd1b2 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -68,7 +68,14 @@ export default function MobileToggleButton({ {`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage( { id: "booking.nights" }, { totalNights: nights } - )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.children" }, { totalChildren })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} + )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${ + totalChildren > 0 + ? intl.formatMessage( + { id: "booking.children" }, + { totalChildren } + ) + ", " + : "" + }${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} </Caption> </div> <div className={styles.icon}> diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index 836dd5b49..19fd4d8b7 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -9,6 +9,7 @@ import { z } from "zod" import { InfoCircleIcon } from "@/components/Icons" import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" import { Tooltip } from "@/components/TempDesignSystem/Tooltip" import { getIconForFeatureCode } from "../utils" @@ -49,6 +50,12 @@ export default function RoomFilter({ const petFriendly = watch(RoomPackageCodeEnum.PETR) const allergyFriendly = watch(RoomPackageCodeEnum.ALLG) + const selectedFilters = useMemo(() => getValues(), [getValues]) + + const tooltipText = intl.formatMessage({ + id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", + }) + const submitFilter = useCallback(() => { const data = getValues() onFilter(data) @@ -61,9 +68,33 @@ export default function RoomFilter({ return ( <div className={styles.container}> - <Body color="uiTextHighContrast"> - {intl.formatMessage({ id: "Room types available" }, { numberOfRooms })} - </Body> + <div className={styles.infoDesktop}> + <Body color="uiTextHighContrast"> + {intl.formatMessage( + { id: "Room types available" }, + { numberOfRooms } + )} + </Body> + </div> + <div className={styles.infoMobile}> + <div className={styles.filterInfo}> + <Caption type="label" color="burgundy" textTransform="uppercase"> + {intl.formatMessage({ id: "Filter" })} + </Caption> + <Caption type="label" color="burgundy"> + {Object.entries(selectedFilters) + .filter(([_, value]) => value) + .map(([key]) => intl.formatMessage({ id: key })) + .join(", ")} + </Caption> + </div> + <Caption color="uiTextHighContrast"> + {intl.formatMessage( + { id: "Room types available" }, + { numberOfRooms } + )} + </Caption> + </div> <FormProvider {...methods}> <form onSubmit={handleSubmit(submitFilter)}> <div className={styles.roomsFilter}> @@ -80,13 +111,7 @@ export default function RoomFilter({ Icon={getIconForFeatureCode(option.code)} /> ))} - <Tooltip - text={intl.formatMessage({ - id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", - })} - position="bottom" - arrow="right" - > + <Tooltip text={tooltipText} position="bottom" arrow="right"> <InfoCircleIcon className={styles.infoIcon} /> </Tooltip> </div> diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css index c0eff095a..9cce04e43 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -17,3 +17,27 @@ stroke: var(--UI-Text-Medium-contrast); fill: transparent; } +.filterInfo { + display: flex; + flex-direction: row; + gap: var(--Spacing-x-half); + align-items: flex-end; +} + +.infoDesktop { + display: none; +} + +.infoMobile { + display: block; +} + +@media (min-width: 768px) { + .infoDesktop { + display: block; + } + + .infoMobile { + display: none; + } +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx index 76f366adb..dc7ca20fc 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -40,6 +40,9 @@ export default function PriceList({ </Subtitle> <Body color="uiTextHighContrast" textTransform="bold"> {publicLocalPrice.currency} + <span className={styles.perNight}> + /{intl.formatMessage({ id: "night" })} + </span> </Body> </div> ) : ( @@ -64,6 +67,9 @@ export default function PriceList({ </Subtitle> <Body color="red" textTransform="bold"> {memberLocalPrice.currency} + <span className={styles.perNight}> + /{intl.formatMessage({ id: "night" })} + </span> </Body> </div> ) : ( diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css index 7320cf1be..4f3431525 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css @@ -12,3 +12,8 @@ display: flex; gap: var(--Spacing-x-half); } + +.perNight { + font-weight: 400; + font-size: var(--typography-Caption-Regular-fontSize); +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 98915efa6..3b9f6dbf6 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -2,6 +2,8 @@ import { useIntl } from "react-intl" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./rateSummary.module.css" @@ -13,12 +15,19 @@ export default function RateSummary({ rateSummary, isUserLoggedIn, packages, + roomsAvailability, }: RateSummaryProps) { const intl = useIntl() + const { + member, + public: publicRate, + features, + roomType, + priceName, + } = rateSummary + const priceToShow = isUserLoggedIn ? member : publicRate - const priceToShow = isUserLoggedIn ? rateSummary.member : rateSummary.public - - const isPetRoomSelect = rateSummary.features.some( + const isPetRoomSelect = features.some( (feature) => feature.code === RoomPackageCodeEnum.PETR ) @@ -26,17 +35,23 @@ export default function RateSummary({ (pkg) => pkg.code === RoomPackageCodeEnum.PETR ) - const petRoomPrice = petRoomPackage ? petRoomPackage.calculatedPrice : null - const petRoomCurrency = petRoomPackage ? petRoomPackage.currency : null + const petRoomPrice = petRoomPackage?.calculatedPrice ?? null + const petRoomCurrency = petRoomPackage?.currency ?? null + + const checkInDate = new Date(roomsAvailability.checkInDate) + const checkOutDate = new Date(roomsAvailability.checkOutDate) + const nights = Math.ceil( + (checkOutDate.getTime() - checkInDate.getTime()) / (1000 * 60 * 60 * 24) + ) return ( <div className={styles.summary}> <div className={styles.summaryText}> - <Subtitle color="uiTextHighContrast">{rateSummary.roomType}</Subtitle> - <Body color="uiTextMediumContrast">{rateSummary.priceName}</Body> + <Subtitle color="uiTextHighContrast">{roomType}</Subtitle> + <Body color="uiTextMediumContrast">{priceName}</Body> </div> <div className={styles.summaryPrice}> - <div className={styles.summaryPriceText}> + <div className={styles.summaryPriceTextDesktop}> <Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}> {priceToShow?.localPrice.pricePerStay}{" "} {priceToShow?.localPrice.currency} @@ -47,6 +62,38 @@ export default function RateSummary({ {priceToShow?.requestedPrice?.currency} </Body> </div> + <div className={styles.summaryPriceTextMobile}> + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: "Total price" })} + </Caption> + <Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}> + {priceToShow?.localPrice.pricePerStay}{" "} + {priceToShow?.localPrice.currency} + </Subtitle> + <Footnote + color="uiTextMediumContrast" + className={styles.summaryPriceTextMobile} + > + {intl.formatMessage( + { id: "booking.nights" }, + { totalNights: nights } + )} + ,{" "} + {intl.formatMessage( + { id: "booking.adults" }, + { totalAdults: roomsAvailability.occupancy?.adults } + )} + {roomsAvailability.occupancy?.children && ( + <> + ,{" "} + {intl.formatMessage( + { id: "booking.children" }, + { totalChildren: roomsAvailability.occupancy.children } + )} + </> + )} + </Footnote> + </div> {isPetRoomSelect && ( <div className={styles.petInfo}> <Body color="uiTextHighContrast" textTransform="bold"> @@ -57,7 +104,7 @@ export default function RateSummary({ </Body> </div> )} - <Button type="submit" theme="base"> + <Button type="submit" theme="base" className={styles.continueButton}> {intl.formatMessage({ id: "Continue" })} </Button> </div> diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css index 07e9841b4..5cb5a4229 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css @@ -5,7 +5,7 @@ left: 0; right: 0; background-color: var(--Base-Surface-Primary-light-Normal); - padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5); + padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x5); display: flex; justify-content: space-between; align-items: center; @@ -13,10 +13,50 @@ .summaryPrice { display: flex; + width: 100%; gap: var(--Spacing-x4); } .petInfo { border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); padding-left: var(--Spacing-x2); + display: none; +} + +.summaryText { + display: none; +} + +.summaryPriceTextDesktop { + display: none; +} + +.continueButton { + margin-left: auto; + height: fit-content; + width: 100%; +} + +.summaryPriceTextMobile { + white-space: nowrap; +} + +@media (min-width: 768px) { + .summary { + padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5); + } + .petInfo, + .summaryText, + .summaryPriceTextDesktop { + display: block; + } + .summaryPriceTextMobile { + display: none; + } + .summaryPrice { + width: auto; + } + .continueButton { + width: auto; + } } diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 3b161d8c7..d1ab92174 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -12,8 +12,8 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import ImageGallery from "../../ImageGallery" -import RoomSidePeek from "../RoomSidePeek" import { getIconForFeatureCode } from "../../utils" +import RoomSidePeek from "../RoomSidePeek" import styles from "./roomCard.module.css" @@ -26,17 +26,19 @@ export default function RoomCard({ handleSelectRate, }: RoomCardProps) { const intl = useIntl() - const saveRate = rateDefinitions.find( - // TODO: Update string when API has decided - (rate) => rate.cancellationRule === "NonCancellable" - ) - const changeRate = rateDefinitions.find( - // TODO: Update string when API has decided - (rate) => rate.cancellationRule === "Modifiable" - ) - const flexRate = rateDefinitions.find( - // TODO: Update string when API has decided - (rate) => rate.cancellationRule === "CancellableBefore6PM" + + // TODO: Update string when API has decided + const rateTypes = { + saveRate: "NonCancellable", + changeRate: "Modifiable", + flexRate: "CancellableBefore6PM", + } + + const rates = Object.fromEntries( + Object.entries(rateTypes).map(([key, rule]) => [ + key, + rateDefinitions.find((rate) => rate.cancellationRule === rule), + ]) ) function findProductForRate(rate: RateDefinition | undefined) { @@ -49,9 +51,7 @@ export default function RoomCard({ : undefined } - function getPriceInformationForRate( - rate: typeof saveRate | typeof changeRate | typeof flexRate - ) { + function getPriceInformationForRate(rate: RateDefinition | undefined) { return rateDefinitions.find((def) => def.rateCode === rate?.rateCode) ?.generalTerms } @@ -59,10 +59,7 @@ export default function RoomCard({ const selectedRoom = roomCategories.find( (room) => room.name === roomConfiguration.roomType ) - const roomSize = selectedRoom?.roomSize - const occupancy = selectedRoom?.occupancy.total - const roomDescription = selectedRoom?.descriptions.short - const images = selectedRoom?.images + const { roomSize, occupancy, descriptions, images } = selectedRoom || {} const mainImage = images?.[0] return ( @@ -74,7 +71,7 @@ export default function RoomCard({ { id: "booking.guests", }, - { nrOfGuests: occupancy } + { nrOfGuests: occupancy?.total } )} </Caption> <Caption color="uiTextMediumContrast"> @@ -93,7 +90,7 @@ export default function RoomCard({ <Subtitle className={styles.name} type="two"> {roomConfiguration.roomType} </Subtitle> - <Body>{roomDescription}</Body> + <Body>{descriptions?.short}</Body> </div> <Caption color="uiTextHighContrast"> {intl.formatMessage({ @@ -101,39 +98,29 @@ export default function RoomCard({ })} </Caption> <div className={styles.flexibilityOptions}> - <FlexibilityOption - name={intl.formatMessage({ id: "Non-refundable" })} - value="non-refundable" - paymentTerm={intl.formatMessage({ id: "Pay now" })} - product={findProductForRate(saveRate)} - priceInformation={getPriceInformationForRate(saveRate)} - handleSelectRate={handleSelectRate} - roomType={roomConfiguration.roomType} - roomTypeCode={roomConfiguration.roomTypeCode} - features={roomConfiguration.features} - /> - <FlexibilityOption - name={intl.formatMessage({ id: "Free rebooking" })} - value="free-rebooking" - paymentTerm={intl.formatMessage({ id: "Pay now" })} - product={findProductForRate(changeRate)} - priceInformation={getPriceInformationForRate(changeRate)} - handleSelectRate={handleSelectRate} - roomType={roomConfiguration.roomType} - roomTypeCode={roomConfiguration.roomTypeCode} - features={roomConfiguration.features} - /> - <FlexibilityOption - name={intl.formatMessage({ id: "Free cancellation" })} - value="free-cancellation" - paymentTerm={intl.formatMessage({ id: "Pay later" })} - product={findProductForRate(flexRate)} - priceInformation={getPriceInformationForRate(flexRate)} - handleSelectRate={handleSelectRate} - roomType={roomConfiguration.roomType} - roomTypeCode={roomConfiguration.roomTypeCode} - features={roomConfiguration.features} - /> + {Object.entries(rates).map(([key, rate]) => ( + <FlexibilityOption + key={key} + name={intl.formatMessage({ + id: + key === "flexRate" + ? "Free cancellation" + : key === "saveRate" + ? "Non-refundable" + : "Free rebooking", + })} + value={key.toLowerCase()} + paymentTerm={intl.formatMessage({ + id: key === "flexRate" ? "Pay later" : "Pay now", + })} + product={findProductForRate(rate)} + priceInformation={getPriceInformationForRate(rate)} + handleSelectRate={handleSelectRate} + roomType={roomConfiguration.roomType} + roomTypeCode={roomConfiguration.roomTypeCode} + features={roomConfiguration.features} + /> + ))} </div> </div> </div> diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 351efd5b3..3c0305190 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -1,6 +1,6 @@ "use client" import { useRouter, useSearchParams } from "next/navigation" -import { useState } from "react" +import { useMemo,useState } from "react" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" @@ -23,27 +23,32 @@ export default function RoomSelection({ const searchParams = useSearchParams() const isUserLoggedIn = !!user - function handleSubmit(e: React.FormEvent<HTMLFormElement>) { - e.preventDefault() - const searchParamsObject = getHotelReservationQueryParams(searchParams) + const { roomConfigurations, rateDefinitions } = roomsAvailability - const queryParams = new URLSearchParams(searchParams) + const queryParams = useMemo(() => { + const params = new URLSearchParams(searchParams) + const searchParamsObject = getHotelReservationQueryParams(searchParams) searchParamsObject.room.forEach((item, index) => { if (rateSummary?.roomTypeCode) { - queryParams.set(`room[${index}].roomtype`, rateSummary.roomTypeCode) + params.set(`room[${index}].roomtype`, rateSummary.roomTypeCode) } if (rateSummary?.public?.rateCode) { - queryParams.set(`room[${index}].ratecode`, rateSummary.public.rateCode) + params.set(`room[${index}].ratecode`, rateSummary.public.rateCode) } if (rateSummary?.member?.rateCode) { - queryParams.set( + params.set( `room[${index}].counterratecode`, rateSummary.member.rateCode ) } }) + return params + }, [searchParams, rateSummary]) + + function handleSubmit(e: React.FormEvent<HTMLFormElement>) { + e.preventDefault() router.push(`select-bed?${queryParams}`) } @@ -55,10 +60,10 @@ export default function RoomSelection({ onSubmit={handleSubmit} > <ul className={styles.roomList}> - {roomsAvailability.roomConfigurations.map((roomConfiguration) => ( + {roomConfigurations.map((roomConfiguration) => ( <li key={roomConfiguration.roomType}> <RoomCard - rateDefinitions={roomsAvailability.rateDefinitions} + rateDefinitions={rateDefinitions} roomConfiguration={roomConfiguration} roomCategories={roomCategories} handleSelectRate={setRateSummary} @@ -71,6 +76,7 @@ export default function RoomSelection({ rateSummary={rateSummary} isUserLoggedIn={isUserLoggedIn} packages={packages} + roomsAvailability={roomsAvailability} /> )} </form> diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 0cd98156e..74fe0e811 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useCallback,useState } from "react" import { RoomsAvailability } from "@/server/routers/hotels/output" @@ -20,21 +20,25 @@ export default function Rooms({ }: RoomSelectionProps) { const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability) - function handleFilter(filter: Record<string, boolean | undefined>) { - const selectedCodes = Object.keys(filter).filter((key) => filter[key]) + const handleFilter = useCallback( + (filter: Record<string, boolean | undefined>) => { + const selectedCodes = Object.keys(filter).filter((key) => filter[key]) - if (selectedCodes.length === 0) { - setRooms(roomsAvailability) - return - } + if (selectedCodes.length === 0) { + setRooms(roomsAvailability) + return + } - const filteredRooms = roomsAvailability.roomConfigurations.filter((room) => - room.features.some((feature) => - selectedCodes.includes(feature.code as RoomPackageCodes) + const filteredRooms = roomsAvailability.roomConfigurations.filter( + (room) => + room.features.some((feature) => + selectedCodes.includes(feature.code as RoomPackageCodes) + ) ) - ) - setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) - } + setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) + }, + [roomsAvailability] + ) return ( <div className={styles.content}> diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css index 19e1db812..44fa78a14 100644 --- a/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css @@ -25,3 +25,13 @@ border-color: var(--Base-Button-Primary-Fill-Disabled); cursor: not-allowed; } + +.caption { + display: none; +} + +@media (min-width: 768px) { + .caption { + display: block; + } +} diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx index c88f17d29..528469df1 100644 --- a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx @@ -40,7 +40,7 @@ export default function FilterChip({ height={iconHeight} width={iconWidth} /> - <Caption type="bold" color={color}> + <Caption type="bold" color={color} className={styles.caption}> {label} </Caption> <input diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 74d3e165b..625646fb8 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/nat pr. voksen", "A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.", "A photo of the room": "Et foto af værelset", + "ACCE": "Tilgængelighed", + "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", "Accessible Room": "Tilgængelighedsrum", @@ -107,6 +109,7 @@ "FAQ": "Ofte stillede spørgsmål", "Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.", "Fair": "Messe", + "Filter": "Filter", "Find booking": "Find booking", "Find hotels": "Find hotel", "First name": "Fornavn", @@ -211,6 +214,7 @@ "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", "Overview": "Oversigt", + "PETR": "Kæledyr", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", "Password": "Adgangskode", @@ -300,6 +304,7 @@ "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", "Total Points": "Samlet antal point", "Total incl VAT": "Inkl. moms", + "Total price": "Samlet pris", "Tourist": "Turist", "Transaction date": "Overførselsdato", "Transactions": "Transaktioner", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index adb01b099..0029444d2 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/Nacht pro Erwachsener", "A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.", "A photo of the room": "Ein Foto des Zimmers", + "ACCE": "Zugänglichkeit", + "ALLG": "Allergie", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Über das Hotel", "Accessible Room": "Barrierefreies Zimmer", @@ -107,6 +109,7 @@ "FAQ": "Häufig gestellte Fragen", "Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.", "Fair": "Messe", + "Filter": "Filter", "Find booking": "Buchung finden", "Find hotels": "Hotels finden", "First name": "Vorname", @@ -211,6 +214,7 @@ "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", "Overview": "Übersicht", + "PETR": "Haustier", "Parking": "Parken", "Parking / Garage": "Parken / Garage", "Password": "Passwort", @@ -301,6 +305,7 @@ "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", "Total Points": "Gesamtpunktzahl", "Total incl VAT": "Gesamt inkl. MwSt.", + "Total price": "Gesamtpreis", "Tourist": "Tourist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktionen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 2e92e936a..8aebd5004 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/night per adult", "A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.", "A photo of the room": "A photo of the room", + "ACCE": "Accessibility", + "ALLG": "Allergy", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", "Accessible Room": "Accessibility room", @@ -114,6 +116,7 @@ "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.", "Fair": "Fair", + "Filter": "Filter", "Find booking": "Find booking", "Find hotels": "Find hotels", "First name": "First name", @@ -220,6 +223,7 @@ "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", "Overview": "Overview", + "PETR": "Pet", "Parking": "Parking", "Parking / Garage": "Parking / Garage", "Password": "Password", @@ -315,6 +319,7 @@ "Total Points": "Total Points", "Total cost": "Total cost", "Total incl VAT": "Total incl VAT", + "Total price": "Total price", "Tourist": "Tourist", "Transaction date": "Transaction date", "Transactions": "Transactions", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 5f464d30f..bb417d8c8 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/yö per aikuinen", "A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.", "A photo of the room": "Kuva huoneesta", + "ACCE": "Saavutettavuus", + "ALLG": "Allergia", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Tietoja hotellista", "Accessible Room": "Esteetön huone", @@ -107,6 +109,7 @@ "FAQ": "Usein kysytyt kysymykset", "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Fair": "Messukeskus", + "Filter": "Suodatin", "Find booking": "Etsi varaus", "Find hotels": "Etsi hotelleja", "First name": "Etunimi", @@ -211,6 +214,7 @@ "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", "Overview": "Yleiskatsaus", + "PETR": "Lemmikki", "Parking": "Pysäköinti", "Parking / Garage": "Pysäköinti / Autotalli", "Password": "Salasana", @@ -301,6 +305,7 @@ "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", "Total Points": "Kokonaispisteet", "Total incl VAT": "Yhteensä sis. alv", + "Total price": "Kokonaishinta", "Tourist": "Turisti", "Transaction date": "Tapahtuman päivämäärä", "Transactions": "Tapahtumat", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 8ef615923..76d2745db 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per voksen", "A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.", "A photo of the room": "Et bilde av rommet", + "ACCE": "Tilgjengelighet", + "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", "Accessible Room": "Tilgjengelighetsrom", @@ -106,6 +108,7 @@ "FAQ": "Ofte stilte spørsmål", "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Fair": "Messe", + "Filter": "Filter", "Find booking": "Finn booking", "Find hotels": "Finn hotell", "First name": "Fornavn", @@ -209,6 +212,7 @@ "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", "Overview": "Oversikt", + "PETR": "Kjæledyr", "Parking": "Parkering", "Parking / Garage": "Parkering / Garasje", "Password": "Passord", @@ -298,6 +302,7 @@ "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", "Total Points": "Totale poeng", "Total incl VAT": "Sum inkl mva", + "Total price": "Totalpris", "Tourist": "Turist", "Transaction date": "Transaksjonsdato", "Transactions": "Transaksjoner", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index df8d576c8..dd1b443e2 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per vuxen", "A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.", "A photo of the room": "Ett foto av rummet", + "ACCE": "Tillgänglighet", + "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", "Accessible Room": "Tillgänglighetsrum", @@ -106,6 +108,7 @@ "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.", "Fair": "Mässa", + "Filter": "Filter", "Find booking": "Hitta bokning", "Find hotels": "Hitta hotell", "First name": "Förnamn", @@ -209,6 +212,7 @@ "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", "Overview": "Översikt", + "PETR": "Husdjur", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", "Password": "Lösenord", @@ -298,6 +302,7 @@ "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", "Total Points": "Poäng totalt", "Total incl VAT": "Totalt inkl moms", + "Total price": "Totalpris", "Tourist": "Turist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktioner", @@ -374,15 +379,15 @@ "number": "nummer", "or": "eller", "points": "poäng", + "room type": "rumtyp", + "room types": "rumstyper", "special character": "speciell karaktär", "spendable points expiring by": "{points} poäng förfaller {date}", "to": "till", - "uppercase letter": "stor bokstav", - "{amount} out of {total}": "{amount} av {total}", "type": "typ", "types": "typer", - "room type": "rumtyp", - "room types": "rumstyper", + "uppercase letter": "stor bokstav", + "{amount} out of {total}": "{amount} av {total}", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index c9685f180..73af97078 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -1,3 +1,5 @@ +import { RoomsAvailability } from "@/server/routers/hotels/output" + import { RoomPackageData } from "./roomFilter" import { Rate } from "./selectRate" @@ -5,4 +7,5 @@ export interface RateSummaryProps { rateSummary: Rate isUserLoggedIn: boolean packages: RoomPackageData + roomsAvailability: RoomsAvailability } From 3b2b39fdc6f0fe633d0d96a0376c3cbb0912b362 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 13:13:09 +0100 Subject: [PATCH 29/49] feat(sw-453): removed log --- .../(public)/hotelreservation/(standard)/select-rate/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 48a0b30b1..603bbbf4a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -58,8 +58,6 @@ export default async function SelectRatePage({ return "No hotel data found" // TODO: Add a proper error message } - console.log(selectRoomParamsObject) - const roomCategories = hotelData?.included return ( From c14b413a34eb9284079bbb3d79bd7bbddf18c0fa Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 13:58:37 +0100 Subject: [PATCH 30/49] feat(sw-453): fixed filter bug with key --- .../HotelReservation/SelectRate/RoomSelection/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 3c0305190..b6e686faa 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -1,6 +1,6 @@ "use client" import { useRouter, useSearchParams } from "next/navigation" -import { useMemo,useState } from "react" +import { useMemo, useState } from "react" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" @@ -61,7 +61,7 @@ export default function RoomSelection({ > <ul className={styles.roomList}> {roomConfigurations.map((roomConfiguration) => ( - <li key={roomConfiguration.roomType}> + <li key={roomConfiguration.roomTypeCode}> <RoomCard rateDefinitions={rateDefinitions} roomConfiguration={roomConfiguration} From 62cf5dab80dcb885baf639d6344bafcb25054cb5 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 17:04:24 +0100 Subject: [PATCH 31/49] feat(sw-453): fixed pr comments and default filter --- .../(standard)/select-rate/page.tsx | 18 +++++++++---- .../SelectRate/RoomFilter/index.tsx | 12 +++++---- .../RoomSelection/RateSummary/index.tsx | 13 +++++----- .../RoomSelection/RoomCard/index.tsx | 22 +++++++--------- .../SelectRate/RoomSelection/index.tsx | 4 +-- .../SelectRate/Rooms/index.tsx | 26 ++++++++++++------- .../HotelReservation/SelectRate/utils.ts | 10 +++---- i18n/dictionaries/da.json | 4 +++ i18n/dictionaries/de.json | 4 +++ i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 4 +++ i18n/dictionaries/no.json | 4 +++ i18n/dictionaries/sv.json | 1 + server/routers/hotels/output.ts | 6 ++--- server/routers/hotels/schemas/packages.ts | 6 ++--- .../hotelReservation/selectRate/roomCard.ts | 1 - .../hotelReservation/selectRate/roomFilter.ts | 6 ++--- 17 files changed, 87 insertions(+), 55 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 603bbbf4a..85c546997 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -1,3 +1,5 @@ +import { notFound } from "next/navigation" + import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" @@ -17,10 +19,16 @@ export default async function SelectRatePage({ setLang(params.lang) const selectRoomParams = new URLSearchParams(searchParams) + console.log(selectRoomParams) const selectRoomParamsObject = getHotelReservationQueryParams(selectRoomParams) - const adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms - const children = selectRoomParamsObject.room?.[0].child?.length // TODO: Handle multiple rooms + + if (!selectRoomParamsObject.room) { + return notFound() + } + + const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms + const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms const [hotelData, roomsAvailability, packages, user] = await Promise.all([ serverClient().hotel.hotelData.get({ @@ -42,9 +50,9 @@ export default async function SelectRatePage({ adults: adults, children: children, packageCodes: [ - RoomPackageCodeEnum.ACCE, - RoomPackageCodeEnum.PETR, - RoomPackageCodeEnum.ALLG, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, ], }), getProfileSafely(), diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index 19fd4d8b7..ab3bd5626 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -17,7 +17,7 @@ import { getIconForFeatureCode } from "../utils" import styles from "./roomFilter.module.css" import { - RoomFilterProps, + type RoomFilterProps, RoomPackageCodeEnum, } from "@/types/components/hotelReservation/selectRate/roomFilter" @@ -47,8 +47,8 @@ export default function RoomFilter({ }) const { watch, getValues, handleSubmit } = methods - const petFriendly = watch(RoomPackageCodeEnum.PETR) - const allergyFriendly = watch(RoomPackageCodeEnum.ALLG) + const petFriendly = watch(RoomPackageCodeEnum.PET_ROOM) + const allergyFriendly = watch(RoomPackageCodeEnum.ALLERGY_ROOM) const selectedFilters = useMemo(() => getValues(), [getValues]) @@ -104,8 +104,10 @@ export default function RoomFilter({ key={option.code} label={intl.formatMessage({ id: option.description })} disabled={ - (option.code === RoomPackageCodeEnum.ALLG && petFriendly) || - (option.code === RoomPackageCodeEnum.PETR && allergyFriendly) + (option.code === RoomPackageCodeEnum.ALLERGY_ROOM && + petFriendly) || + (option.code === RoomPackageCodeEnum.PET_ROOM && + allergyFriendly) } selected={getValues(option.code)} Icon={getIconForFeatureCode(option.code)} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 3b9f6dbf6..cc594aed1 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -1,3 +1,4 @@ +import { differenceInCalendarDays } from "date-fns" import { useIntl } from "react-intl" import Button from "@/components/TempDesignSystem/Button" @@ -27,12 +28,12 @@ export default function RateSummary({ } = rateSummary const priceToShow = isUserLoggedIn ? member : publicRate - const isPetRoomSelect = features.some( - (feature) => feature.code === RoomPackageCodeEnum.PETR + const isPetRoomSelected = features.some( + (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM ) const petRoomPackage = packages.find( - (pkg) => pkg.code === RoomPackageCodeEnum.PETR + (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM ) const petRoomPrice = petRoomPackage?.calculatedPrice ?? null @@ -40,9 +41,7 @@ export default function RateSummary({ const checkInDate = new Date(roomsAvailability.checkInDate) const checkOutDate = new Date(roomsAvailability.checkOutDate) - const nights = Math.ceil( - (checkOutDate.getTime() - checkInDate.getTime()) / (1000 * 60 * 60 * 24) - ) + const nights = differenceInCalendarDays(checkOutDate, checkInDate) return ( <div className={styles.summary}> @@ -94,7 +93,7 @@ export default function RateSummary({ )} </Footnote> </div> - {isPetRoomSelect && ( + {isPetRoomSelected && ( <div className={styles.petInfo}> <Body color="uiTextHighContrast" textTransform="bold"> + {petRoomPrice} {petRoomCurrency} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index d1ab92174..665d91b31 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -27,20 +27,18 @@ export default function RoomCard({ }: RoomCardProps) { const intl = useIntl() - // TODO: Update string when API has decided - const rateTypes = { - saveRate: "NonCancellable", - changeRate: "Modifiable", - flexRate: "CancellableBefore6PM", + const rates = { + saveRate: rateDefinitions.find( + (rate) => rate.cancellationRule === "NonCancellable" + ), + changeRate: rateDefinitions.find( + (rate) => rate.cancellationRule === "Modifiable" + ), + flexRate: rateDefinitions.find( + (rate) => rate.cancellationRule === "CancellableBefore6PM" + ), } - const rates = Object.fromEntries( - Object.entries(rateTypes).map(([key, rule]) => [ - key, - rateDefinitions.find((rate) => rate.cancellationRule === rule), - ]) - ) - function findProductForRate(rate: RateDefinition | undefined) { return rate ? roomConfiguration.products.find( diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index b6e686faa..9929a7451 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -8,8 +8,8 @@ import getHotelReservationQueryParams from "./utils" import styles from "./roomSelection.module.css" -import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" -import { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" +import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" export default function RoomSelection({ roomsAvailability, diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 74fe0e811..ea0901f44 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -1,6 +1,6 @@ "use client" -import { useCallback,useState } from "react" +import { useCallback, useState } from "react" import { RoomsAvailability } from "@/server/routers/hotels/output" @@ -9,8 +9,7 @@ import RoomSelection from "../RoomSelection" import styles from "./rooms.module.css" -import { RoomPackageCodes } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" +import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" export default function Rooms({ roomsAvailability, @@ -18,26 +17,35 @@ export default function Rooms({ user, packages, }: RoomSelectionProps) { - const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability) + const defaultRooms = roomsAvailability.roomConfigurations.filter( + (room) => room.features.length === 0 + ) + const [rooms, setRooms] = useState<RoomsAvailability>({ + ...roomsAvailability, + roomConfigurations: defaultRooms, + }) + + console.log(rooms) const handleFilter = useCallback( (filter: Record<string, boolean | undefined>) => { const selectedCodes = Object.keys(filter).filter((key) => filter[key]) if (selectedCodes.length === 0) { - setRooms(roomsAvailability) + setRooms({ + ...roomsAvailability, + roomConfigurations: defaultRooms, + }) return } const filteredRooms = roomsAvailability.roomConfigurations.filter( (room) => - room.features.some((feature) => - selectedCodes.includes(feature.code as RoomPackageCodes) - ) + room.features.some((feature) => selectedCodes.includes(feature.code)) ) setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) }, - [roomsAvailability] + [roomsAvailability, defaultRooms] ) return ( diff --git a/components/HotelReservation/SelectRate/utils.ts b/components/HotelReservation/SelectRate/utils.ts index 6002b0705..d91476bb5 100644 --- a/components/HotelReservation/SelectRate/utils.ts +++ b/components/HotelReservation/SelectRate/utils.ts @@ -1,17 +1,17 @@ -import { AllergyIcon,PetsIcon, WheelchairIcon } from "@/components/Icons" +import { AllergyIcon, PetsIcon, WheelchairIcon } from "@/components/Icons" import { RoomPackageCodeEnum, - RoomPackageCodes, + type RoomPackageCodes, } from "@/types/components/hotelReservation/selectRate/roomFilter" export function getIconForFeatureCode(featureCode: RoomPackageCodes) { switch (featureCode) { - case RoomPackageCodeEnum.ACCE: + case RoomPackageCodeEnum.ACCESSIBILITY_ROOM: return WheelchairIcon - case RoomPackageCodeEnum.ALLG: + case RoomPackageCodeEnum.ALLERGY_ROOM: return AllergyIcon - case RoomPackageCodeEnum.PETR: + case RoomPackageCodeEnum.PET_ROOM: return PetsIcon default: return PetsIcon diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 625646fb8..e47484466 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -7,6 +7,7 @@ "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", + "Accessibility": "Tilgængelighed", "Accessible Room": "Tilgængelighedsrum", "Activities": "Aktiviteter", "Add code": "Tilføj kode", @@ -357,12 +358,14 @@ "Zoom in": "Zoom ind", "Zoom out": "Zoom ud", "as of today": "pr. dags dato", + "booking.accommodatesUpTo": "Plads til {nrOfGuests, plural, one {# person} other {op til # personer}}", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.children": "{totalChildren, plural, one {# barn} other {# børn}}", "booking.guests": "Maks {nrOfGuests, plural, one {# gæst} other {# gæster}}", "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", + "booking.thisRoomIsEquippedWith": "Dette værelse er udstyret med", "by": "inden", "characters": "tegn", "guest": "gæst", @@ -385,6 +388,7 @@ "spendable points expiring by": "{points} Brugbare point udløber den {date}", "to": "til", "uppercase letter": "stort bogstav", + "{amount} out of {total}": "{amount} ud af {total}", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 0029444d2..88a5092e5 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -7,6 +7,7 @@ "ALLG": "Allergie", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Über das Hotel", + "Accessibility": "Zugänglichkeit", "Accessible Room": "Barrierefreies Zimmer", "Activities": "Aktivitäten", "Add code": "Code hinzufügen", @@ -358,12 +359,14 @@ "Zoom in": "Vergrößern", "Zoom out": "Verkleinern", "as of today": "Stand heute", + "booking.accommodatesUpTo": "Bietet Platz für {nrOfGuests, plural, one {# Person } other {bis zu # Personen}}", "booking.adults": "{totalAdults, plural, one {# erwachsene} other {# erwachsene}}", "booking.children": "{totalChildren, plural, one {# kind} other {# kinder}}", "booking.guests": "Max {nrOfGuests, plural, one {# gast} other {# gäste}}", "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", "booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", + "booking.thisRoomIsEquippedWith": "Dieses Zimmer ist ausgestattet mit", "by": "bis", "characters": "figuren", "guest": "gast", @@ -386,6 +389,7 @@ "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", "to": "zu", "uppercase letter": "großbuchstabe", + "{amount} out of {total}": "{amount} von {total}", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 8aebd5004..75fbbd805 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -7,6 +7,7 @@ "ALLG": "Allergy", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", + "Accessibility": "Accessibility", "Accessible Room": "Accessibility room", "Activities": "Activities", "Add Room": "Add room", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index bb417d8c8..6f2f28b63 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -7,6 +7,7 @@ "ALLG": "Allergia", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Tietoja hotellista", + "Accessibility": "Saavutettavuus", "Accessible Room": "Esteetön huone", "Activities": "Aktiviteetit", "Add code": "Lisää koodi", @@ -358,12 +359,14 @@ "Zoom in": "Lähennä", "Zoom out": "Loitonna", "as of today": "tänään", + "booking.accommodatesUpTo": "Huoneeseen {nrOfGuests, plural, one {# person} other {mahtuu 2 henkilöä}}", "booking.adults": "{totalAdults, plural, one {# aikuinen} other {# aikuiset}}", "booking.children": "{totalChildren, plural, one {# lapsi} other {# lasta}}", "booking.guests": "Max {nrOfGuests, plural, one {# vieras} other {# vieraita}}", "booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}", "booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}", "booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", + "booking.thisRoomIsEquippedWith": "Tämä huone on varustettu", "by": "mennessä", "characters": "hahmoja", "guest": "Vieras", @@ -386,6 +389,7 @@ "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "to": "to", "uppercase letter": "iso kirjain", + "{amount} out of {total}": "{amount}/{total}", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 76d2745db..0b45a4673 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -7,6 +7,7 @@ "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accessibility": "Tilgjengelighet", "Accessible Room": "Tilgjengelighetsrom", "Activities": "Aktiviteter", "Add code": "Legg til kode", @@ -355,11 +356,13 @@ "Zoom in": "Zoom inn", "Zoom out": "Zoom ut", "as of today": "per i dag", + "booking.accommodatesUpTo": "Plass til {nrOfGuests, plural, one {# person} other {opptil # personer}}", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.children": "{totalChildren, plural, one {# barn} other {# barn}}", "booking.guests": "Maks {nrOfGuests, plural, one {# gjest} other {# gjester}}", "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", + "booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med", "by": "innen", "characters": "tegn", "guest": "gjest", @@ -382,6 +385,7 @@ "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "to": "til", "uppercase letter": "stor bokstav", + "{amount} out of {total}": "{amount} av {total}", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index dd1b443e2..406aa4cb4 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -7,6 +7,7 @@ "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accessibility": "Tillgänglighet", "Accessible Room": "Tillgänglighetsrum", "Activities": "Aktiviteter", "Add code": "Lägg till kod", diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 91032ffdc..182575c2d 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -550,9 +550,9 @@ const roomConfigurationSchema = z.object({ z.object({ inventory: z.number(), code: z.enum([ - RoomPackageCodeEnum.PETR, - RoomPackageCodeEnum.ALLG, - RoomPackageCodeEnum.ACCE, + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, ]), }) ), diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts index a239c6b50..2f12f7255 100644 --- a/server/routers/hotels/schemas/packages.ts +++ b/server/routers/hotels/schemas/packages.ts @@ -14,9 +14,9 @@ export const getRoomPackagesInputSchema = z.object({ const packagesSchema = z.array( z.object({ code: z.enum([ - RoomPackageCodeEnum.PETR, - RoomPackageCodeEnum.ALLG, - RoomPackageCodeEnum.ACCE, + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, ]), itemCode: z.string(), description: z.string(), diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index 99552916d..a6ed91ac1 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -3,7 +3,6 @@ import { RoomConfiguration, } from "@/server/routers/hotels/output" -import { RoomPackageCodes } from "./roomFilter" import { Rate } from "./selectRate" import { RoomData } from "@/types/hotel" diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index aaf807508..d42669295 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -3,9 +3,9 @@ import { z } from "zod" import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages" export enum RoomPackageCodeEnum { - PETR = "PETR", - ALLG = "ALLG", - ACCE = "ACCE", + PET_ROOM = "PETR", + ALLERGY_ROOM = "ALLG", + ACCESSIBILITY_ROOM = "ACCE", } export interface RoomFilterProps { numberOfRooms: number From c679fa83e2ff71a0f7bdc61fca61eccc989b216c Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 17:06:42 +0100 Subject: [PATCH 32/49] feat(sw-453): removed log --- .../(public)/hotelreservation/(standard)/select-rate/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 85c546997..2da2d78a6 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -19,7 +19,6 @@ export default async function SelectRatePage({ setLang(params.lang) const selectRoomParams = new URLSearchParams(searchParams) - console.log(selectRoomParams) const selectRoomParamsObject = getHotelReservationQueryParams(selectRoomParams) From 153822b738520fff36d523355a71b29f69983707 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 17:07:27 +0100 Subject: [PATCH 33/49] feat(sw-453): removed log --- components/HotelReservation/SelectRate/Rooms/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index ea0901f44..923bfa35c 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -25,8 +25,6 @@ export default function Rooms({ roomConfigurations: defaultRooms, }) - console.log(rooms) - const handleFilter = useCallback( (filter: Record<string, boolean | undefined>) => { const selectedCodes = Object.keys(filter).filter((key) => filter[key]) From 83cd9cf5183f941ecb299da1bb745f3e5fcbcb88 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 17:22:21 +0100 Subject: [PATCH 34/49] feat(sw-453): import types --- .../hotelreservation/(standard)/select-rate/page.tsx | 2 +- .../SelectRate/RoomSelection/RateSummary/index.tsx | 2 +- .../HotelReservation/SelectRate/RoomSelection/utils.ts | 2 +- .../hotelReservation/selectRate/rateSummary.ts | 7 +++---- .../hotelReservation/selectRate/roomSelection.ts | 10 ++++------ 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 2da2d78a6..b361aaa10 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -9,7 +9,7 @@ import getHotelReservationQueryParams from "@/components/HotelReservation/Select import { setLang } from "@/i18n/serverContext" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" export default async function SelectRatePage({ diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index cc594aed1..b7ecc3c63 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -9,7 +9,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./rateSummary.module.css" -import { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" +import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" export default function RateSummary({ diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index 0b1ab884a..1ae94cc9c 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -1,6 +1,6 @@ import { getFormattedUrlQueryParams } from "@/utils/url" -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" function getHotelReservationQueryParams(searchParams: URLSearchParams) { return getFormattedUrlQueryParams(searchParams, { diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index 73af97078..f6c0f03b6 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -1,7 +1,6 @@ -import { RoomsAvailability } from "@/server/routers/hotels/output" - -import { RoomPackageData } from "./roomFilter" -import { Rate } from "./selectRate" +import type { RoomsAvailability } from "@/server/routers/hotels/output" +import type { RoomPackageData } from "./roomFilter" +import type { Rate } from "./selectRate" export interface RateSummaryProps { rateSummary: Rate diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 03e84245e..8d006779c 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,9 +1,7 @@ -import { RoomsAvailability } from "@/server/routers/hotels/output" - -import { RoomPackageData } from "./roomFilter" - -import { RoomData } from "@/types/hotel" -import { SafeUser } from "@/types/user" +import type { RoomData } from "@/types/hotel" +import type { SafeUser } from "@/types/user" +import type { RoomsAvailability } from "@/server/routers/hotels/output" +import type { RoomPackageData } from "./roomFilter" export interface RoomSelectionProps { roomsAvailability: RoomsAvailability From f35ccbd9978281785a7305c5e7dff189518c2f39 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 29 Oct 2024 08:34:52 +0100 Subject: [PATCH 35/49] feat(sw-453): correct filter --- components/HotelReservation/SelectRate/Rooms/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 923bfa35c..8f9030149 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -39,7 +39,9 @@ export default function Rooms({ const filteredRooms = roomsAvailability.roomConfigurations.filter( (room) => - room.features.some((feature) => selectedCodes.includes(feature.code)) + selectedCodes.every((selectedCode) => + room.features.some((feature) => feature.code === selectedCode) + ) ) setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) }, From 3705ff0120e7dfeb13c318f2ce8a4d8ae23f0b1a Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 29 Oct 2024 11:09:29 +0100 Subject: [PATCH 36/49] feat(sw-453): removed useMemo on getValues --- .gitignore | 3 +++ components/HotelReservation/SelectRate/RoomFilter/index.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4663988fb..8a6f4e73e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,8 @@ certificates #vscode .vscode/ +#cursor +.cursorrules + # localfile with all the CSS variables exported from design system variables.css \ No newline at end of file diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index ab3bd5626..eb7c442b3 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -50,7 +50,7 @@ export default function RoomFilter({ const petFriendly = watch(RoomPackageCodeEnum.PET_ROOM) const allergyFriendly = watch(RoomPackageCodeEnum.ALLERGY_ROOM) - const selectedFilters = useMemo(() => getValues(), [getValues]) + const selectedFilters = getValues() const tooltipText = intl.formatMessage({ id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", From 1e92348fb497150c8d355ddcd47a6b2591461f90 Mon Sep 17 00:00:00 2001 From: Michael Zetterberg <michael.zetterberg@scandichotels.com> Date: Tue, 22 Oct 2024 07:09:26 +0200 Subject: [PATCH 37/49] fix(SW-126): remove feature flag communications settings in profile --- app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx index 13512d701..73496fe4e 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx @@ -1,5 +1,3 @@ -import { env } from "@/env/server" - import Divider from "@/components/TempDesignSystem/Divider" import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout" @@ -17,7 +15,7 @@ export default function ProfileLayout({ {profile} <Divider color="burgundy" opacity={8} /> {creditCards} - {env.HIDE_FOR_NEXT_RELEASE ? null : communication} + {communication} </section> </main> ) From 62ddad5730827bb741685911c296abd1c205158f Mon Sep 17 00:00:00 2001 From: Michael Zetterberg <michael.zetterberg@scandichotels.com> Date: Tue, 29 Oct 2024 04:51:35 +0100 Subject: [PATCH 38/49] fix(SW-714): rendering of RTE nested links/references --- components/JsonToHtml/renderOptions.tsx | 41 ++++++++++++++++--------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/components/JsonToHtml/renderOptions.tsx b/components/JsonToHtml/renderOptions.tsx index 219981529..2ce1afbec 100644 --- a/components/JsonToHtml/renderOptions.tsx +++ b/components/JsonToHtml/renderOptions.tsx @@ -42,6 +42,17 @@ import type { import { RTEMarkType } from "@/types/transitionTypes/rte/node" import type { RenderOptions } from "@/types/transitionTypes/rte/option" +function noNestedLinksOrReferences(node: RTENode) { + if ("type" in node) { + if (node.type === RTETypeEnum.reference) { + return node.children + } else if (node.type === RTETypeEnum.a) { + return node.children + } + } + return node +} + function extractPossibleAttributes(attrs: Attributes | undefined) { if (!attrs) return {} const props: Record<string, any> = {} @@ -82,7 +93,13 @@ export const renderOptions: RenderOptions = { variant="underscored" color="burgundy" > - {next(node.children, embeds, fullRenderOptions)} + {next( + // Sometimes editors happen to nest a reference inside a link and vice versa. + // In that case use the outermost link, i.e. ignore nested links. + node.children.flatMap(noNestedLinksOrReferences), + embeds, + fullRenderOptions + )} </Link> ) } @@ -340,28 +357,22 @@ export const renderOptions: RenderOptions = { ) { // If entry is not an ImageContainer, it is a page and we return it as a link const props = extractPossibleAttributes(node.attrs) - let href = "" - if (entry?.node.__typename === ContentEnum.blocks.AccountPage) { - href = removeMultipleSlashes( - `/${entry.node.system.locale}${entry.node.url}` - ) - } else { - href = - entry.node?.web?.original_url || - removeMultipleSlashes( - `/${entry.node.system.locale}${entry.node.url}` - ) - } return ( <Link {...props} - href={href} + href={entry.node.url} key={node.uid} variant="underscored" color="burgundy" > - {next(node.children, embeds, fullRenderOptions)} + {next( + // Sometimes editors happen to nest a reference inside a link and vice versa. + // In that case use the outermost link, i.e. ignore nested links. + node.children.flatMap(noNestedLinksOrReferences), + embeds, + fullRenderOptions + )} </Link> ) } From fc8844eb96dc4c1392ecff32adeb55cec319950c Mon Sep 17 00:00:00 2001 From: Bianca Widstam <bianca.widstam@scandichotels.com> Date: Tue, 29 Oct 2024 13:54:12 +0000 Subject: [PATCH 39/49] feat/SW-689-image-gallery-sizes (pull request #781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feat/SW-689 image gallery sizes * feat(SW-689): initial gallery changes * feat(SW-689): remove console.log * feat(SW-689): remove unneccessary code * feat(SW-689): change sizes * feat(SW-689): change size * feat(SW-689): add design for ipad for fullview * feat(SW-689): fix import type * feat(SW-689): fix tripAdvisor placement * feat(SW-689): fix image gallery type * feat(SW-689): fix check gallery length Approved-by: Christian Andolf Approved-by: Matilda Landström --- .../HotelPage/PreviewImages/index.tsx | 6 +-- components/ContentType/HotelPage/index.tsx | 4 +- .../SelectRate/HotelInfoCard/index.tsx | 17 ++++----- .../SelectRate/ImageGallery/index.tsx | 9 +---- components/Lightbox/FullView.tsx | 10 +++-- components/Lightbox/Gallery.tsx | 38 ++++++++++--------- components/Lightbox/Lightbox.module.css | 19 ++++++++++ components/Lightbox/index.tsx | 2 +- server/routers/hotels/output.ts | 26 ++++--------- server/routers/hotels/query.ts | 3 +- server/routers/utils/hotels.ts | 30 --------------- types/components/hotelPage/previewImages.ts | 4 +- .../selectRate/imageGallery.ts | 4 +- types/components/lightbox/lightbox.ts | 16 +++----- types/hotel.ts | 5 +-- 15 files changed, 82 insertions(+), 111 deletions(-) delete mode 100644 server/routers/utils/hotels.ts diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 99704a61b..2a0074c5c 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -21,9 +21,9 @@ export default async function PreviewImages({ {images.slice(0, 3).map((image, index) => ( <Image key={index} - src={image.url} - alt={image.alt} - title={image.title} + src={image.imageSizes.medium} + alt={image.metaData.altText} + title={image.metaData.title} width={index === 0 ? 752 : 292} height={index === 0 ? 540 : 266} className={styles.image} diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index d10244040..f5718a7f2 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -63,7 +63,9 @@ export default async function HotelPage() { return ( <div className={styles.pageContainer}> <div className={styles.hotelImages}> - <PreviewImages images={hotelImages} hotelName={hotelName} /> + {hotelImages?.length && ( + <PreviewImages images={hotelImages} hotelName={hotelName} /> + )} </div> <TabNavigation restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)} diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 120c0385d..b48220795 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -3,7 +3,6 @@ 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" @@ -29,14 +28,6 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { {hotelAttributes && ( <section className={styles.wrapper}> <div className={styles.imageWrapper}> - {hotelAttributes.ratings?.tripAdvisor && ( - <div className={styles.tripAdvisor}> - <TripAdvisorIcon color="burgundy" /> - <Caption color="burgundy"> - {hotelAttributes.ratings.tripAdvisor.rating} - </Caption> - </div> - )} {hotelAttributes.gallery && ( <ImageGallery title={hotelAttributes.name} @@ -46,6 +37,14 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { ]} /> )} + {hotelAttributes.ratings?.tripAdvisor && ( + <div className={styles.tripAdvisor}> + <TripAdvisorIcon color="burgundy" /> + <Caption color="burgundy"> + {hotelAttributes.ratings.tripAdvisor.rating} + </Caption> + </div> + )} </div> <div className={styles.hotelContent}> <div className={styles.hotelInformation}> diff --git a/components/HotelReservation/SelectRate/ImageGallery/index.tsx b/components/HotelReservation/SelectRate/ImageGallery/index.tsx index 1d4500cdb..4ff21af9f 100644 --- a/components/HotelReservation/SelectRate/ImageGallery/index.tsx +++ b/components/HotelReservation/SelectRate/ImageGallery/index.tsx @@ -9,14 +9,7 @@ import type { ImageGalleryProps } from "@/types/components/hotelReservation/sele export default function ImageGallery({ images, title }: ImageGalleryProps) { return ( - <Lightbox - images={images.map((image) => ({ - url: image.imageSizes.small, - alt: image.metaData.altText, - title: image.metaData.title, - }))} - dialogTitle={title} - > + <Lightbox images={images} dialogTitle={title}> <div className={styles.triggerArea} id="lightboxTrigger"> <Image src={images[0].imageSizes.medium} diff --git a/components/Lightbox/FullView.tsx b/components/Lightbox/FullView.tsx index 5541d365b..360b7a276 100644 --- a/components/Lightbox/FullView.tsx +++ b/components/Lightbox/FullView.tsx @@ -40,7 +40,7 @@ export default function FullView({ <div className={styles.fullViewImageContainer}> <AnimatePresence initial={false} custom={currentIndex}> <motion.div - key={image.url} + key={image.imageSizes.medium} custom={currentIndex} initial={{ opacity: 0, x: 300 }} animate={{ opacity: 1, x: 0 }} @@ -49,14 +49,16 @@ export default function FullView({ className={styles.fullViewImage} > <Image - alt={image.alt} + alt={image.metaData.altText} fill - src={image.url} + src={image.imageSizes.medium} style={{ objectFit: "cover" }} /> <div className={styles.fullViewFooter}> - {image.title && <Body color="white">{image.title}</Body>} + {image.metaData.title && ( + <Body color="white">{image.metaData.title}</Body> + )} </div> </motion.div> </AnimatePresence> diff --git a/components/Lightbox/Gallery.tsx b/components/Lightbox/Gallery.tsx index 23149d3de..578cc961e 100644 --- a/components/Lightbox/Gallery.tsx +++ b/components/Lightbox/Gallery.tsx @@ -20,7 +20,7 @@ export default function Gallery({ selectedImage, }: GalleryProps) { const mainImage = selectedImage || images[0] - const mainImageIndex = images.findIndex((img) => img.url === mainImage.url) + const mainImageIndex = images.findIndex((img) => img === mainImage) function getThumbImages() { const thumbs = [] @@ -55,16 +55,18 @@ export default function Gallery({ {/* Desktop Gallery */} <div className={styles.desktopGallery}> <div className={styles.galleryHeader}> - {mainImage.title && ( + {mainImage.metaData.title && ( <div className={styles.imageCaption}> - <Caption color="textMediumContrast">{mainImage.title}</Caption> + <Caption color="textMediumContrast"> + {mainImage.metaData.title} + </Caption> </div> )} </div> <div className={styles.mainImageWrapper}> <AnimatePresence initial={false} mode="wait"> <motion.div - key={mainImage.url} + key={mainImage.imageSizes.medium} className={styles.mainImageContainer} initial={{ opacity: 0, x: 300 }} animate={{ opacity: 1, x: 0 }} @@ -72,10 +74,10 @@ export default function Gallery({ transition={{ duration: 0.3 }} > <Image - src={mainImage.url} - alt={mainImage.alt} - layout="fill" - objectFit="cover" + src={mainImage.imageSizes.medium} + alt={mainImage.metaData.altText} + fill + className={styles.image} onClick={onImageClick} /> </motion.div> @@ -100,7 +102,7 @@ export default function Gallery({ <AnimatePresence initial={false}> {getThumbImages().map((image, index) => ( <motion.div - key={image.url} + key={image.imageSizes.tiny} className={styles.thumbnailContainer} onClick={() => onSelectImage(image)} initial={{ opacity: 0, x: 50 }} @@ -109,10 +111,10 @@ export default function Gallery({ transition={{ duration: 0.2, delay: index * 0.05 }} > <Image - src={image.url} - alt={image.alt} - layout="fill" - objectFit="cover" + src={image.imageSizes.tiny} + alt={image.metaData.altText} + fill + className={styles.image} /> </motion.div> ))} @@ -139,7 +141,7 @@ export default function Gallery({ <div className={styles.thumbnailGrid}> {images.map((image, index) => ( <motion.div - key={image.url} + key={image.imageSizes.small} className={`${styles.thumbnailContainer} ${index % 3 === 0 ? styles.fullWidthImage : ""}`} onClick={() => { onSelectImage(image) @@ -150,10 +152,10 @@ export default function Gallery({ transition={{ duration: 0.3, delay: index * 0.05 }} > <Image - src={image.url} - alt={image.alt} - layout="fill" - objectFit="cover" + src={image.imageSizes.small} + alt={image.metaData.altText} + fill + className={styles.image} /> </motion.div> ))} diff --git a/components/Lightbox/Lightbox.module.css b/components/Lightbox/Lightbox.module.css index ae46b5a0e..c98745601 100644 --- a/components/Lightbox/Lightbox.module.css +++ b/components/Lightbox/Lightbox.module.css @@ -184,6 +184,25 @@ max-width: 548px; } +.image { + object-fit: cover; +} + +@media (min-width: 768px) and (max-width: 1367px) { + .fullViewContainer { + grid-template-columns: 1fr; + justify-items: center; + padding: var(--Spacing-x5); + } + + .fullViewImageContainer { + position: relative; + width: 100%; + height: 100%; + max-height: 35rem; + } +} + @media (min-width: 1367px) { .mobileGallery, .thumbnailGrid { diff --git a/components/Lightbox/index.tsx b/components/Lightbox/index.tsx index f76063848..e9e5d8fa1 100644 --- a/components/Lightbox/index.tsx +++ b/components/Lightbox/index.tsx @@ -97,7 +97,7 @@ export default function Lightbox({ onClose={() => setIsOpen(false)} onSelectImage={(image) => { setSelectedImageIndex( - images.findIndex((img) => img.url === image.url) + images.findIndex((img) => img === image) ) }} onImageClick={() => setIsFullView(true)} diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 182575c2d..e4c90c5dc 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -163,30 +163,20 @@ export const facilitySchema = z.object({ ), }) +export const imageSchema = z.object({ + metaData: imageMetaDataSchema, + imageSizes: imageSizesSchema, +}) + export const gallerySchema = z.object({ - heroImages: z.array( - z.object({ - metaData: imageMetaDataSchema, - imageSizes: imageSizesSchema, - }) - ), - smallerImages: z.array( - z.object({ - metaData: imageMetaDataSchema, - imageSizes: imageSizesSchema, - }) - ), + heroImages: z.array(imageSchema), + smallerImages: z.array(imageSchema), }) const healthFacilitySchema = z.object({ type: z.string(), content: z.object({ - images: z.array( - z.object({ - metaData: imageMetaDataSchema, - imageSizes: imageSizesSchema, - }) - ), + images: z.array(imageSchema), texts: z.object({ facilityInformation: z.string().optional(), surroundingInformation: z.string().optional(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 883b3a3ee..c7a1da08f 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -9,7 +9,6 @@ import { notFound, serverErrorByStatus, } from "@/server/errors/trpc" -import { extractHotelImages } from "@/server/routers/utils/hotels" import { contentStackUidWithServiceProcedure, publicProcedure, @@ -230,7 +229,7 @@ export const hotelQueryRouter = router({ const included = validatedHotelData.data.included || [] const hotelAttributes = validatedHotelData.data.data.attributes - const images = extractHotelImages(hotelAttributes) + const images = hotelAttributes.gallery?.smallerImages const hotelAlerts = hotelAttributes.meta?.specialAlerts || [] const roomCategories = included diff --git a/server/routers/utils/hotels.ts b/server/routers/utils/hotels.ts deleted file mode 100644 index 37ae167cb..000000000 --- a/server/routers/utils/hotels.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ImageItem } from "@/types/components/lightbox/lightbox" -import type { Hotel } from "@/types/hotel" - -export function extractHotelImages(hotelData: Hotel): ImageItem[] { - const images: ImageItem[] = [] - - if (hotelData.hotelContent?.images) { - images.push({ - url: hotelData.hotelContent.images.imageSizes.large, - alt: hotelData.hotelContent.images.metaData.altText, - title: - hotelData.hotelContent.images.metaData.title || - hotelData.hotelContent.images.metaData.altText, - }) - } - - if (hotelData.healthFacilities) { - hotelData.healthFacilities.forEach((facility) => { - facility.content.images.forEach((image) => { - images.push({ - url: image.imageSizes.large, - alt: image.metaData.altText, - title: image.metaData.title || image.metaData.altText, - }) - }) - }) - } - - return images -} diff --git a/types/components/hotelPage/previewImages.ts b/types/components/hotelPage/previewImages.ts index 204a23fd4..d808fdcf7 100644 --- a/types/components/hotelPage/previewImages.ts +++ b/types/components/hotelPage/previewImages.ts @@ -1,6 +1,6 @@ -import type { ImageItem } from "@/types/components/lightbox/lightbox" +import type { GalleryImage } from "@/types/hotel" export type PreviewImagesProps = { - images: ImageItem[] + images: GalleryImage[] hotelName: string } diff --git a/types/components/hotelReservation/selectRate/imageGallery.ts b/types/components/hotelReservation/selectRate/imageGallery.ts index 333ff2d94..5d75189fa 100644 --- a/types/components/hotelReservation/selectRate/imageGallery.ts +++ b/types/components/hotelReservation/selectRate/imageGallery.ts @@ -1,3 +1,3 @@ -import type { GalleryImages } from "@/types/hotel" +import type { GalleryImage } from "@/types/hotel" -export type ImageGalleryProps = { images: GalleryImages; title: string } +export type ImageGalleryProps = { images: GalleryImage[]; title: string } diff --git a/types/components/lightbox/lightbox.ts b/types/components/lightbox/lightbox.ts index fc8d11066..af592bca6 100644 --- a/types/components/lightbox/lightbox.ts +++ b/types/components/lightbox/lightbox.ts @@ -1,26 +1,22 @@ -export interface ImageItem { - url: string - alt: string - title: string -} +import type { GalleryImage } from "@/types/hotel" export interface LightboxProps { - images: ImageItem[] + images: GalleryImage[] dialogTitle: string /* Accessible title for dialog screen readers */ children: React.ReactNode } export interface GalleryProps { - images: ImageItem[] + images: GalleryImage[] dialogTitle: string onClose: () => void - onSelectImage: (image: ImageItem) => void + onSelectImage: (image: GalleryImage) => void onImageClick: () => void - selectedImage: ImageItem | null + selectedImage: GalleryImage | null } export interface FullViewProps { - image: ImageItem + image: GalleryImage onClose: () => void onNext: () => void onPrev: () => void diff --git a/types/hotel.ts b/types/hotel.ts index 972c6459e..9c95a9067 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -2,8 +2,8 @@ import { z } from "zod" import { facilitySchema, - gallerySchema, getHotelDataSchema, + imageSchema, parkingSchema, pointOfInterestSchema, } from "@/server/routers/hotels/output" @@ -22,8 +22,7 @@ export type HotelTripAdvisor = | undefined export type RoomData = z.infer<typeof roomSchema> -export type GallerySchema = z.infer<typeof gallerySchema> -export type GalleryImages = GallerySchema["heroImages"] +export type GalleryImage = z.infer<typeof imageSchema> export type PointOfInterest = z.output<typeof pointOfInterestSchema> From 62f549e85d44ab6251dfd09e8007eddf4904debb Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson <simon.emanuelsson@scandichotels.com> Date: Mon, 28 Oct 2024 10:12:03 +0100 Subject: [PATCH 40/49] feat: get breakfast package from API --- actions/editProfile.ts | 2 +- actions/registerUser.ts | 2 +- actions/registerUserBookingFlow.ts | 2 +- .../[step]/@hotelHeader/[...paths]/page.tsx | 1 + .../[step]/@hotelHeader/loading.tsx | 5 + .../(standard)/[step]/@hotelHeader/page.tsx | 22 ++ .../[step]/@sidePeek/[...paths]/page.tsx | 1 + .../(standard)/[step]/@sidePeek/loading.tsx | 5 + .../(standard)/[step]/@sidePeek/page.tsx | 21 ++ .../(standard)/[step]/layout.tsx | 40 ++-- .../(standard)/[step]/page.tsx | 23 +- .../EnterDetails/BedType/index.tsx | 10 +- .../EnterDetails/BedType/schema.ts | 4 +- .../EnterDetails/Breakfast/index.tsx | 92 +++++--- .../EnterDetails/Breakfast/schema.ts | 14 +- .../Form/ChoiceCard/_Card/card.module.css | 4 + .../Form/ChoiceCard/_Card/card.ts | 9 + .../Form/ChoiceCard/_Card/index.tsx | 105 +++++---- constants/booking.ts | 15 +- i18n/dictionaries/da.json | 3 +- i18n/dictionaries/de.json | 3 +- i18n/dictionaries/en.json | 3 +- i18n/dictionaries/fi.json | 3 +- i18n/dictionaries/no.json | 3 +- i18n/dictionaries/sv.json | 3 +- lib/api/endpoints.ts | 205 ++++++++++++++++-- lib/api/index.ts | 2 +- lib/trpc/memoizedRequests/index.ts | 6 + lib/trpc/server.ts | 1 - server/context.ts | 5 +- server/routers/booking/mutation.ts | 2 +- server/routers/booking/query.ts | 4 +- server/routers/contentstack/reward/query.ts | 6 +- server/routers/hotels/input.ts | 4 + server/routers/hotels/output.ts | 31 ++- server/routers/hotels/query.ts | 132 ++++++++++- server/routers/hotels/utils.ts | 6 +- server/routers/user/mutation.ts | 31 +-- server/routers/user/query.ts | 35 +-- server/routers/user/utils.ts | 2 +- server/trpc.ts | 5 +- stores/enter-details.ts | 15 +- types/components/enterDetails/breakfast.ts | 20 +- types/enums/bedType.ts | 2 +- types/enums/breakfast.ts | 5 +- types/enums/currency.ts | 7 + types/enums/packages.ts | 7 + 47 files changed, 718 insertions(+), 210 deletions(-) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx create mode 100644 types/enums/currency.ts create mode 100644 types/enums/packages.ts diff --git a/actions/editProfile.ts b/actions/editProfile.ts index 8f0791c48..acf5d1b12 100644 --- a/actions/editProfile.ts +++ b/actions/editProfile.ts @@ -148,7 +148,7 @@ export const editProfile = protectedServerActionProcedure ) } - const apiResponse = await api.patch(api.endpoints.v1.profile, { + const apiResponse = await api.patch(api.endpoints.v1.Profile.profile, { body, cache: "no-store", headers: { diff --git a/actions/registerUser.ts b/actions/registerUser.ts index e65fc357f..ecd2318b4 100644 --- a/actions/registerUser.ts +++ b/actions/registerUser.ts @@ -55,7 +55,7 @@ export const registerUser = serviceServerActionProcedure let apiResponse try { - apiResponse = await api.post(api.endpoints.v1.profile, { + apiResponse = await api.post(api.endpoints.v1.Profile.profile, { body: parsedPayload.data, headers: { Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/actions/registerUserBookingFlow.ts b/actions/registerUserBookingFlow.ts index a0539d351..a34cad231 100644 --- a/actions/registerUserBookingFlow.ts +++ b/actions/registerUserBookingFlow.ts @@ -33,7 +33,7 @@ export const registerUserBookingFlow = serviceServerActionProcedure // TODO: Consume the API to register the user as soon as passwordless signup is enabled. // let apiResponse // try { - // apiResponse = await api.post(api.endpoints.v1.profile, { + // apiResponse = await api.post(api.endpoints.v1.Profile.profile, { // body: payload, // headers: { // Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx new file mode 100644 index 000000000..0fad268cc --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingHotelHeader() { + return <LoadingSpinner /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx new file mode 100644 index 000000000..58a216006 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx @@ -0,0 +1,22 @@ +import { redirect } from "next/navigation" + +import { getHotelData } from "@/lib/trpc/memoizedRequests" + +import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function HotelHeader({ + params, + searchParams, +}: PageArgs<LangParams, { hotel: string }>) { + const home = `/${params.lang}` + if (!searchParams.hotel) { + redirect(home) + } + const hotel = await getHotelData(searchParams.hotel, params.lang) + if (!hotel?.data) { + redirect(home) + } + return <HotelSelectionHeader hotel={hotel.data.attributes} /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx new file mode 100644 index 000000000..67515d4f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingHotelSidePeek() { + return <LoadingSpinner /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx new file mode 100644 index 000000000..13b770699 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx @@ -0,0 +1,21 @@ +import { redirect } from "next/navigation" + +import { getHotelData } from "@/lib/trpc/memoizedRequests" + +import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function HotelSidePeek({ + params, + searchParams, +}: PageArgs<LangParams, { hotel: string }>) { + if (!searchParams.hotel) { + redirect(`/${params.lang}`) + } + const hotel = await getHotelData(searchParams.hotel, params.lang) + if (!hotel?.data) { + redirect(`/${params.lang}`) + } + return <SidePeek hotel={hotel.data.attributes} /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 0e8edd50e..271d19e6d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -1,46 +1,32 @@ -import { redirect } from "next/navigation" - -import { - getCreditCardsSafely, - getHotelData, - getProfileSafely, -} from "@/lib/trpc/memoizedRequests" - import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" -import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" import Summary from "@/components/HotelReservation/EnterDetails/Summary" -import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" import { setLang } from "@/i18n/serverContext" +import { preload } from "./page" + import styles from "./layout.module.css" import { StepEnum } from "@/types/components/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" -function preload(id: string, lang: string) { - void getHotelData(id, lang) - void getProfileSafely() - void getCreditCardsSafely() -} - export default async function StepLayout({ children, + hotelHeader, params, -}: React.PropsWithChildren<LayoutArgs<LangParams & { step: StepEnum }>>) { - setLang(params.lang) - preload("811", params.lang) - - const hotel = await getHotelData("811", params.lang) - - if (!hotel?.data) { - redirect(`/${params.lang}`) + sidePeek, +}: React.PropsWithChildren< + LayoutArgs<LangParams & { step: StepEnum }> & { + hotelHeader: React.ReactNode + sidePeek: React.ReactNode } - +>) { + setLang(params.lang) + preload() return ( <EnterDetailsProvider step={params.step}> <main className={styles.layout}> - <HotelSelectionHeader hotel={hotel.data.attributes} /> + {hotelHeader} <div className={styles.content}> <SelectedRoom /> {children} @@ -48,7 +34,7 @@ export default async function StepLayout({ <Summary /> </aside> </div> - <SidePeek hotel={hotel.data.attributes} /> + {sidePeek} </main> </EnterDetailsProvider> ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 8e8d3c891..77ec20203 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -1,6 +1,7 @@ -import { notFound } from "next/navigation" +import { notFound, redirect } from "next/navigation" import { + getBreakfastPackages, getCreditCardsSafely, getHotelData, getProfileSafely, @@ -17,22 +18,32 @@ import { getIntl } from "@/i18n" import { StepEnum } from "@/types/components/enterDetails/step" import type { LangParams, PageArgs } from "@/types/params" +export function preload() { + void getProfileSafely() + void getCreditCardsSafely() +} + function isValidStep(step: string): step is StepEnum { return Object.values(StepEnum).includes(step as StepEnum) } export default async function StepPage({ params, -}: PageArgs<LangParams & { step: StepEnum }>) { - const { step, lang } = params + searchParams, +}: PageArgs<LangParams & { step: StepEnum }, { hotel: string }>) { + if (!searchParams.hotel) { + redirect(`/${params.lang}`) + } + void getBreakfastPackages(searchParams.hotel) const intl = await getIntl() - const hotel = await getHotelData("811", lang) + const hotel = await getHotelData(searchParams.hotel, params.lang) const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() + const breakfastPackages = await getBreakfastPackages(searchParams.hotel) - if (!isValidStep(step) || !hotel) { + if (!isValidStep(params.step) || !hotel) { return notFound() } @@ -51,7 +62,7 @@ export default async function StepPage({ step={StepEnum.breakfast} label={intl.formatMessage({ id: "Select breakfast options" })} > - <Breakfast /> + <Breakfast packages={breakfastPackages} /> </SectionAccordion> <SectionAccordion header="Details" diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index 106aaa80a..e73eacf87 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -15,7 +15,7 @@ import { bedTypeSchema } from "./schema" import styles from "./bedOptions.module.css" import type { BedTypeSchema } from "@/types/components/enterDetails/bedType" -import { bedTypeEnum } from "@/types/enums/bedType" +import { BedTypeEnum } from "@/types/enums/bedType" export default function BedType() { const intl = useIntl() @@ -61,7 +61,7 @@ export default function BedType() { <RadioCard Icon={KingBedIcon} iconWidth={46} - id={bedTypeEnum.KING} + id={BedTypeEnum.KING} name="bedType" subtitle={intl.formatMessage( { id: "{width} cm × {length} cm" }, @@ -72,12 +72,12 @@ export default function BedType() { )} text={text} title={intl.formatMessage({ id: "King bed" })} - value={bedTypeEnum.KING} + value={BedTypeEnum.KING} /> <RadioCard Icon={KingBedIcon} iconWidth={46} - id={bedTypeEnum.QUEEN} + id={BedTypeEnum.QUEEN} name="bedType" subtitle={intl.formatMessage( { id: "{width} cm × {length} cm" }, @@ -88,7 +88,7 @@ export default function BedType() { )} text={text} title={intl.formatMessage({ id: "Queen bed" })} - value={bedTypeEnum.QUEEN} + value={BedTypeEnum.QUEEN} /> </form> </FormProvider> diff --git a/components/HotelReservation/EnterDetails/BedType/schema.ts b/components/HotelReservation/EnterDetails/BedType/schema.ts index d9f52a407..8f77ba768 100644 --- a/components/HotelReservation/EnterDetails/BedType/schema.ts +++ b/components/HotelReservation/EnterDetails/BedType/schema.ts @@ -1,7 +1,7 @@ import { z } from "zod" -import { bedTypeEnum } from "@/types/enums/bedType" +import { BedTypeEnum } from "@/types/enums/bedType" export const bedTypeSchema = z.object({ - bedType: z.nativeEnum(bedTypeEnum), + bedType: z.nativeEnum(BedTypeEnum), }) diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index c2ea00754..603a3aaad 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -7,36 +7,50 @@ import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" -import { BreakfastIcon, NoBreakfastIcon } from "@/components/Icons" +import { Highlight } from "@/components/TempDesignSystem/Form/ChoiceCard/_Card" import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio" -import { breakfastSchema } from "./schema" +import { breakfastFormSchema } from "./schema" import styles from "./breakfast.module.css" -import type { BreakfastSchema } from "@/types/components/enterDetails/breakfast" -import { breakfastEnum } from "@/types/enums/breakfast" +import type { + BreakfastFormSchema, + BreakfastProps, +} from "@/types/components/enterDetails/breakfast" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" -export default function Breakfast() { +export default function Breakfast({ packages }: BreakfastProps) { const intl = useIntl() const breakfast = useEnterDetailsStore((state) => state.data.breakfast) - const methods = useForm<BreakfastSchema>({ - defaultValues: breakfast ? { breakfast } : undefined, + let defaultValues = undefined + if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { + defaultValues = { breakfast: BreakfastPackageEnum.NO_BREAKFAST } + } else if (breakfast?.code) { + defaultValues = { breakfast: breakfast.code } + } + const methods = useForm<BreakfastFormSchema>({ + defaultValues, criteriaMode: "all", mode: "all", - resolver: zodResolver(breakfastSchema), + resolver: zodResolver(breakfastFormSchema), reValidateMode: "onChange", }) const completeStep = useEnterDetailsStore((state) => state.completeStep) const onSubmit = useCallback( - (values: BreakfastSchema) => { - completeStep(values) + (values: BreakfastFormSchema) => { + const pkg = packages?.find((p) => p.code === values.breakfast) + if (pkg) { + completeStep({ breakfast: pkg }) + } else { + completeStep({ breakfast: BreakfastPackageEnum.NO_BREAKFAST }) + } }, - [completeStep] + [completeStep, packages] ) useEffect(() => { @@ -47,30 +61,46 @@ export default function Breakfast() { return () => subscription.unsubscribe() }, [methods, onSubmit]) + if (!packages) { + return null + } + return ( <FormProvider {...methods}> <form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}> - <RadioCard - Icon={BreakfastIcon} - id={breakfastEnum.BREAKFAST} - name="breakfast" - subtitle={intl.formatMessage<React.ReactNode>( - { id: "<b>{amount} {currency}</b>/night per adult" }, - { - amount: "150", - b: (str) => <b>{str}</b>, - currency: "SEK", + {packages.map((pkg) => ( + <RadioCard + key={pkg.code} + id={pkg.code} + name="breakfast" + subtitle={ + pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST + ? intl.formatMessage<React.ReactNode>( + { id: "breakfast.price.free" }, + { + amount: pkg.originalPrice, + currency: pkg.currency, + free: (str) => <Highlight>{str}</Highlight>, + strikethrough: (str) => <s>{str}</s>, + } + ) + : intl.formatMessage( + { id: "breakfast.price" }, + { + amount: pkg.packagePrice, + currency: pkg.currency, + } + ) } - )} - text={intl.formatMessage({ - id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", - })} - title={intl.formatMessage({ id: "Breakfast buffet" })} - value={breakfastEnum.BREAKFAST} - /> + text={intl.formatMessage({ + id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", + })} + title={intl.formatMessage({ id: "Breakfast buffet" })} + value={pkg.code} + /> + ))} <RadioCard - Icon={NoBreakfastIcon} - id={breakfastEnum.NO_BREAKFAST} + id={BreakfastPackageEnum.NO_BREAKFAST} name="breakfast" subtitle={intl.formatMessage( { id: "{amount} {currency}" }, @@ -83,7 +113,7 @@ export default function Breakfast() { id: "You can always change your mind later and add breakfast at the hotel.", })} title={intl.formatMessage({ id: "No breakfast" })} - value={breakfastEnum.NO_BREAKFAST} + value={BreakfastPackageEnum.NO_BREAKFAST} /> </form> </FormProvider> diff --git a/components/HotelReservation/EnterDetails/Breakfast/schema.ts b/components/HotelReservation/EnterDetails/Breakfast/schema.ts index 34cc5efca..5f8c1f354 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/schema.ts +++ b/components/HotelReservation/EnterDetails/Breakfast/schema.ts @@ -1,7 +1,15 @@ import { z } from "zod" -import { breakfastEnum } from "@/types/enums/breakfast" +import { breakfastPackageSchema } from "@/server/routers/hotels/output" -export const breakfastSchema = z.object({ - breakfast: z.nativeEnum(breakfastEnum), +import { BreakfastPackageEnum } from "@/types/enums/breakfast" + +export const breakfastStoreSchema = z.object({ + breakfast: breakfastPackageSchema.or( + z.literal(BreakfastPackageEnum.NO_BREAKFAST) + ), +}) + +export const breakfastFormSchema = z.object({ + breakfast: z.string().or(z.literal(BreakfastPackageEnum.NO_BREAKFAST)), }) diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css index 1044596f6..fa7d6d13a 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css @@ -70,3 +70,7 @@ .listItem:nth-of-type(n + 2) { margin-top: var(--Spacing-x-quarter); } + +.highlight { + color: var(--Scandic-Brand-Scandic-Red); +} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts index d1a961150..145116409 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts @@ -34,3 +34,12 @@ export type CheckboxProps = export type RadioProps = | Omit<ListCardProps, "type"> | Omit<TextCardProps, "type"> + +export interface ListProps extends Pick<ListCardProps, "declined"> { + list?: ListCardProps["list"] +} + +export interface SubtitleProps + extends Pick<BaseCardProps, "highlightSubtitle" | "subtitle"> {} + +export interface TextProps extends Pick<TextCardProps, "text"> {} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx index c57d56cc4..2a3faf57b 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx @@ -2,16 +2,16 @@ import { useFormContext } from "react-hook-form" -import { CheckIcon, CloseIcon, HeartIcon } from "@/components/Icons" +import { CheckIcon, CloseIcon } from "@/components/Icons" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import styles from "./card.module.css" -import type { CardProps } from "./card" +import type { CardProps, ListProps, SubtitleProps, TextProps } from "./card" export default function Card({ - Icon = HeartIcon, + Icon, iconHeight = 32, iconWidth = 32, declined = false, @@ -26,56 +26,79 @@ export default function Card({ value, }: CardProps) { const { register } = useFormContext() - return ( <label className={styles.label} data-declined={declined} tabIndex={0}> - <Caption className={styles.title} type="label" uppercase> + <Caption className={styles.title} color="burgundy" type="label" uppercase> {title} </Caption> - {subtitle ? ( - <Caption - className={styles.subtitle} - color={highlightSubtitle ? "baseTextAccent" : "uiTextHighContrast"} - type="regular" - > - {subtitle} - </Caption> - ) : null} - <Icon - className={styles.icon} - color="uiTextHighContrast" - height={iconHeight} - width={iconWidth} - /> - {list - ? list.map((listItem) => ( - <span key={listItem.title} className={styles.listItem}> - {declined ? ( - <CloseIcon - color="uiTextMediumContrast" - height={20} - width={20} - /> - ) : ( - <CheckIcon color="baseIconLowContrast" height={20} width={20} /> - )} - <Footnote color="uiTextMediumContrast">{listItem.title}</Footnote> - </span> - )) - : null} - {text ? ( - <Footnote className={styles.text} color="uiTextMediumContrast"> - {text} - </Footnote> + <Subtitle highlightSubtitle={highlightSubtitle} subtitle={subtitle} /> + {Icon ? ( + <Icon + className={styles.icon} + color="uiTextHighContrast" + height={iconHeight} + width={iconWidth} + /> ) : null} + <List declined={declined} list={list} /> + <Text text={text} /> <input + {...register(name)} aria-hidden id={id || name} hidden type={type} value={value} - {...register(name)} /> </label> ) } + +function List({ declined, list }: ListProps) { + if (!list) { + return null + } + + return list.map((listItem) => ( + <span key={listItem.title} className={styles.listItem}> + {declined ? ( + <CloseIcon color="uiTextMediumContrast" height={20} width={20} /> + ) : ( + <CheckIcon color="baseIconLowContrast" height={20} width={20} /> + )} + <Footnote color="uiTextMediumContrast">{listItem.title}</Footnote> + </span> + )) +} + +function Subtitle({ highlightSubtitle, subtitle }: SubtitleProps) { + if (!subtitle) { + return null + } + + return ( + <Caption + className={styles.subtitle} + color={highlightSubtitle ? "baseTextAccent" : "uiTextMediumContrast"} + type="label" + uppercase + > + {subtitle} + </Caption> + ) +} + +function Text({ text }: TextProps) { + if (!text) { + return null + } + return ( + <Footnote className={styles.text} color="uiTextMediumContrast"> + {text} + </Footnote> + ) +} + +export function Highlight({ children }: React.PropsWithChildren) { + return <span className={styles.highlight}>{children}</span> +} diff --git a/constants/booking.ts b/constants/booking.ts index 8f5acb120..da6b30695 100644 --- a/constants/booking.ts +++ b/constants/booking.ts @@ -1,7 +1,18 @@ export enum BookingStatusEnum { - CreatedInOhip = "CreatedInOhip", - PaymentRegistered = "PaymentRegistered", BookingCompleted = "BookingCompleted", + Cancelled = "Cancelled", + CheckedOut = "CheckedOut", + ConfirmedInScorpio = "ConfirmedInScorpio", + CreatedInOhip = "CreatedInOhip", + PaymentAuthorized = "PaymentAuthorized", + PaymentCancelled = "PaymentCancelled", + PaymentError = "PaymentError", + PaymentFailed = "PaymentFailed", + PaymentRegistered = "PaymentRegistered", + PaymentSucceeded = "PaymentSucceeded", + PendingAcceptPriceChange = "PendingAcceptPriceChange", + PendingGuarantee = "PendingGuarantee", + PendingPayment = "PendingPayment", } export enum BedTypeEnum { diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index e47484466..c66448f17 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/nat pr. voksen", "A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.", "A photo of the room": "Et foto af værelset", "ACCE": "Tilgængelighed", @@ -366,6 +365,8 @@ "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", "booking.thisRoomIsEquippedWith": "Dette værelse er udstyret med", + "breakfast.price": "{amount} {currency}/nat", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/nat", "by": "inden", "characters": "tegn", "guest": "gæst", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 88a5092e5..1959939ee 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/Nacht pro Erwachsener", "A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.", "A photo of the room": "Ein Foto des Zimmers", "ACCE": "Zugänglichkeit", @@ -367,6 +366,8 @@ "booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", "booking.thisRoomIsEquippedWith": "Dieses Zimmer ist ausgestattet mit", + "breakfast.price": "{amount} {currency}/Nacht", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/Nacht", "by": "bis", "characters": "figuren", "guest": "gast", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 75fbbd805..2fa6793d7 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/night per adult", "A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.", "A photo of the room": "A photo of the room", "ACCE": "Accessibility", @@ -385,6 +384,8 @@ "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", "booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsLink>Terms & Conditions</termsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyLink>Scandic's Privacy policy</privacyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.", "booking.thisRoomIsEquippedWith": "This room is equipped with", + "breakfast.price": "{amount} {currency}/night", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/night", "by": "by", "characters": "characters", "from": "from", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 6f2f28b63..883b9d277 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/yö per aikuinen", "A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.", "A photo of the room": "Kuva huoneesta", "ACCE": "Saavutettavuus", @@ -367,6 +366,8 @@ "booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}", "booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", "booking.thisRoomIsEquippedWith": "Tämä huone on varustettu", + "breakfast.price": "{amount} {currency}/yö", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/yö", "by": "mennessä", "characters": "hahmoja", "guest": "Vieras", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 0b45a4673..075f84783 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per voksen", "A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.", "A photo of the room": "Et bilde av rommet", "ACCE": "Tilgjengelighet", @@ -363,6 +362,8 @@ "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", "booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med", + "breakfast.price": "{amount} {currency}/natt", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt", "by": "innen", "characters": "tegn", "guest": "gjest", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 406aa4cb4..0b48022d3 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per vuxen", "A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.", "A photo of the room": "Ett foto av rummet", "ACCE": "Tillgänglighet", @@ -364,6 +363,8 @@ "booking.rooms": "{totalRooms, plural, one {# rum} other {# rum}}", "booking.terms": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella <termsLink>Villkoren och villkoren</termsLink>, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med <privacyLink>Scandics integritetspolicy</privacyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under min besök i fall att något är tillbaka betalt.", "booking.thisRoomIsEquippedWith": "Detta rum är utrustat med", + "breakfast.price": "{amount} {currency}/natt", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt", "by": "innan", "characters": "tecken", "guest": "gäst", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index be1aee2fd..100fb7518 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -2,29 +2,190 @@ * Nested enum requires namespace */ export namespace endpoints { - export const enum v0 { - profile = "profile/v0/Profile", + namespace base { + export const enum path { + availability = "availability", + booking = "booking", + hotel = "hotel", + package = "package", + profile = "profile", + } + + export const enum enitity { + Ancillary = "Ancillary", + Availabilities = "availabilities", + Bookings = "Bookings", + Breakfast = "breakfast", + Cities = "Cities", + Countries = "Countries", + Hotels = "Hotels", + Locations = "Locations", + Packages = "packages", + Profile = "Profile", + Reward = "Reward", + Stays = "Stays", + Transaction = "Transaction", + } } - export const enum v1 { - hotelsAvailability = "availability/v1/availabilities/city", - roomsAvailability = "availability/v1/availabilities/hotel", - profile = "profile/v1/Profile", - booking = "booking/v1/Bookings", - creditCards = `${profile}/creditCards`, - city = "hotel/v1/Cities", - citiesCountry = `${city}/country`, - countries = "hotel/v1/Countries", - friendTransactions = "profile/v1/Transaction/friendTransactions", - hotels = "hotel/v1/Hotels", - initiateSaveCard = `${creditCards}/initiateSaveCard`, - locations = "hotel/v1/Locations", - previousStays = "booking/v1/Stays/past", - upcomingStays = "booking/v1/Stays/future", - rewards = `${profile}/reward`, - tierRewards = `${profile}/TierRewards`, - subscriberId = `${profile}/SubscriberId`, - packages = "package/v1/packages/hotel", + + export namespace v1 { + const version = "v1" + /** + * availability (Swagger) + * https://tstapi.scandichotels.com/availability/swagger/v1/index.html + */ + export namespace Availability { + export function city(cityId: string) { + return `${base.path.availability}/${version}/${base.enitity.Availabilities}/city/${cityId}` + } + export function hotel(hotelId: string) { + return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotel/${hotelId}` + } + } + + /** + * booking (Swagger) + * https://tstapi.scandichotels.com/booking/swagger/v1/index.html + */ + export namespace Booking { + export const bookings = `${base.path.booking}/${version}/${base.enitity.Bookings}` + + export function booking(confirmationNumber: string) { + return `${bookings}/${confirmationNumber}` + } + export function cancel(confirmationNumber: string) { + return `${bookings}/${confirmationNumber}/cancel` + } + export function status(confirmationNumber: string) { + return `${bookings}/${confirmationNumber}/status` + } + + export const enum Stays { + future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`, + past = `${base.path.booking}/${version}/${base.enitity.Stays}/past`, + } + } + + /** + * hotel (Swagger) + * https://tstapi.scandichotels.com/hotel/swagger/v1/index.html + */ + export namespace Hotel { + export const cities = `${base.path.hotel}/${version}/${base.enitity.Cities}` + export namespace Cities { + export function city(cityId: string) { + return `${cities}/${cityId}` + } + export function country(countryId: string) { + return `${cities}/country/${countryId}` + } + export function hotel(hotelId: string) { + return `${cities}/hotel/${hotelId}` + } + } + + export const countries = `${base.path.hotel}/${version}/${base.enitity.Countries}` + export namespace Countries { + export function country(countryId: string) { + return `${countries}/${countryId}` + } + } + + export const hotels = `${base.path.hotel}/${version}/${base.enitity.Hotels}` + export namespace Hotels { + export function hotel(hotelId: string) { + return `${hotels}/${hotelId}` + } + export function meetingRooms(hotelId: string) { + return `${hotels}/${hotelId}/meetingRooms` + } + export function merchantInformation(hotelId: string) { + return `${hotels}/${hotelId}/merchantInformation` + } + export function nearbyHotels(hotelId: string) { + return `${hotels}/${hotelId}/nearbyHotels` + } + export function restaurants(hotelId: string) { + return `${hotels}/${hotelId}/restaurants` + } + export function roomCategories(hotelId: string) { + return `${hotels}/${hotelId}/roomCategories` + } + } + + export const locations = `${base.path.hotel}/${version}/${base.enitity.Locations}` + } + + /** + * package (Swagger) + * https://tstapi.scandichotels.com/package/swagger/v1/index.html + */ + export namespace Package { + export namespace Ancillary { + export function hotel(hotelId: string) { + return `${base.path.package}/${version}/${base.enitity.Ancillary}/hotel/${hotelId}` + } + export function hotelAncillaries(hotelId: string) { + return `${base.path.package}/${version}/${base.enitity.Ancillary}/hotel/${hotelId}/ancillaries` + } + } + + export namespace Breakfast { + export function hotel(hotelId: string) { + return `${base.path.package}/${version}/${base.enitity.Breakfast}/hotel/${hotelId}` + } + } + + export namespace Packages { + export function hotel(hotelId: string) { + return `${base.path.package}/${version}/${base.enitity.Packages}/hotel/${hotelId}` + } + } + } + + /** + * profile (Swagger) + * https://tstapi.scandichotels.com/profile/swagger/v1/index.html + */ + export namespace Profile { + export const invalidateSessions = `${base.path.profile}/${version}/${base.enitity.Profile}/invalidateSessions` + export const membership = `${base.path.profile}/${version}/${base.enitity.Profile}/membership` + export const profile = `${base.path.profile}/${version}/${base.enitity.Profile}` + export const reward = `${base.path.profile}/${version}/${base.enitity.Profile}/reward` + export const subscriberId = `${base.path.profile}/${version}/${base.enitity.Profile}/SubscriberId` + export const tierRewards = `${base.path.profile}/${version}/${base.enitity.Profile}/tierRewards` + + export function deleteProfile(profileId: string) { + return `${profile}/${profileId}` + } + + export const creditCards = `${base.path.profile}/${version}/${base.enitity.Profile}/creditCards` + export namespace CreditCards { + export const initiateSaveCard = `${creditCards}/initiateSaveCard` + + export function deleteCreditCard(creditCardId: string) { + return `${creditCards}/${creditCardId}` + } + export function transaction(transactionId: string) { + return `${creditCards}/${transactionId}` + } + } + + export namespace Reward { + export const allTiers = `${base.path.profile}/${version}/${base.enitity.Reward}/AllTiers` + export const reward = `${base.path.profile}/${version}/${base.enitity.Reward}` + export const unwrap = `${base.path.profile}/${version}/${base.enitity.Reward}/Unwrap` + + export function claim(rewardId: string) { + return `${base.path.profile}/${version}/${base.enitity.Reward}/Claim/${rewardId}` + } + } + + export const enum Transaction { + friendTransactions = `${base.path.profile}/${version}/${base.enitity.Transaction}/friendTransactions`, + } + } } } -export type Endpoint = endpoints.v0 | endpoints.v1 +export type Endpoint = string diff --git a/lib/api/index.ts b/lib/api/index.ts index 475e0da4e..46bae9f88 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -28,7 +28,7 @@ const wrappedFetch = fetchRetry(fetch, { }) export async function get( - endpoint: Endpoint | `${Endpoint}/${string}`, + endpoint: Endpoint, options: RequestOptionsWithOutBody, params = {} ) { diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index d09349a8c..e34f241ce 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -89,3 +89,9 @@ export const getLanguageSwitcher = cache( export const getSiteConfig = cache(async function getMemoizedSiteConfig() { return serverClient().contentstack.base.siteConfig() }) + +export const getBreakfastPackages = cache(async function getMemoizedPackages( + hotelId: string +) { + return serverClient().hotel.packages.breakfast({ hotelId }) +}) diff --git a/lib/trpc/server.ts b/lib/trpc/server.ts index 46c0f8840..5d8539a8a 100644 --- a/lib/trpc/server.ts +++ b/lib/trpc/server.ts @@ -6,7 +6,6 @@ import { login } from "@/constants/routes/handleAuth" import { webviews } from "@/constants/routes/webviews" import { appRouter } from "@/server" import { createContext } from "@/server/context" -import { internalServerError } from "@/server/errors/next" import { createCallerFactory } from "@/server/trpc" const createCaller = createCallerFactory(appRouter) diff --git a/server/context.ts b/server/context.ts index 76ea0ba34..f33d6b1c9 100644 --- a/server/context.ts +++ b/server/context.ts @@ -1,5 +1,6 @@ import { cookies, headers } from "next/headers" import { type Session } from "next-auth" +import { cache } from "react" import { Lang } from "@/constants/languages" @@ -37,7 +38,7 @@ export function createContextInner(opts: CreateContextOptions) { * This is the actual context you'll use in your router * @link https://trpc.io/docs/context **/ -export function createContext() { +export const createContext = cache(function () { const h = headers() const cookie = cookies() @@ -66,6 +67,6 @@ export function createContext() { webToken: webviewTokenCookie?.value, contentType: h.get("x-contenttype")!, }) -} +}) export type Context = ReturnType<typeof createContext> diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 9e5677d32..2edbd5bdd 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -62,7 +62,7 @@ export const bookingMutationRouter = router({ Authorization: `Bearer ${ctx.serviceToken}`, } - const apiResponse = await api.post(api.endpoints.v1.booking, { + const apiResponse = await api.post(api.endpoints.v1.Booking.bookings, { headers, body: input, }) diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index 76b874ba3..5c72bb284 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -33,7 +33,7 @@ export const bookingQueryRouter = router({ getBookingConfirmationCounter.add(1, { confirmationNumber }) const apiResponse = await api.get( - `${api.endpoints.v1.booking}/${confirmationNumber}`, + api.endpoints.v1.Booking.booking(confirmationNumber), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -142,7 +142,7 @@ export const bookingQueryRouter = router({ getBookingStatusCounter.add(1, { confirmationNumber }) const apiResponse = await api.get( - `${api.endpoints.v1.booking}/${confirmationNumber}/status`, + api.endpoints.v1.Booking.status(confirmationNumber), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 419e0a2c2..b4e0e54b2 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -75,7 +75,7 @@ function getUniqueRewardIds(rewardIds: string[]) { const getAllCachedApiRewards = unstable_cache( async function (token) { - const apiResponse = await api.get(api.endpoints.v1.tierRewards, { + const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, { headers: { Authorization: `Bearer ${token}`, }, @@ -194,7 +194,7 @@ export const rewardQueryRouter = router({ const { limit, cursor } = input - const apiResponse = await api.get(api.endpoints.v1.rewards, { + const apiResponse = await api.get(api.endpoints.v1.Profile.reward, { cache: undefined, // override defaultOptions headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -393,7 +393,7 @@ export const rewardQueryRouter = router({ surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { getCurrentRewardCounter.add(1) - const apiResponse = await api.get(api.endpoints.v1.rewards, { + const apiResponse = await api.get(api.endpoints.v1.Profile.reward, { cache: undefined, // override defaultOptions headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 6deff63a6..61927c3d2 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -39,3 +39,7 @@ export const getlHotelDataInputSchema = z.object({ .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) .optional(), }) + +export const getBreakfastPackageInput = z.object({ + hotelId: z.string().min(1, { message: "hotelId is required" }), +}) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index e4c90c5dc..f8e2b8a6e 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -9,7 +9,9 @@ import { getPoiGroupByCategoryName } from "./utils" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { AlertTypeEnum } from "@/types/enums/alert" +import { CurrencyEnum } from "@/types/enums/currency" import { FacilityEnum } from "@/types/enums/facilities" +import { PackageTypeEnum } from "@/types/enums/packages" import { PointOfInterestCategoryNameEnum } from "@/types/hotel" const ratingsSchema = z @@ -653,7 +655,7 @@ export const apiCountriesSchema = z.object({ name: z.string(), }), hotelInformationSystemId: z.number().optional(), - id: z.string().optional(), + id: z.string().optional().default(""), language: z.string().optional(), type: z.literal("countries"), }) @@ -794,3 +796,30 @@ export const apiLocationsSchema = z.object({ }) ), }) + +export const breakfastPackageSchema = z.object({ + code: z.string(), + currency: z.nativeEnum(CurrencyEnum), + description: z.string(), + originalPrice: z.number().default(0), + packagePrice: z.number(), + packageType: z.enum([ + PackageTypeEnum.BreakfastAdult, + PackageTypeEnum.BreakfastChildren, + ]), + totalPrice: z.number(), +}) + +export const breakfastPackagesSchema = z + .object({ + data: z.object({ + attributes: z.object({ + hotelId: z.number(), + packages: z.array(breakfastPackageSchema), + }), + type: z.literal("breakfastpackage"), + }), + }) + .transform(({ data }) => + data.attributes.packages.filter((pkg) => pkg.code.match(/^(BRF\d+)$/gm)) + ) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index c7a1da08f..a7b6c220e 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -13,6 +13,7 @@ import { contentStackUidWithServiceProcedure, publicProcedure, router, + safeProtectedServiceProcedure, serviceProcedure, } from "@/server/trpc" import { toApiLang } from "@/server/utils" @@ -24,11 +25,13 @@ import { getHotelPageCounter, validateHotelPageRefs, } from "../contentstack/hotelPage/utils" +import { getVerifiedUser, parsedUser } from "../user/query" import { getRoomPackagesInputSchema, getRoomPackagesSchema, } from "./schemas/packages" import { + getBreakfastPackageInput, getHotelInputSchema, getHotelsAvailabilityInputSchema, getlHotelDataInputSchema, @@ -36,6 +39,7 @@ import { getRoomsAvailabilityInputSchema, } from "./input" import { + breakfastPackagesSchema, getHotelDataSchema, getHotelsAvailabilitySchema, getRatesSchema, @@ -51,6 +55,7 @@ import { import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { Facility } from "@/types/hotel" import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage" @@ -88,6 +93,14 @@ const roomsAvailabilityFailCounter = meter.createCounter( "trpc.hotel.availability.rooms-fail" ) +const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast") +const breakfastPackagesSuccessCounter = meter.createCounter( + "trpc.package.breakfast-success" +) +const breakfastPackagesFailCounter = meter.createCounter( + "trpc.package.breakfast-fail" +) + async function getContentstackData(lang: Lang, uid?: string | null) { if (!uid) { return null @@ -169,7 +182,7 @@ export const hotelQueryRouter = router({ }) ) const apiResponse = await api.get( - `${api.endpoints.v1.hotels}/${hotelId}`, + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -320,7 +333,7 @@ export const hotelQueryRouter = router({ JSON.stringify({ query: { cityId, params } }) ) const apiResponse = await api.get( - `${api.endpoints.v1.hotelsAvailability}/${cityId}`, + api.endpoints.v1.Availability.city(cityId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -444,7 +457,7 @@ export const hotelQueryRouter = router({ JSON.stringify({ query: { hotelId, params } }) ) const apiResponse = await api.get( - `${api.endpoints.v1.roomsAvailability}/${hotelId}`, + api.endpoints.v1.Availability.hotel(hotelId.toString()), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -587,7 +600,7 @@ export const hotelQueryRouter = router({ ) const apiResponse = await api.get( - `${api.endpoints.v1.hotels}/${hotelId}`, + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -734,7 +747,7 @@ export const hotelQueryRouter = router({ ) const apiResponse = await api.get( - `${api.endpoints.v1.packages}/${hotelId}`, + api.endpoints.v1.Package.Packages.hotel(hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -789,5 +802,114 @@ export const hotelQueryRouter = router({ return validatedPackagesData.data }), + breakfast: safeProtectedServiceProcedure + .input(getBreakfastPackageInput) + .query(async function ({ ctx, input }) { + const params = { + Adults: 2, + EndDate: "2024-10-28", + StartDate: "2024-10-25", + } + const metricsData = { ...input, ...params } + breakfastPackagesCounter.add(1, metricsData) + console.info( + "api.package.breakfast start", + JSON.stringify({ query: metricsData }) + ) + + const apiResponse = await api.get( + api.endpoints.v1.Package.Breakfast.hotel(input.hotelId), + { + cache: undefined, + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + next: { + revalidate: 60, + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + breakfastPackagesFailCounter.add(1, { + ...metricsData, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.hotels.hotelsAvailability error", + JSON.stringify({ + query: metricsData, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return null + } + + const apiJson = await apiResponse.json() + const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson) + if (!breakfastPackages.success) { + hotelsAvailabilityFailCounter.add(1, { + ...metricsData, + error_type: "validation_error", + error: JSON.stringify(breakfastPackages.error), + }) + console.error( + "api.package.breakfast validation error", + JSON.stringify({ + query: metricsData, + error: breakfastPackages.error, + }) + ) + return null + } + + breakfastPackagesSuccessCounter.add(1, metricsData) + console.info( + "api.package.breakfast success", + JSON.stringify({ + query: metricsData, + }) + ) + + if (ctx.session?.token) { + const apiUser = await getVerifiedUser({ session: ctx.session }) + if (apiUser && !("error" in apiUser)) { + const user = parsedUser(apiUser.data, false) + if ( + user.membership && + ["L6", "L7"].includes(user.membership.membershipLevel) + ) { + const originalBreakfastPackage = breakfastPackages.data.find( + (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST + ) + const freeBreakfastPackage = breakfastPackages.data.find( + (pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST + ) + if (freeBreakfastPackage) { + if (originalBreakfastPackage) { + freeBreakfastPackage.originalPrice = + originalBreakfastPackage.packagePrice + } + return [freeBreakfastPackage] + } + } + } + } + + return breakfastPackages.data.filter( + (pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST + ) + }), }), }) diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts index 84020bfaa..6d384b3cf 100644 --- a/server/routers/hotels/utils.ts +++ b/server/routers/hotels/utils.ts @@ -96,7 +96,7 @@ export async function getCountries( return unstable_cache( async function (searchParams) { const countryResponse = await api.get( - api.endpoints.v1.countries, + api.endpoints.v1.Hotel.countries, options, searchParams ) @@ -136,7 +136,7 @@ export async function getCitiesByCountry( await Promise.all( searchedCountries.data.map(async (country) => { const countryResponse = await api.get( - `${api.endpoints.v1.citiesCountry}/${country.name}`, + api.endpoints.v1.Hotel.Cities.country(country.name), options, searchParams ) @@ -182,7 +182,7 @@ export async function getLocations( groupedCitiesByCountry: CitiesGroupedByCountry | null ) { const apiResponse = await api.get( - api.endpoints.v1.locations, + api.endpoints.v1.Hotel.locations, options, searchParams ) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts index 005941090..b03e6a68e 100644 --- a/server/routers/user/mutation.ts +++ b/server/routers/user/mutation.ts @@ -35,16 +35,19 @@ export const userMutationRouter = router({ "api.user.creditCard.add start", JSON.stringify({ query: { language: input.language } }) ) - const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, { - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - body: { - language: input.language, - mobileToken: false, - redirectUrl: `api/web/add-card-callback/${input.language}`, - }, - }) + const apiResponse = await api.post( + api.endpoints.v1.Profile.CreditCards.initiateSaveCard, + { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + body: { + language: input.language, + mobileToken: false, + redirectUrl: `api/web/add-card-callback/${input.language}`, + }, + } + ) if (!apiResponse.ok) { const text = await apiResponse.text() @@ -85,7 +88,7 @@ export const userMutationRouter = router({ .mutation(async function ({ ctx, input }) { console.info("api.user.creditCard.save start", JSON.stringify({})) const apiResponse = await api.post( - `${api.endpoints.v1.creditCards}/${input.transactionId}`, + api.endpoints.v1.Profile.CreditCards.transaction(input.transactionId), { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -118,7 +121,9 @@ export const userMutationRouter = router({ JSON.stringify({ query: {} }) ) const apiResponse = await api.remove( - `${api.endpoints.v1.creditCards}/${input.creditCardId}`, + api.endpoints.v1.Profile.CreditCards.deleteCreditCard( + input.creditCardId + ), { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -149,7 +154,7 @@ export const userMutationRouter = router({ ctx, }) { generatePreferencesLinkCounter.add(1) - const apiResponse = await api.get(api.endpoints.v1.subscriberId, { + const apiResponse = await api.get(api.endpoints.v1.Profile.subscriberId, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 224f03198..a0d7face0 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -89,7 +89,7 @@ export const getVerifiedUser = cache( } getVerifiedUserCounter.add(1) console.info("api.user.profile getVerifiedUser start", JSON.stringify({})) - const apiResponse = await api.get(api.endpoints.v1.profile, { + const apiResponse = await api.get(api.endpoints.v1.Profile.profile, { headers: { Authorization: `Bearer ${session.token.access_token}`, }, @@ -163,7 +163,7 @@ export const getVerifiedUser = cache( } ) -function parsedUser(data: User, isMFA: boolean) { +export function parsedUser(data: User, isMFA: boolean) { const country = countries.find((c) => c.code === data.address.countryCode) const user = { @@ -211,7 +211,7 @@ function parsedUser(data: User, isMFA: boolean) { async function getCreditCards(session: Session) { getCreditCardsCounter.add(1) console.info("api.profile.creditCards start", JSON.stringify({})) - const apiResponse = await api.get(api.endpoints.v1.creditCards, { + const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, { headers: { Authorization: `Bearer ${session.token.access_token}`, }, @@ -354,7 +354,7 @@ export const userQueryRouter = router({ JSON.stringify({ query: { params } }) ) const previousStaysResponse = await api.get( - api.endpoints.v1.previousStays, + api.endpoints.v1.Booking.Stays.past, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -430,7 +430,7 @@ export const userQueryRouter = router({ ) const apiResponse = await api.get( - api.endpoints.v1.previousStays, + api.endpoints.v1.Booking.Stays.past, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -492,7 +492,7 @@ export const userQueryRouter = router({ ) const nextCursor = verifiedData.data.links && - verifiedData.data.links.offset < verifiedData.data.links.totalCount + verifiedData.data.links.offset < verifiedData.data.links.totalCount ? verifiedData.data.links.offset : undefined @@ -525,7 +525,7 @@ export const userQueryRouter = router({ JSON.stringify({ query: { params } }) ) const apiResponse = await api.get( - api.endpoints.v1.upcomingStays, + api.endpoints.v1.Booking.Stays.future, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -585,7 +585,7 @@ export const userQueryRouter = router({ }) const nextCursor = verifiedData.data.links && - verifiedData.data.links.offset < verifiedData.data.links.totalCount + verifiedData.data.links.offset < verifiedData.data.links.totalCount ? verifiedData.data.links.offset : undefined @@ -611,13 +611,16 @@ export const userQueryRouter = router({ "api.transaction.friendTransactions start", JSON.stringify({}) ) - const apiResponse = await api.get(api.endpoints.v1.friendTransactions, { - cache: undefined, // override defaultOptions - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - next: { revalidate: 30 * 60 * 1000 }, - }) + const apiResponse = await api.get( + api.endpoints.v1.Profile.Transaction.friendTransactions, + { + cache: undefined, // override defaultOptions + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + next: { revalidate: 30 * 60 * 1000 }, + } + ) if (!apiResponse.ok) { // switch (apiResponse.status) { @@ -740,7 +743,7 @@ export const userQueryRouter = router({ membershipCards: protectedProcedure.query(async function ({ ctx }) { getProfileCounter.add(1) console.info("api.profile start", JSON.stringify({})) - const apiResponse = await api.get(api.endpoints.v1.profile, { + const apiResponse = await api.get(api.endpoints.v1.Profile.profile, { cache: "no-store", headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, diff --git a/server/routers/user/utils.ts b/server/routers/user/utils.ts index cde50270f..c720195b1 100644 --- a/server/routers/user/utils.ts +++ b/server/routers/user/utils.ts @@ -35,7 +35,7 @@ async function updateStaysBookingUrl( // Temporary API call needed till we have user name in ctx session data getProfileCounter.add(1) console.info("api.user.profile updatebookingurl start", JSON.stringify({})) - const apiResponse = await api.get(api.endpoints.v1.profile, { + const apiResponse = await api.get(api.endpoints.v1.Profile.profile, { cache: "no-store", headers: { Authorization: `Bearer ${token}`, diff --git a/server/trpc.ts b/server/trpc.ts index 0079a1ff6..688ea01cf 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -176,7 +176,7 @@ export const protectedServerActionProcedure = serverActionProcedure.use( } ) -// NOTE: This is actually save to use, just the implementation could change +// NOTE: This is actually safe to use, just the implementation could change // in minor version bumps. Please read: https://trpc.io/docs/faq#unstable export const contentStackUidWithServiceProcedure = contentstackExtendedProcedureUID.unstable_concat(serviceProcedure) @@ -186,3 +186,6 @@ export const contentStackBaseWithServiceProcedure = export const contentStackBaseWithProtectedProcedure = contentstackBaseProcedure.unstable_concat(protectedProcedure) + +export const safeProtectedServiceProcedure = + safeProtectedProcedure.unstable_concat(serviceProcedure) diff --git a/stores/enter-details.ts b/stores/enter-details.ts index d4d2666d1..b5c862d36 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -3,21 +3,22 @@ import { createContext, useContext } from "react" import { create, useStore } from "zustand" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" -import { breakfastSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" +import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" +import { BreakfastPackage } from "@/types/components/enterDetails/breakfast" import { DetailsSchema } from "@/types/components/enterDetails/details" import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" import { StepEnum } from "@/types/components/enterDetails/step" -import { bedTypeEnum } from "@/types/enums/bedType" -import { breakfastEnum } from "@/types/enums/breakfast" +import { BedTypeEnum } from "@/types/enums/bedType" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" const SESSION_STORAGE_KEY = "enterDetails" interface EnterDetailsState { data: { - bedType: bedTypeEnum | undefined - breakfast: breakfastEnum | undefined + bedType: BedTypeEnum | undefined + breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined } & DetailsSchema steps: StepEnum[] currentStep: StepEnum @@ -26,7 +27,7 @@ interface EnterDetailsState { completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void navigate: ( step: StepEnum, - updatedData?: Record<string, string | boolean> + updatedData?: Record<string, string | boolean | BreakfastPackage> ) => void setCurrentStep: (step: StepEnum) => void openSidePeek: (key: SidePeekEnum | null) => void @@ -75,7 +76,7 @@ export function initEditDetailsState(currentStep: StepEnum) { initialData = { ...initialData, ...validatedBedType.data } isValid[StepEnum.selectBed] = true } - const validatedBreakfast = breakfastSchema.safeParse(inputData) + const validatedBreakfast = breakfastStoreSchema.safeParse(inputData) if (validatedBreakfast.success) { validPaths.push(StepEnum.details) initialData = { ...initialData, ...validatedBreakfast.data } diff --git a/types/components/enterDetails/breakfast.ts b/types/components/enterDetails/breakfast.ts index 868bc96a1..21ba37bd0 100644 --- a/types/components/enterDetails/breakfast.ts +++ b/types/components/enterDetails/breakfast.ts @@ -1,5 +1,21 @@ import { z } from "zod" -import { breakfastSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" +import { + breakfastPackageSchema, + breakfastPackagesSchema, +} from "@/server/routers/hotels/output" -export interface BreakfastSchema extends z.output<typeof breakfastSchema> {} +import { breakfastFormSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" + +export interface BreakfastFormSchema + extends z.output<typeof breakfastFormSchema> {} + +export interface BreakfastPackages + extends z.output<typeof breakfastPackagesSchema> {} + +export interface BreakfastPackage + extends z.output<typeof breakfastPackageSchema> {} + +export interface BreakfastProps { + packages: BreakfastPackages | null +} diff --git a/types/enums/bedType.ts b/types/enums/bedType.ts index 0b4ba284d..2feb6d980 100644 --- a/types/enums/bedType.ts +++ b/types/enums/bedType.ts @@ -1,4 +1,4 @@ -export enum bedTypeEnum { +export enum BedTypeEnum { KING = "KING", QUEEN = "QUEEN", } diff --git a/types/enums/breakfast.ts b/types/enums/breakfast.ts index 567db2860..81ff51a2e 100644 --- a/types/enums/breakfast.ts +++ b/types/enums/breakfast.ts @@ -1,4 +1,5 @@ -export enum breakfastEnum { - BREAKFAST = "BREAKFAST", +export enum BreakfastPackageEnum { + FREE_MEMBER_BREAKFAST = "BRF0", + REGULAR_BREAKFAST = "BRF1", NO_BREAKFAST = "NO_BREAKFAST", } diff --git a/types/enums/currency.ts b/types/enums/currency.ts new file mode 100644 index 000000000..c04ed8450 --- /dev/null +++ b/types/enums/currency.ts @@ -0,0 +1,7 @@ +export enum CurrencyEnum { + DKK = "DKK", + EUR = "EUR", + NOK = "NOK", + PLN = "PLN", + SEK = "SEK", +} diff --git a/types/enums/packages.ts b/types/enums/packages.ts new file mode 100644 index 000000000..f030ccaab --- /dev/null +++ b/types/enums/packages.ts @@ -0,0 +1,7 @@ +export enum PackageTypeEnum { + AccessibleFriendlyRoom = "AccessibleFriendlyRoom", + AllergyRoom = "AllergyRoom", + BreakfastAdult = "BreakfastAdult", + BreakfastChildren = "BreakfastChildren", + PetRoom = "PetRoom", +} From bbcaa1cbf4c450669c969ae04f254f8cfcca491b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= <matilda.landstrom@scandichotels.com> Date: Tue, 29 Oct 2024 14:43:41 +0100 Subject: [PATCH 41/49] fix: hide empty hotel faq --- components/ContentType/HotelPage/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index f5718a7f2..8eb441e01 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -70,7 +70,7 @@ export default async function HotelPage() { <TabNavigation restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)} hasActivities={!!activitiesCard} - hasFAQ={!!faq} + hasFAQ={!!faq.accordions.length} /> <main className={styles.mainSection}> <div id={HotelHashValues.overview} className={styles.overview}> @@ -100,7 +100,7 @@ export default async function HotelPage() { </div> <Rooms rooms={roomCategories} /> <Facilities facilities={facilities} activitiesCard={activitiesCard} /> - {faq && ( + {faq.accordions.length > 0 && ( <AccordionSection accordion={faq.accordions} title={faq.title} /> )} </main> From eff0d122cd94b7b9f32e9f86564ea760933ff4e9 Mon Sep 17 00:00:00 2001 From: Bianca Widstam <bianca.widstam@scandichotels.com> Date: Wed, 30 Oct 2024 09:34:54 +0000 Subject: [PATCH 42/49] feat/SW-711-update-children-params (pull request #791) Feat/SW-711 update children params * feat(SW-711): add new child params for availability * feat(SW-711): fix children schema * feat(SW-711): fix optional values * feat(SW-711): add children as parameter iff not undefined * feat(SW-711): add bedType enum * feat(SW-711): remove optional number type * feat(SW-711): fix wrong slash * feat(SW-711): remove optional Approved-by: Hrishikesh Vaipurkar --- .../(standard)/select-hotel/page.tsx | 5 ++++- .../(standard)/select-hotel/utils.ts | 18 ++++++++++++++++++ .../(standard)/select-rate/page.tsx | 11 ++++++++--- .../RoomSelection/RateSummary/index.tsx | 4 ++-- server/routers/hotels/input.ts | 4 ++-- server/routers/hotels/output.ts | 8 +++++++- server/routers/hotels/query.ts | 5 ++--- .../selectHotel/availabilityInput.ts | 2 +- .../hotelReservation/selectRate/selectRate.ts | 2 +- 9 files changed, 45 insertions(+), 14 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 1591e811b..36fbd59c7 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -5,6 +5,7 @@ import { getLocations } from "@/lib/trpc/memoizedRequests" import { fetchAvailableHotels, + generateChildrenString, getFiltersFromHotels, } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" @@ -42,7 +43,9 @@ export default async function SelectHotelPage({ const selectHotelParamsObject = getHotelReservationQueryParams(selectHotelParams) const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selectHotelParamsObject.room[0].child?.length // TODO: Handle multiple rooms + const children = selectHotelParamsObject.room[0].child + ? generateChildrenString(selectHotelParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms const hotels = await fetchAvailableHotels({ cityId: city.id, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index 8dfc76a1d..a6a48e12f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -2,9 +2,11 @@ import { serverClient } from "@/lib/trpc/server" import { getLang } from "@/i18n/serverContext" +import { BedTypeEnum } from "@/types/components/bookingWidget/enums" import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput" import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters" +import { Child } from "@/types/components/hotelReservation/selectRate/selectRate" export async function fetchAvailableHotels( input: AvailabilityInput @@ -41,3 +43,19 @@ export function getFiltersFromHotels(hotels: HotelData[]) { return filterList } + +const bedTypeMap: Record<number, string> = { + [BedTypeEnum.IN_ADULTS_BED]: "ParentsBed", + [BedTypeEnum.IN_CRIB]: "Crib", + [BedTypeEnum.IN_EXTRA_BED]: "ExtraBed", +} + +export function generateChildrenString(children: Child[]): string { + return `[${children + ?.map((child) => { + const age = child.age + const bedType = bedTypeMap[+child.bed] + return `${age}:${bedType}` + }) + .join(",")}]` +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index b361aaa10..35a940f0b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -8,6 +8,8 @@ import Rooms from "@/components/HotelReservation/SelectRate/Rooms" import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" +import { generateChildrenString } from "../select-hotel/utils" + import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" @@ -27,7 +29,10 @@ export default async function SelectRatePage({ } const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms + const childrenCount = selectRoomParamsObject.room[0].child?.length + const children = selectRoomParamsObject.room[0].child + ? generateChildrenString(selectRoomParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms const [hotelData, roomsAvailability, packages, user] = await Promise.all([ serverClient().hotel.hotelData.get({ @@ -46,8 +51,8 @@ export default async function SelectRatePage({ hotelId: searchParams.hotel, startDate: searchParams.fromDate, endDate: searchParams.toDate, - adults: adults, - children: children, + adults, + children: childrenCount, packageCodes: [ RoomPackageCodeEnum.ACCESSIBILITY_ROOM, RoomPackageCodeEnum.PET_ROOM, diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index b7ecc3c63..827a1d280 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -82,12 +82,12 @@ export default function RateSummary({ { id: "booking.adults" }, { totalAdults: roomsAvailability.occupancy?.adults } )} - {roomsAvailability.occupancy?.children && ( + {roomsAvailability.occupancy?.children?.length && ( <> ,{" "} {intl.formatMessage( { id: "booking.children" }, - { totalChildren: roomsAvailability.occupancy.children } + { totalChildren: roomsAvailability.occupancy.children.length } )} </> )} diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 61927c3d2..194a16496 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -11,7 +11,7 @@ export const getHotelsAvailabilityInputSchema = z.object({ roomStayStartDate: z.string(), roomStayEndDate: z.string(), adults: z.number(), - children: z.number().optional().default(0), + children: z.string().optional(), promotionCode: z.string().optional().default(""), reservationProfileType: z.string().optional().default(""), attachedProfileId: z.string().optional().default(""), @@ -22,7 +22,7 @@ export const getRoomsAvailabilityInputSchema = z.object({ roomStayStartDate: z.string(), roomStayEndDate: z.string(), adults: z.number(), - children: z.number().optional().default(0), + children: z.string().optional(), promotionCode: z.string().optional(), reservationProfileType: z.string().optional().default(""), attachedProfileId: z.string().optional().default(""), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index f8e2b8a6e..84ab77171 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -1,5 +1,6 @@ import { z } from "zod" +import { BedTypeEnum } from "@/constants/booking" import { dt } from "@/lib/dt" import { toLang } from "@/server/utils" @@ -458,9 +459,14 @@ export const getHotelDataSchema = z.object({ included: z.array(roomSchema).optional(), }) +export const childrenSchema = z.object({ + age: z.number(), + bedType: z.nativeEnum(BedTypeEnum), +}) + const occupancySchema = z.object({ adults: z.number(), - children: z.number(), + children: z.array(childrenSchema), }) const bestPricePerStaySchema = z.object({ diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index a7b6c220e..c783d0be5 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -312,13 +312,12 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, - children, + ...(children && { children }), promotionCode, reservationProfileType, attachedProfileId, language: apiLang, } - hotelsAvailabilityCounter.add(1, { cityId, roomStayStartDate, @@ -437,7 +436,7 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, - children, + ...(children && { children }), promotionCode, reservationProfileType, attachedProfileId, diff --git a/types/components/hotelReservation/selectHotel/availabilityInput.ts b/types/components/hotelReservation/selectHotel/availabilityInput.ts index ff25984b9..5b3a51b93 100644 --- a/types/components/hotelReservation/selectHotel/availabilityInput.ts +++ b/types/components/hotelReservation/selectHotel/availabilityInput.ts @@ -3,7 +3,7 @@ export type AvailabilityInput = { roomStayStartDate: string roomStayEndDate: string adults: number - children?: number + children?: string promotionCode?: string reservationProfileType?: string attachedProfileId?: string diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index b22c010c0..553d09827 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -1,6 +1,6 @@ import { Product, RoomConfiguration } from "@/server/routers/hotels/output" -interface Child { +export interface Child { bed: string age: number } From 6523e2329b2792f2d8f3fb7425e44e73dc97e403 Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Wed, 23 Oct 2024 09:57:45 +0200 Subject: [PATCH 43/49] feat(SW-614): added i18n strings --- i18n/dictionaries/da.json | 9 +++++++++ i18n/dictionaries/de.json | 11 +++++++++-- i18n/dictionaries/en.json | 9 +++++++++ i18n/dictionaries/fi.json | 11 +++++++++-- i18n/dictionaries/no.json | 13 +++++++++++-- i18n/dictionaries/sv.json | 13 +++++++++++-- 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index c66448f17..cbfec3e69 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -68,6 +68,7 @@ "Coming up": "Er lige om hjørnet", "Compare all levels": "Sammenlign alle niveauer", "Complete booking & go to payment": "Udfyld booking & gå til betaling", + "Complete the booking": "Fuldfør bookingen", "Contact information": "Kontaktoplysninger", "Contact us": "Kontakt os", "Continue": "Blive ved", @@ -125,6 +126,7 @@ "Go back to edit": "Gå tilbage til redigering", "Go back to overview": "Gå tilbage til oversigten", "Go to My Benefits": "Gå til ‘Mine fordele’", + "Guarantee booking with credit card": "Garantere booking med kreditkort", "Guest information": "Gæsteinformation", "Guests & Rooms": "Gæster & værelser", "Hi": "Hei", @@ -227,6 +229,7 @@ "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Vær opmærksom på, at dette er påkrævet, og at dit kort kun vil blive opkrævet i tilfælde af en no-show.", "Points": "Point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", @@ -237,6 +240,7 @@ "Previous victories": "Tidligere sejre", "Proceed to login": "Fortsæt til login", "Proceed to payment method": "Fortsæt til betalingsmetode", + "Provide a payment card in the next step": "Giv os dine betalingsoplysninger i næste skridt", "Public price from": "Offentlig pris fra", "Public transport": "Offentlig transport", "Queen bed": "Queensize-seng", @@ -302,6 +306,7 @@ "Theatre": "Teater", "There are no transactions to display": "Der er ingen transaktioner at vise", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", "Total Points": "Samlet antal point", "Total incl VAT": "Inkl. moms", "Total price": "Samlet pris", @@ -330,7 +335,9 @@ "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Motion", + "What you have to do to guarantee booking": "Hvad du skal gøre for at garantere booking", "When": "Hvornår", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din booking, vil vi holde bookingen indtil 07:00 til dagen efter check-in. Dette vil give dig som gæst tilføjet fleksibilitet til check-in-tider.", "Where should you go next?": "Find inspiration til dit næste ophold", "Where to": "Hvor", "Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig", @@ -369,6 +376,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/nat", "by": "inden", "characters": "tegn", + "guaranteeing": "garanti", "guest": "gæst", "guests": "gæster", "hotelPages.rooms.roomCard.person": "person", @@ -382,6 +390,7 @@ "nights": "nætter", "number": "nummer", "or": "eller", + "paying": "betaler ", "points": "Point", "room type": "værelsestype", "room types": "værelsestyper", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 1959939ee..901d47949 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -68,6 +68,7 @@ "Coming up": "Demnächst", "Compare all levels": "Vergleichen Sie alle Levels", "Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen", + "Complete the booking": "Buchung abschließen", "Contact information": "Kontaktinformationen", "Contact us": "Kontaktieren Sie uns", "Continue": "Weitermachen", @@ -125,6 +126,7 @@ "Go back to edit": "Zurück zum Bearbeiten", "Go back to overview": "Zurück zur Übersicht", "Go to My Benefits": "Gehen Sie zu „Meine Vorteile“", + "Guarantee booking with credit card": "Buchung mit Kreditkarte garantieren", "Guest information": "Informationen für Gäste", "Guests & Rooms": "Gäste & Zimmer", "Hi": "Hallo", @@ -179,8 +181,6 @@ "Membership ID": "Mitglieds-ID", "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", "Membership cards": "Mitgliedskarten", - "Membership terms and conditions": "Mitgliedschaftsbedingungen", - "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", "Museum": "Museum", @@ -227,6 +227,7 @@ "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Bitte beachten Sie, dass dies erforderlich ist und dass Ihr Kreditkartenkonto nur in einem No-Show-Fall belastet wird.", "Points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", @@ -237,6 +238,7 @@ "Previous victories": "Bisherige Siege", "Proceed to login": "Weiter zum Login", "Proceed to payment method": "Weiter zur Zahlungsmethode", + "Provide a payment card in the next step": "Geben Sie Ihre Zahlungskarteninformationen im nächsten Schritt an", "Public price from": "Öffentlicher Preis ab", "Public transport": "Öffentliche Verkehrsmittel", "Queen bed": "Queensize-Bett", @@ -303,6 +305,7 @@ "Theatre": "Theater", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", "Total Points": "Gesamtpunktzahl", "Total incl VAT": "Gesamt inkl. MwSt.", "Total price": "Gesamtpreis", @@ -331,7 +334,9 @@ "Welcome": "Willkommen", "Welcome to": "Willkommen zu", "Wellness & Exercise": "Wellness & Bewegung", + "What you have to do to guarantee booking": "Was Sie tun müssen, um eine Buchung zu garantieren", "When": "Wann", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Wenn Sie Ihre Buchung garantieren, halten wir die Buchung bis 07:00 am Tag nach dem Check-in. Dies wird Ihnen als Gast zusätzliche Flexibilität für die Check-in-Zeiten gewähren.", "Where should you go next?": "Wo geht es als Nächstes hin?", "Where to": "Wohin", "Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?", @@ -370,6 +375,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/Nacht", "by": "bis", "characters": "figuren", + "guaranteeing": "garantiert", "guest": "gast", "guests": "gäste", "hotelPages.rooms.roomCard.person": "person", @@ -383,6 +389,7 @@ "nights": "Nächte", "number": "nummer", "or": "oder", + "paying": "bezahlt", "points": "Punkte", "room type": "zimmerart", "room types": "zimmerarten", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 2fa6793d7..fb7daa428 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -74,6 +74,7 @@ "Coming up": "Coming up", "Compare all levels": "Compare all levels", "Complete booking & go to payment": "Complete booking & go to payment", + "Complete the booking": "Complete the booking", "Contact information": "Contact information", "Contact us": "Contact us", "Continue": "Continue", @@ -134,6 +135,7 @@ "Go back to overview": "Go back to overview", "Go to My Benefits": "Go to My Benefits", "Guest": "Guest", + "Guarantee booking with credit card": "Guarantee booking with credit card", "Guest information": "Guest information", "Guests & Rooms": "Guests & Rooms", "Hi": "Hi", @@ -237,6 +239,7 @@ "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.", "Points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", @@ -248,6 +251,7 @@ "Print confirmation": "Print confirmation", "Proceed to login": "Proceed to login", "Proceed to payment method": "Proceed to payment method", + "Provide a payment card in the next step": "Provide a payment card in the next step", "Public price from": "Public price from", "Public transport": "Public transport", "Queen bed": "Queen bed", @@ -318,6 +322,7 @@ "Things nearby HOTEL_NAME": "Things nearby {hotelName}", "Total Points": "Total Points", "Total cost": "Total cost", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", "Total incl VAT": "Total incl VAT", "Total price": "Total price", "Tourist": "Tourist", @@ -346,7 +351,9 @@ "Welcome": "Welcome", "Welcome to": "Welcome to", "Wellness & Exercise": "Wellness & Exercise", + "What you have to do to guarantee booking": "What you have to do to guarantee booking", "When": "When", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.", "Where should you go next?": "Where should you go next?", "Where to": "Where to", "Which room class suits you the best?": "Which room class suits you the best?", @@ -389,6 +396,7 @@ "by": "by", "characters": "characters", "from": "from", + "guaranteeing": "guaranteeing", "guest": "guest", "guests": "guests", "hotelPages.rooms.roomCard.person": "person", @@ -403,6 +411,7 @@ "nights": "nights", "number": "number", "or": "or", + "paying": "paying", "points": "Points", "room type": "room type", "room types": "room types", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 883b9d277..6ae282cd1 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -68,6 +68,7 @@ "Coming up": "Tulossa", "Compare all levels": "Vertaa kaikkia tasoja", "Complete booking & go to payment": "Täydennä varaus & siirry maksamaan", + "Complete the booking": "Täydennä varaus", "Contact information": "Yhteystiedot", "Contact us": "Ota meihin yhteyttä", "Continue": "Jatkaa", @@ -125,6 +126,7 @@ "Go back to edit": "Palaa muokkaamaan", "Go back to overview": "Palaa yleiskatsaukseen", "Go to My Benefits": "Siirry kohtaan ‘Omat edut’", + "Guarantee booking with credit card": "Varmista varaus luottokortilla", "Guest information": "Vieraan tiedot", "Guests & Rooms": "Vieraat & Huoneet", "Hi": "Hi", @@ -227,6 +229,7 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Huomaa, että tämä on pakollinen, ja että maksukorttiisi kirjataan vain, jos varausmyyntiä ei tapahtu.", "Points": "Pisteet", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", @@ -237,6 +240,7 @@ "Previous victories": "Edelliset voitot", "Proceed to login": "Jatka kirjautumiseen", "Proceed to payment method": "Siirry maksutavalle", + "Provide a payment card in the next step": "Anna maksukortin tiedot seuraavassa vaiheessa", "Public price from": "Julkinen hinta alkaen", "Public transport": "Julkinen liikenne", "Queen bed": "Queen-vuode", @@ -303,6 +307,7 @@ "Theatre": "Teatteri", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", "Total Points": "Kokonaispisteet", "Total incl VAT": "Yhteensä sis. alv", "Total price": "Kokonaishinta", @@ -331,7 +336,9 @@ "Welcome": "Tervetuloa", "Welcome to": "Tervetuloa", "Wellness & Exercise": "Hyvinvointi & Liikunta", + "What you have to do to guarantee booking": "Mitä sinun on tehtävä varmistaaksesi varauksen", "When": "Kun", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Jos varaat varauksen, pidämme varauksen 07:00 päivän jälkeen tarkistuspäivän jälkeen. Tämä tarjoaa sinulle lisään tarkistuspäivän aikaan.", "Where should you go next?": "Mihin menisit seuraavaksi?", "Where to": "Minne", "Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?", @@ -344,8 +351,6 @@ "You have <b>#</b> gifts waiting for you!": "Sinulla on <b>{amount}</b> lahjaa odottamassa sinua!", "You have no previous stays.": "Sinulla ei ole aiempia majoituksia.", "You have no upcoming stays.": "Sinulla ei ole tulevia majoituksia.", - "You'll find all your gifts in 'My benefits'": "Löydät kaikki lahjasi kohdasta ‘Omat edut’", - "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!", "Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!", "Your current level": "Nykyinen tasosi", @@ -370,6 +375,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/yö", "by": "mennessä", "characters": "hahmoja", + "guaranteeing": "varmistetaan", "guest": "Vieras", "guests": "Vieraita", "hotelPages.rooms.roomCard.person": "henkilö", @@ -383,6 +389,7 @@ "nights": "yötä", "number": "määrä", "or": "tai", + "paying": "maksaa", "points": "pistettä", "room type": "huonetyyppi", "room types": "huonetyypit", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 075f84783..5041708dc 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -68,6 +68,7 @@ "Coming up": "Kommer opp", "Compare all levels": "Sammenlign alle nivåer", "Complete booking & go to payment": "Fullfør bestilling & gå til betaling", + "Complete the booking": "Fullfør reservasjonen", "Contact information": "Kontaktinformasjon", "Contact us": "Kontakt oss", "Continue": "Fortsette", @@ -124,6 +125,7 @@ "Go back to edit": "Gå tilbake til redigering", "Go back to overview": "Gå tilbake til oversikten", "Go to My Benefits": "Gå til ‘Mine fordeler’", + "Guarantee booking with credit card": "Garantere booking med kredittkort", "Guest information": "Informasjon til gjester", "Guests & Rooms": "Gjester & rom", "Hi": "Hei", @@ -225,6 +227,7 @@ "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Vær oppmerksom på at dette er påkrevd, og at ditt kredittkort kun vil bli belastet i tilfelle av en no-show.", "Points": "Poeng", "Points being calculated": "Poeng beregnes", "Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021", @@ -235,6 +238,7 @@ "Previous victories": "Tidligere seire", "Proceed to login": "Fortsett til innlogging", "Proceed to payment method": "Fortsett til betalingsmetode", + "Provide a payment card in the next step": "Gi oss dine betalingskortdetaljer i neste steg", "Public price from": "Offentlig pris fra", "Public transport": "Offentlig transport", "Queen bed": "Queen-size-seng", @@ -300,9 +304,10 @@ "Theatre": "Teater", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", + "Total price": "Totalpris", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.", "Total Points": "Totale poeng", "Total incl VAT": "Sum inkl mva", - "Total price": "Totalpris", "Tourist": "Turist", "Transaction date": "Transaksjonsdato", "Transactions": "Transaksjoner", @@ -328,7 +333,9 @@ "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Trening", + "What you have to do to guarantee booking": "Hva du må gjøre for å garantere reservasjonen", "When": "Når", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din reservasjon, vil vi holde reservasjonen til 07:00 til dagen etter check-in. Dette vil gi deg som gjest tilføjet fleksibilitet for check-in-tider.", "Where should you go next?": "Hvor ønsker du å reise neste gang?", "Where to": "Hvor skal du", "Which room class suits you the best?": "Hvilken romklasse passer deg best?", @@ -366,6 +373,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt", "by": "innen", "characters": "tegn", + "guaranteeing": "garantiert", "guest": "gjest", "guests": "gjester", "hotelPages.rooms.roomCard.person": "person", @@ -379,9 +387,10 @@ "nights": "netter", "number": "antall", "or": "eller", - "points": "poeng", "room type": "romtype", "room types": "romtyper", + "paying": "betaler", + "points": "poeng", "special character": "spesiell karakter", "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "to": "til", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 0b48022d3..8f3f0eeda 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -68,6 +68,7 @@ "Coming up": "Kommer härnäst", "Compare all levels": "Jämför alla nivåer", "Complete booking & go to payment": "Fullför bokning & gå till betalning", + "Complete the booking": "Slutför bokningen", "Contact information": "Kontaktinformation", "Contact us": "Kontakta oss", "Continue": "Fortsätt", @@ -124,6 +125,7 @@ "Go back to edit": "Gå tillbaka till redigeringen", "Go back to overview": "Gå tillbaka till översikten", "Go to My Benefits": "Gå till ‘Mina förmåner’", + "Guarantee booking with credit card": "Garantera bokning med kreditkort", "Guest information": "Information till gästerna", "Guests & Rooms": "Gäster & rum", "Hi": "Hej", @@ -225,6 +227,7 @@ "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Vänligen notera att detta är obligatoriskt, och att ditt kreditkort endast debiteras i händelse av en no-show.", "Points": "Poäng", "Points being calculated": "Poäng beräknas", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", @@ -235,6 +238,7 @@ "Previous victories": "Tidigare segrar", "Proceed to login": "Fortsätt till inloggning", "Proceed to payment method": "Gå vidare till betalningsmetod", + "Provide a payment card in the next step": "Ge oss dina betalkortdetaljer i nästa steg", "Public price from": "Offentligt pris från", "Public transport": "Kollektivtrafik", "Queen bed": "Queen size-säng", @@ -300,9 +304,10 @@ "Theatre": "Teater", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", + "Total price": "Totalpris", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.", "Total Points": "Poäng totalt", "Total incl VAT": "Totalt inkl moms", - "Total price": "Totalpris", "Tourist": "Turist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktioner", @@ -328,7 +333,9 @@ "Welcome": "Välkommen", "Welcome to": "Välkommen till", "Wellness & Exercise": "Hälsa & Träning", + "What you have to do to guarantee booking": "Vad du måste göra för att garantera bokningen", "When": "När", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "När du garanterar din bokning kommer vi att hålla bokningen till 07:00 till dagen efter check-in. Detta ger dig som gäst extra flexibilitet för check-in-tider.", "Where should you go next?": "Låter inte en spontanweekend härligt?", "Where to": "Vart", "Which room class suits you the best?": "Vilken rumsklass passar dig bäst?", @@ -367,6 +374,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt", "by": "innan", "characters": "tecken", + "guaranteeing": "garanterar", "guest": "gäst", "guests": "gäster", "hotelPages.rooms.roomCard.person": "person", @@ -388,8 +396,9 @@ "to": "till", "type": "typ", "types": "typer", - "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", + "paying": "betalar", + "uppercase letter": "stor bokstav", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" From a690750c361b54ba9c1c91ac0bad3bc4a8dc8c29 Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Wed, 23 Oct 2024 13:22:49 +0200 Subject: [PATCH 44/49] feat(SW-614): Add mustBeGuaranteed flag and update content based on this --- .../(standard)/[step]/page.tsx | 30 ++++++++--- .../guaranteeDetails.module.css | 26 ++++++++++ .../Payment/GuaranteeDetails/index.tsx | 50 +++++++++++++++++++ .../EnterDetails/Payment/index.tsx | 17 +++++++ i18n/dictionaries/da.json | 2 +- i18n/dictionaries/de.json | 2 +- i18n/dictionaries/en.json | 2 +- i18n/dictionaries/fi.json | 2 +- i18n/dictionaries/no.json | 2 +- i18n/dictionaries/sv.json | 2 +- lib/trpc/memoizedRequests/index.ts | 20 ++++++++ .../hotelReservation/selectRate/section.ts | 1 + 12 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css create mode 100644 components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 77ec20203..45191398d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -5,6 +5,7 @@ import { getCreditCardsSafely, getHotelData, getProfileSafely, + getRoomAvailability, } from "@/lib/trpc/memoizedRequests" import BedType from "@/components/HotelReservation/EnterDetails/BedType" @@ -21,6 +22,7 @@ import type { LangParams, PageArgs } from "@/types/params" export function preload() { void getProfileSafely() void getCreditCardsSafely() + void getRoomAvailability("811", 1, "2024-11-01", "2024-11-02") } function isValidStep(step: string): step is StepEnum { @@ -43,10 +45,19 @@ export default async function StepPage({ const savedCreditCards = await getCreditCardsSafely() const breakfastPackages = await getBreakfastPackages(searchParams.hotel) - if (!isValidStep(params.step) || !hotel) { + const roomAvailability = await getRoomAvailability( + searchParams.hotel, + Number(searchParams.adults), + searchParams.checkIn, + searchParams.checkOut + ) + + if (!isValidStep(params.step) || !hotel || !roomAvailability) { return notFound() } + const mustBeGuaranteed = false + return ( <section> <HistoryStateManager /> @@ -72,17 +83,24 @@ export default async function StepPage({ <Details user={user} /> </SectionAccordion> <SectionAccordion - header="Payment" + header={mustBeGuaranteed ? "Payment Guarantee" : "Payment"} step={StepEnum.payment} - label={intl.formatMessage({ id: "Select payment method" })} + label={ + mustBeGuaranteed + ? intl.formatMessage({ id: "Guarantee booking with credit card" }) + : intl.formatMessage({ id: "Select payment method" }) + } > <Payment - hotelId={hotel.data.attributes.operaId} + hotelId={searchParams.hotel} otherPaymentOptions={ - hotel.data.attributes.merchantInformationData - .alternatePaymentOptions + mustBeGuaranteed + ? [] + : hotel.data.attributes.merchantInformationData + .alternatePaymentOptions } savedCreditCards={savedCreditCards} + mustBeGuaranteed={mustBeGuaranteed} /> </SectionAccordion> </section> diff --git a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css new file mode 100644 index 000000000..a14bc3473 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css @@ -0,0 +1,26 @@ +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); + padding-top: var(--Spacing-x2); +} + +.content ol { + margin: 0; +} + +.summary { + list-style: none; + display: flex; + align-items: center; + gap: var(--Spacing-x-half); +} + +.summary::-webkit-details-marker, +.summary::marker { + display: none; +} + +.summary .icon { + height: 16px; +} diff --git a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx new file mode 100644 index 000000000..99021715a --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx @@ -0,0 +1,50 @@ +import { useIntl } from "react-intl" + +import ChevronDown from "@/components/Icons/ChevronDown" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./guaranteeDetails.module.css" + +export default function GuaranteeDetails() { + const intl = useIntl() + return ( + <details> + <Caption color="burgundy" type="bold" asChild> + <summary className={styles.summary}> + {intl.formatMessage({ id: "How it works" })} + <ChevronDown color="burgundy" className={styles.icon} /> + </summary> + </Caption> + <section className={styles.content}> + <Body> + {intl.formatMessage({ + id: "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.", + })} + </Body> + <Body> + {intl.formatMessage({ + id: "What you have to do to guarantee booking:", + })} + </Body> + <ol> + <Body asChild> + <li>{intl.formatMessage({ id: "Complete the booking" })}</li> + </Body> + <Body asChild> + <li> + {intl.formatMessage({ + id: "Provide a payment card in the next step", + })} + </li> + </Body> + </ol> + <Body> + {intl.formatMessage({ + id: "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.", + })} + </Body> + </section> + </details> + ) +} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index ba126c1f2..f31ea816b 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -30,6 +30,7 @@ import { toast } from "@/components/TempDesignSystem/Toasts" import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" import useLang from "@/hooks/useLang" +import GuaranteeDetails from "./GuaranteeDetails" import PaymentOption from "./PaymentOption" import { PaymentFormData, paymentSchema } from "./schema" @@ -48,6 +49,7 @@ export default function Payment({ hotelId, otherPaymentOptions, savedCreditCards, + mustBeGuaranteed, }: PaymentProps) { const router = useRouter() const lang = useLang() @@ -169,12 +171,26 @@ export default function Payment({ return <LoadingSpinner /> } + const paymentVerb = mustBeGuaranteed + ? intl.formatMessage({ id: "guaranteeing" }) + : intl.formatMessage({ id: "paying" }) + return ( <FormProvider {...methods}> <form className={styles.paymentContainer} onSubmit={methods.handleSubmit(handleSubmit)} > + {mustBeGuaranteed ? ( + <section className={styles.section}> + <Body> + {intl.formatMessage({ + id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", + })} + </Body> + <GuaranteeDetails /> + </section> + ) : null} {savedCreditCards?.length ? ( <section className={styles.section}> <Body color="uiTextHighContrast" textTransform="bold"> @@ -238,6 +254,7 @@ export default function Payment({ id: "booking.terms", }, { + paymentVerb, termsLink: (str) => ( <Link className={styles.link} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index cbfec3e69..5e0b2446f 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -335,7 +335,7 @@ "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Motion", - "What you have to do to guarantee booking": "Hvad du skal gøre for at garantere booking", + "What you have to do to guarantee booking:": "Hvad du skal gøre for at garantere booking:", "When": "Hvornår", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din booking, vil vi holde bookingen indtil 07:00 til dagen efter check-in. Dette vil give dig som gæst tilføjet fleksibilitet til check-in-tider.", "Where should you go next?": "Find inspiration til dit næste ophold", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 901d47949..aaf57329f 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -334,7 +334,7 @@ "Welcome": "Willkommen", "Welcome to": "Willkommen zu", "Wellness & Exercise": "Wellness & Bewegung", - "What you have to do to guarantee booking": "Was Sie tun müssen, um eine Buchung zu garantieren", + "What you have to do to guarantee booking:": "Was Sie tun müssen, um eine Buchung zu garantieren:", "When": "Wann", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Wenn Sie Ihre Buchung garantieren, halten wir die Buchung bis 07:00 am Tag nach dem Check-in. Dies wird Ihnen als Gast zusätzliche Flexibilität für die Check-in-Zeiten gewähren.", "Where should you go next?": "Wo geht es als Nächstes hin?", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index fb7daa428..24d888259 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -351,7 +351,7 @@ "Welcome": "Welcome", "Welcome to": "Welcome to", "Wellness & Exercise": "Wellness & Exercise", - "What you have to do to guarantee booking": "What you have to do to guarantee booking", + "What you have to do to guarantee booking:": "What you have to do to guarantee booking:", "When": "When", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.", "Where should you go next?": "Where should you go next?", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 6ae282cd1..fbd1682d6 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -336,7 +336,7 @@ "Welcome": "Tervetuloa", "Welcome to": "Tervetuloa", "Wellness & Exercise": "Hyvinvointi & Liikunta", - "What you have to do to guarantee booking": "Mitä sinun on tehtävä varmistaaksesi varauksen", + "What you have to do to guarantee booking:": "Mitä sinun on tehtävä varmistaaksesi varauksen:", "When": "Kun", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Jos varaat varauksen, pidämme varauksen 07:00 päivän jälkeen tarkistuspäivän jälkeen. Tämä tarjoaa sinulle lisään tarkistuspäivän aikaan.", "Where should you go next?": "Mihin menisit seuraavaksi?", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 5041708dc..c15bf403f 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -333,7 +333,7 @@ "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Trening", - "What you have to do to guarantee booking": "Hva du må gjøre for å garantere reservasjonen", + "What you have to do to guarantee booking:": "Hva du må gjøre for å garantere reservasjonen:", "When": "Når", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din reservasjon, vil vi holde reservasjonen til 07:00 til dagen etter check-in. Dette vil gi deg som gjest tilføjet fleksibilitet for check-in-tider.", "Where should you go next?": "Hvor ønsker du å reise neste gang?", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 8f3f0eeda..6ce4280fb 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -333,7 +333,7 @@ "Welcome": "Välkommen", "Welcome to": "Välkommen till", "Wellness & Exercise": "Hälsa & Träning", - "What you have to do to guarantee booking": "Vad du måste göra för att garantera bokningen", + "What you have to do to guarantee booking:": "Vad du måste göra för att garantera bokningen:", "When": "När", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "När du garanterar din bokning kommer vi att hålla bokningen till 07:00 till dagen efter check-in. Detta ger dig som gäst extra flexibilitet för check-in-tider.", "Where should you go next?": "Låter inte en spontanweekend härligt?", diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index e34f241ce..5aa915497 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -60,6 +60,26 @@ export const getHotelData = cache(async function getMemoizedHotelData( }) }) +export const getRoomAvailability = cache( + async function getMemoizedRoomAvailability( + hotelId: string, + adults: number, + roomStayStartDate: string, + roomStayEndDate: string, + children?: number, + promotionCode?: string + ) { + return serverClient().hotel.availability.rooms({ + hotelId: parseInt(hotelId), + adults, + roomStayStartDate, + roomStayEndDate, + children, + promotionCode, + }) + } +) + export const getFooter = cache(async function getMemoizedFooter() { return serverClient().contentstack.base.footer() }) diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index a02107caf..85dc2a55d 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -31,6 +31,7 @@ export interface PaymentProps { hotelId: string otherPaymentOptions: string[] savedCreditCards: CreditCard[] | null + mustBeGuaranteed: boolean } export interface SectionPageProps { From 7819db2bb213627d9267041e132a37215ab67282 Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Thu, 24 Oct 2024 14:45:25 +0200 Subject: [PATCH 45/49] fix(SW-614): update enter details strings --- .../(standard)/[step]/page.tsx | 27 ++++++++++++------- .../EnterDetails/Payment/index.tsx | 6 ++--- i18n/dictionaries/da.json | 5 ++++ i18n/dictionaries/de.json | 5 ++++ i18n/dictionaries/en.json | 7 ++++- i18n/dictionaries/fi.json | 5 ++++ i18n/dictionaries/no.json | 5 ++++ i18n/dictionaries/sv.json | 5 ++++ 8 files changed, 52 insertions(+), 13 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 45191398d..689a4d8de 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -58,38 +58,47 @@ export default async function StepPage({ const mustBeGuaranteed = false + const paymentGuarantee = intl.formatMessage({ + id: "Payment Guarantee", + }) + const payment = intl.formatMessage({ + id: "Payment", + }) + const guaranteeWithCard = intl.formatMessage({ + id: "Guarantee booking with credit card", + }) + const selectPaymentMethod = intl.formatMessage({ + id: "Select payment method", + }) + return ( <section> <HistoryStateManager /> <SectionAccordion - header="Select bed" + header={intl.formatMessage({ id: "Select bed" })} step={StepEnum.selectBed} label={intl.formatMessage({ id: "Request bedtype" })} > <BedType /> </SectionAccordion> <SectionAccordion - header="Food options" + header={intl.formatMessage({ id: "Food options" })} step={StepEnum.breakfast} label={intl.formatMessage({ id: "Select breakfast options" })} > <Breakfast packages={breakfastPackages} /> </SectionAccordion> <SectionAccordion - header="Details" + header={intl.formatMessage({ id: "Details" })} step={StepEnum.details} label={intl.formatMessage({ id: "Enter your details" })} > <Details user={user} /> </SectionAccordion> <SectionAccordion - header={mustBeGuaranteed ? "Payment Guarantee" : "Payment"} + header={mustBeGuaranteed ? paymentGuarantee : payment} step={StepEnum.payment} - label={ - mustBeGuaranteed - ? intl.formatMessage({ id: "Guarantee booking with credit card" }) - : intl.formatMessage({ id: "Select payment method" }) - } + label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} > <Payment hotelId={searchParams.hotel} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index f31ea816b..e29692993 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -171,9 +171,9 @@ export default function Payment({ return <LoadingSpinner /> } - const paymentVerb = mustBeGuaranteed - ? intl.formatMessage({ id: "guaranteeing" }) - : intl.formatMessage({ id: "paying" }) + const guaranteeing = intl.formatMessage({ id: "guaranteeing" }) + const paying = intl.formatMessage({ id: "paying" }) + const paymentVerb = mustBeGuaranteed ? guaranteeing : paying return ( <FormProvider {...methods}> diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 5e0b2446f..67755749a 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -86,6 +86,7 @@ "Description": "Beskrivelse", "Destination": "Destination", "Destinations & hotels": "Destinationer & hoteller", + "Details": "Detaljer", "Disabled booking options header": "Vi beklager", "Disabled booking options text": "Koder, checks og bonusnætter er endnu ikke tilgængelige på den nye hjemmeside.", "Discard changes": "Kassér ændringer", @@ -116,6 +117,7 @@ "First name": "Fornavn", "Flexibility": "Fleksibilitet", "Follow us": "Følg os", + "Food options": "Madvalg", "Former Scandic Hotel": "Tidligere Scandic Hotel", "Free cancellation": "Gratis afbestilling", "Free rebooking": "Gratis ombooking", @@ -222,6 +224,8 @@ "Password": "Adgangskode", "Pay later": "Betal senere", "Pay now": "Betal nu", + "Payment": "Betaling", + "Payment Guarantee": "Garanti betaling", "Payment info": "Betalingsoplysninger", "Pet Room": "Kæledyrsrum", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kæledyrsrum har en ekstra gebyr på 20 EUR per ophold", @@ -272,6 +276,7 @@ "See room details": "Se værelsesdetaljer", "See rooms": "Se værelser", "Select a country": "Vælg et land", + "Select bed": "Vælg seng", "Select breakfast options": "Vælg morgenmadsmuligheder", "Select country of residence": "Vælg bopælsland", "Select date of birth": "Vælg fødselsdato", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index aaf57329f..008d7654e 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -86,6 +86,7 @@ "Description": "Beschreibung", "Destination": "Bestimmungsort", "Destinations & hotels": "Reiseziele & Hotels", + "Details": "Details", "Disabled booking options header": "Es tut uns leid", "Disabled booking options text": "Codes, Schecks und Bonusnächte sind auf der neuen Website noch nicht verfügbar.", "Discard changes": "Änderungen verwerfen", @@ -116,6 +117,7 @@ "First name": "Vorname", "Flexibility": "Flexibilität", "Follow us": "Folgen Sie uns", + "Food options": "Speisen & Getränke", "Former Scandic Hotel": "Ehemaliges Scandic Hotel", "Free cancellation": "Kostenlose Stornierung", "Free rebooking": "Kostenlose Umbuchung", @@ -220,6 +222,8 @@ "Password": "Passwort", "Pay later": "Später bezahlen", "Pay now": "Jetzt bezahlen", + "Payment": "Zahlung", + "Payment Guarantee": "Zahlungsgarantie", "Payment info": "Zahlungsinformationen", "Pet Room": "Haustierzimmer", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Haustierzimmer haben einen zusätzlichen Preis von 20 EUR pro Aufenthalt", @@ -271,6 +275,7 @@ "See room details": "Zimmerdetails ansehen", "See rooms": "Zimmer ansehen", "Select a country": "Wähle ein Land", + "Select bed": "Betttyp auswählen", "Select breakfast options": "Wählen Sie Frühstücksoptionen", "Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus", "Select date of birth": "Geburtsdatum auswählen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 24d888259..e7443b158 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -92,6 +92,7 @@ "Description": "Description", "Destination": "Destination", "Destinations & hotels": "Destinations & hotels", + "Details": "Details", "Disabled booking options header": "We're sorry", "Disabled booking options text": "Codes, cheques and reward nights aren't available on the new website yet.", "Discard changes": "Discard changes", @@ -123,6 +124,7 @@ "First name": "First name", "Flexibility": "Flexibility", "Follow us": "Follow us", + "Food options": "Food options", "Former Scandic Hotel": "Former Scandic Hotel", "Free cancellation": "Free cancellation", "Free rebooking": "Free rebooking", @@ -231,6 +233,8 @@ "Password": "Password", "Pay later": "Pay later", "Pay now": "Pay now", + "Payment": "Payment", + "Payment Guarantee": "Payment Guarantee", "Payment info": "Payment info", "Payment received": "Payment received", "Pet Room": "Pet room", @@ -286,6 +290,7 @@ "See room details": "See room details", "See rooms": "See rooms", "Select a country": "Select a country", + "Select bed": "Select bed", "Select breakfast options": "Select breakfast options", "Select country of residence": "Select country of residence", "Select date of birth": "Select date of birth", @@ -320,9 +325,9 @@ "Theatre": "Theatre", "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", "Total Points": "Total Points", "Total cost": "Total cost", - "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", "Total incl VAT": "Total incl VAT", "Total price": "Total price", "Tourist": "Tourist", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index fbd1682d6..65f7529b1 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -86,6 +86,7 @@ "Description": "Kuvaus", "Destination": "Kohde", "Destinations & hotels": "Kohteet ja hotellit", + "Details": "Tiedot", "Disabled booking options header": "Olemme pahoillamme", "Disabled booking options text": "Koodit, sekit ja palkintoillat eivät ole vielä saatavilla uudella verkkosivustolla.", "Discard changes": "Hylkää muutokset", @@ -116,6 +117,7 @@ "First name": "Etunimi", "Flexibility": "Joustavuus", "Follow us": "Seuraa meitä", + "Food options": "Ruokavalio", "Former Scandic Hotel": "Entinen Scandic-hotelli", "Free cancellation": "Ilmainen peruutus", "Free rebooking": "Ilmainen uudelleenvaraus", @@ -222,6 +224,8 @@ "Password": "Salasana", "Pay later": "Maksa myöhemmin", "Pay now": "Maksa nyt", + "Payment": "Maksu", + "Payment Guarantee": "Varmistusmaksu", "Payment info": "Maksutiedot", "Pet Room": "Lemmikkihuone", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Lemmikkihuoneen lisäkustannus on 20 EUR per majoitus", @@ -273,6 +277,7 @@ "See room details": "Katso huoneen tiedot", "See rooms": "Katso huoneet", "Select a country": "Valitse maa", + "Select bed": "Valitse vuodetyyppi", "Select breakfast options": "Valitse aamiaisvaihtoehdot", "Select country of residence": "Valitse asuinmaa", "Select date of birth": "Valitse syntymäaika", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index c15bf403f..f13a25268 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -85,6 +85,7 @@ "Description": "Beskrivelse", "Destination": "Destinasjon", "Destinations & hotels": "Destinasjoner og hoteller", + "Details": "Detaljer", "Disabled booking options header": "Vi beklager", "Disabled booking options text": "Koder, checks og belønningsnætter er enda ikke tilgjengelige på den nye nettsiden.", "Discard changes": "Forkaste endringer", @@ -115,6 +116,7 @@ "First name": "Fornavn", "Flexibility": "Fleksibilitet", "Follow us": "Følg oss", + "Food options": "Matvalg", "Former Scandic Hotel": "Tidligere Scandic-hotell", "Free cancellation": "Gratis avbestilling", "Free rebooking": "Gratis ombooking", @@ -220,6 +222,8 @@ "Password": "Passord", "Pay later": "Betal senere", "Pay now": "Betal nå", + "Payment": "Betaling", + "Payment Guarantee": "Garantera betalning", "Payment info": "Betalingsinformasjon", "Pet Room": "Kjæledyrsrom", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kjæledyrsrom har en tilleggsavgift på 20 EUR per opphold", @@ -270,6 +274,7 @@ "See room details": "Se detaljer om rommet", "See rooms": "Se rom", "Select a country": "Velg et land", + "Select bed": "Vælg seng", "Select breakfast options": "Velg frokostalternativer", "Select country of residence": "Velg bostedsland", "Select date of birth": "Velg fødselsdato", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 6ce4280fb..b74e19c0b 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -85,6 +85,7 @@ "Description": "Beskrivning", "Destination": "Destination", "Destinations & hotels": "Destinationer & hotell", + "Details": "Detaljer", "Disabled booking options header": "Vi beklagar", "Disabled booking options text": "Koder, bonuscheckar och belöningsnätter är inte tillgängliga på den nya webbplatsen än.", "Discard changes": "Ignorera ändringar", @@ -115,6 +116,7 @@ "First name": "Förnamn", "Flexibility": "Flexibilitet", "Follow us": "Följ oss", + "Food options": "Matval", "Former Scandic Hotel": "Tidigare Scandichotell", "Free cancellation": "Fri avbokning", "Free rebooking": "Fri ombokning", @@ -220,6 +222,8 @@ "Password": "Lösenord", "Pay later": "Betala senare", "Pay now": "Betala nu", + "Payment": "Betalning", + "Payment Guarantee": "Garantera betalning", "Payment info": "Betalningsinformation", "Pet Room": "Husdjursrum", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Husdjursrum har en extra avgift på 20 EUR per vistelse", @@ -270,6 +274,7 @@ "See room details": "Se rumsdetaljer", "See rooms": "Se rum", "Select a country": "Välj ett land", + "Select bed": "Välj säng", "Select breakfast options": "Välj frukostalternativ", "Select country of residence": "Välj bosättningsland", "Select date of birth": "Välj födelsedatum", From 60ceeaf9c347080994976910438a0adc7016956b Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Mon, 28 Oct 2024 11:06:47 +0100 Subject: [PATCH 46/49] fix(SW-614): move filtering logic to routes --- .../(standard)/[step]/page.tsx | 25 +++++++++-------- lib/trpc/memoizedRequests/index.ts | 28 +++++++++++++------ server/routers/hotels/input.ts | 2 ++ server/routers/hotels/output.ts | 1 + server/routers/hotels/query.ts | 16 ++++++++++- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 689a4d8de..7532c6673 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -22,7 +22,12 @@ import type { LangParams, PageArgs } from "@/types/params" export function preload() { void getProfileSafely() void getCreditCardsSafely() - void getRoomAvailability("811", 1, "2024-11-01", "2024-11-02") + void getRoomAvailability({ + hotelId: "811", + adults: 1, + roomStayStartDate: "2024-11-01", + roomStayEndDate: "2024-11-02", + }) } function isValidStep(step: string): step is StepEnum { @@ -45,12 +50,12 @@ export default async function StepPage({ const savedCreditCards = await getCreditCardsSafely() const breakfastPackages = await getBreakfastPackages(searchParams.hotel) - const roomAvailability = await getRoomAvailability( - searchParams.hotel, - Number(searchParams.adults), - searchParams.checkIn, - searchParams.checkOut - ) + const roomAvailability = await getRoomAvailability({ + hotelId: searchParams.hotel, + adults: Number(searchParams.adults), + roomStayStartDate: searchParams.checkIn, + roomStayEndDate: searchParams.checkOut, + }) if (!isValidStep(params.step) || !hotel || !roomAvailability) { return notFound() @@ -103,10 +108,8 @@ export default async function StepPage({ <Payment hotelId={searchParams.hotel} otherPaymentOptions={ - mustBeGuaranteed - ? [] - : hotel.data.attributes.merchantInformationData - .alternatePaymentOptions + hotel.data.attributes.merchantInformationData + .alternatePaymentOptions } savedCreditCards={savedCreditCards} mustBeGuaranteed={mustBeGuaranteed} diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 5aa915497..742fb2606 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -52,23 +52,34 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() { export const getHotelData = cache(async function getMemoizedHotelData( hotelId: string, - language: string + language: string, + isCardOnlyPayment?: boolean ) { return serverClient().hotel.hotelData.get({ hotelId, language, + isCardOnlyPayment, }) }) export const getRoomAvailability = cache( - async function getMemoizedRoomAvailability( - hotelId: string, - adults: number, - roomStayStartDate: string, - roomStayEndDate: string, - children?: number, + async function getMemoizedRoomAvailability({ + hotelId, + adults, + roomStayStartDate, + roomStayEndDate, + children, + promotionCode, + rateCode, + }: { + hotelId: string + adults: number + roomStayStartDate: string + roomStayEndDate: string + children?: number promotionCode?: string - ) { + rateCode?: string + }) { return serverClient().hotel.availability.rooms({ hotelId: parseInt(hotelId), adults, @@ -76,6 +87,7 @@ export const getRoomAvailability = cache( roomStayEndDate, children, promotionCode, + rateCode, }) } ) diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 194a16496..2d69ab642 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -26,6 +26,7 @@ export const getRoomsAvailabilityInputSchema = z.object({ promotionCode: z.string().optional(), reservationProfileType: z.string().optional().default(""), attachedProfileId: z.string().optional().default(""), + rateCode: z.string().optional(), }) export const getRatesInputSchema = z.object({ @@ -35,6 +36,7 @@ export const getRatesInputSchema = z.object({ export const getlHotelDataInputSchema = z.object({ hotelId: z.string(), language: z.string(), + isCardOnlyPayment: z.boolean().optional(), include: z .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) .optional(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 84ab77171..b69207a7d 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -578,6 +578,7 @@ const roomsAvailabilitySchema = z hotelId: z.number(), roomConfigurations: z.array(roomConfigurationSchema), rateDefinitions: z.array(rateDefinitionSchema), + mustBeGuaranteed: z.boolean().optional(), }), relationships: linksSchema.optional(), type: z.string().optional(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index c783d0be5..6ad27655a 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -430,6 +430,7 @@ export const hotelQueryRouter = router({ promotionCode, reservationProfileType, attachedProfileId, + rateCode, } = input const params: Record<string, string | number | undefined> = { @@ -534,6 +535,14 @@ export const hotelQueryRouter = router({ query: { hotelId, params: params }, }) ) + + if (rateCode) { + validateAvailabilityData.data.mustBeGuaranteed = + validateAvailabilityData.data.rateDefinitions.filter( + (rate) => rate.rateCode === rateCode + )[0].mustBeGuaranteed + } + return validateAvailabilityData.data }), }), @@ -577,7 +586,7 @@ export const hotelQueryRouter = router({ get: serviceProcedure .input(getlHotelDataInputSchema) .query(async ({ ctx, input }) => { - const { hotelId, language, include } = input + const { hotelId, language, include, isCardOnlyPayment } = input const params: Record<string, string> = { hotelId, @@ -669,6 +678,11 @@ export const hotelQueryRouter = router({ }) ) + if (isCardOnlyPayment) { + validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions = + [] + } + return validateHotelData.data }), }), From b6bec433fb4505cf702fe5702dcc72e1ab9bc2ce Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Mon, 28 Oct 2024 12:56:22 +0100 Subject: [PATCH 47/49] fix(SW-614): use height prop on icon --- .../Payment/GuaranteeDetails/guaranteeDetails.module.css | 4 ---- .../EnterDetails/Payment/GuaranteeDetails/index.tsx | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css index a14bc3473..32e678fbe 100644 --- a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css +++ b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css @@ -20,7 +20,3 @@ .summary::marker { display: none; } - -.summary .icon { - height: 16px; -} diff --git a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx index 99021715a..f1f8f6783 100644 --- a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx @@ -13,7 +13,7 @@ export default function GuaranteeDetails() { <Caption color="burgundy" type="bold" asChild> <summary className={styles.summary}> {intl.formatMessage({ id: "How it works" })} - <ChevronDown color="burgundy" className={styles.icon} /> + <ChevronDown color="burgundy" height={16} /> </summary> </Caption> <section className={styles.content}> From a5fad308ef225be91a610e5b5439c819f5c45ea1 Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Wed, 30 Oct 2024 10:06:59 +0100 Subject: [PATCH 48/49] fix(SW-614): remove room availabiltiy from preload --- .../hotelreservation/(standard)/[step]/page.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 7532c6673..264f5b04d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -22,12 +22,6 @@ import type { LangParams, PageArgs } from "@/types/params" export function preload() { void getProfileSafely() void getCreditCardsSafely() - void getRoomAvailability({ - hotelId: "811", - adults: 1, - roomStayStartDate: "2024-11-01", - roomStayEndDate: "2024-11-02", - }) } function isValidStep(step: string): step is StepEnum { @@ -42,6 +36,12 @@ export default async function StepPage({ redirect(`/${params.lang}`) } void getBreakfastPackages(searchParams.hotel) + void getRoomAvailability({ + hotelId: searchParams.hotel, + adults: Number(searchParams.adults), + roomStayStartDate: searchParams.checkIn, + roomStayEndDate: searchParams.checkOut, + }) const intl = await getIntl() @@ -55,13 +55,14 @@ export default async function StepPage({ adults: Number(searchParams.adults), roomStayStartDate: searchParams.checkIn, roomStayEndDate: searchParams.checkOut, + rateCode: searchParams.rateCode, }) if (!isValidStep(params.step) || !hotel || !roomAvailability) { return notFound() } - const mustBeGuaranteed = false + const mustBeGuaranteed = roomAvailability?.mustBeGuaranteed ?? false const paymentGuarantee = intl.formatMessage({ id: "Payment Guarantee", From 795eae89bc14a038dd9b8b631b0f9ad8e3f4151c Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Wed, 30 Oct 2024 11:36:43 +0100 Subject: [PATCH 49/49] fix: type issue --- lib/trpc/memoizedRequests/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 742fb2606..ec361b96c 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -76,7 +76,7 @@ export const getRoomAvailability = cache( adults: number roomStayStartDate: string roomStayEndDate: string - children?: number + children?: string promotionCode?: string rateCode?: string }) {