From ec2790a16e19bac6aa08d4edf51133b03f300293 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Thu, 10 Oct 2024 09:52:23 +0200 Subject: [PATCH 01/69] feat:sw-222 check if bookingUrl is linking to current or external page and use an anchor vs link element depending on it --- .../DynamicContent/Stays/StayCard/index.tsx | 36 ++++++++++++++++--- .../Stays/StayCard/stay.module.css | 4 +++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx index 5ab6c0b5d..506df2e65 100644 --- a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx +++ b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx @@ -1,4 +1,6 @@ "use client" +import { useEffect, useState } from "react" + import { dt } from "@/lib/dt" import { CalendarIcon } from "@/components/Icons" @@ -12,8 +14,27 @@ import styles from "./stay.module.css" import type { StayCardProps } from "@/types/components/myPages/stays/stayCard" +const useCheckIfExternal = (bookingUrl: string) => { + const [isExternal, setIsExternal] = useState(false) + + useEffect(() => { + if (typeof window !== "undefined") { + const hostName = window.location.hostname + const bookingURL = new URL(bookingUrl) + + const hostsMatch = hostName === bookingURL.hostname + const langRouteRegex = /\/[a-zA-Z]{2}\// + + setIsExternal(!hostsMatch || !langRouteRegex.test(bookingURL.pathname)) + } + }, [bookingUrl]) + + return isExternal +} + export default function StayCard({ stay }: StayCardProps) { const lang = useLang() + const { checkinDate, checkoutDate, hotelInformation, bookingUrl } = stay.attributes @@ -24,8 +45,13 @@ export default function StayCard({ stay }: StayCardProps) { const departDate = depart.format("DD MMM YYYY") const departDateTime = depart.format("YYYY-MM-DD") - return ( - + // TODO: Remove this check (and hook) and only return when current web is deleted + const isExternal = useCheckIfExternal(bookingUrl) + + const linkProps = { + href: bookingUrl, + className: styles.link, + children: (
- - ) + ), + } + + return isExternal ? : } diff --git a/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css b/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css index 1d3a685d7..995aa102a 100644 --- a/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css +++ b/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css @@ -6,6 +6,10 @@ overflow: hidden; } +.link { + text-decoration: none; +} + .stay:hover { border: 1.5px solid var(--Base-Border-Hover); } From 3cf66e308b0eb2f5b2d0934d415cbd8e6c8b4c19 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Thu, 10 Oct 2024 11:05:00 +0200 Subject: [PATCH 02/69] Fixed regex so it only match urls that starts with lang --- components/Blocks/DynamicContent/Stays/StayCard/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx index 506df2e65..d20ffe496 100644 --- a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx +++ b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx @@ -23,7 +23,7 @@ const useCheckIfExternal = (bookingUrl: string) => { const bookingURL = new URL(bookingUrl) const hostsMatch = hostName === bookingURL.hostname - const langRouteRegex = /\/[a-zA-Z]{2}\// + const langRouteRegex = /^\/[a-zA-Z]{2}\// setIsExternal(!hostsMatch || !langRouteRegex.test(bookingURL.pathname)) } From fd3cd053a242814cce4891bd19696552955c54e9 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Tue, 15 Oct 2024 15:48:23 +0200 Subject: [PATCH 03/69] feat(sw-452): Implement select room form submit --- .../RoomSelection/FlexibilityOption/index.tsx | 2 + .../RoomSelection/RateSummary/index.tsx | 20 ++++----- .../RoomSelection/RoomCard/index.tsx | 3 ++ .../SelectRate/RoomSelection/index.tsx | 41 +++++++++++++++++-- .../SelectRate/RoomSelection/utils.ts | 23 +++++++++++ .../selectRate/flexibilityOption.ts | 9 +++- .../hotelReservation/selectRate/selectRate.ts | 23 +++++++++-- 7 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 components/HotelReservation/SelectRate/RoomSelection/utils.ts diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index 519eef78c..30d5d5264 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -18,6 +18,7 @@ export default function FlexibilityOption({ paymentTerm, priceInformation, roomType, + roomTypeCode, handleSelectRate, }: FlexibilityOptionProps) { const [rootDiv, setRootDiv] = useState(undefined) @@ -46,6 +47,7 @@ export default function FlexibilityOption({ function onChange() { const rate = { + roomTypeCode: roomTypeCode, roomType: roomType, priceName: name, public: publicPrice, diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 29d4b0740..b929bfe76 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -24,17 +24,15 @@ export default function RateSummary({
- <> - - {priceToShow?.localPrice.pricePerStay}{" "} - {priceToShow?.localPrice.currency} - - - {intl.formatMessage({ id: "Approx." })}{" "} - {priceToShow?.requestedPrice?.pricePerStay}{" "} - {priceToShow?.requestedPrice?.currency} - - + + {priceToShow?.localPrice.pricePerStay}{" "} + {priceToShow?.localPrice.currency} + + + {intl.formatMessage({ id: "Approx." })}{" "} + {priceToShow?.requestedPrice?.pricePerStay}{" "} + {priceToShow?.requestedPrice?.currency} +
diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 7d1c15d6d..2eefefe85 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -4,6 +4,7 @@ import { useState } from "react" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" +import { getHotelReservationQueryParams } from "./utils" import styles from "./roomSelection.module.css" @@ -19,12 +20,43 @@ export default function RoomSelection({ const router = useRouter() const searchParams = useSearchParams() + const isUserLoggedIn = !!user function handleSubmit(e: React.FormEvent) { e.preventDefault() + const searchParamsObject = getHotelReservationQueryParams(searchParams) + + /** + * These are the query params that are used on current web and should come from the Booking Widget Search Submit. + * Might need to be changed when Search Submit in the Booking Widget is implemented. + */ const queryParams = new URLSearchParams(searchParams) - queryParams.set("roomClass", e.currentTarget.roomClass?.value) - queryParams.set("flexibility", e.currentTarget.flexibility?.value) + + searchParamsObject.room.forEach((item, index) => { + queryParams.set(`room[${index}].adults`, item.adults.toString()) + + if (Array.isArray(item.child)) { + item.child.forEach((child, childIndex) => { + queryParams.set( + `room[${index}].child[${childIndex}].age`, + child.age.toString() + ) + queryParams.set(`room[${index}].child[${childIndex}].bed`, child.bed) + }) + } + + queryParams.set( + `room[${index}].roomtypecode`, + rateSummary?.roomTypeCode || "" + ) + queryParams.set( + `room[${index}].ratecode`, + isUserLoggedIn + ? rateSummary?.member?.rateCode || "" + : rateSummary?.public?.rateCode || "" + ) + }) + router.push(`select-bed?${queryParams}`) } @@ -48,7 +80,10 @@ export default function RoomSelection({ ))} {rateSummary && ( - + )} diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts new file mode 100644 index 000000000..df8562504 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -0,0 +1,23 @@ +import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" + +function getHotelReservationQueryParams(searchParams: URLSearchParams) { + const searchParamsObject: SelectRateSearchParams = 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) { + nestedAcc[k] = value // Assign value at the last key + } else { + if (!nestedAcc[k]) { + nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as array or object + } + } + return nestedAcc[k] + }, acc) + return acc + }, {} as SelectRateSearchParams) + return searchParamsObject +} + +export { getHotelReservationQueryParams } diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index 473de563b..1835c3b65 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -1,6 +1,10 @@ import { z } from "zod" -import { Product, productTypePriceSchema } from "@/server/routers/hotels/output" +import { + Product, + productTypePriceSchema, + RoomConfiguration, +} from "@/server/routers/hotels/output" import { Rate } from "./selectRate" @@ -12,7 +16,8 @@ export type FlexibilityOptionProps = { value: string paymentTerm: string priceInformation?: Array - roomType: string + roomType: RoomConfiguration["roomType"] + roomTypeCode: RoomConfiguration["roomTypeCode"] handleSelectRate: (rate: Rate) => void } diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index 43152f11b..f806223c6 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -1,13 +1,28 @@ -import { Product } from "@/server/routers/hotels/output" +import { Product, RoomConfiguration } from "@/server/routers/hotels/output" + +interface Child { + bed: string + age: number +} + +interface Room { + adults: number + roomtypecode: string + ratecode: string + child: Child[] +} export interface SelectRateSearchParams { - fromDate: string - toDate: string hotel: string + fromdate: string + todate: string + room: Room[] + [key: string]: any } export interface Rate { - roomType: string + roomType: RoomConfiguration["roomType"] + roomTypeCode: RoomConfiguration["roomTypeCode"] priceName: string public: Product["productType"]["public"] member: Product["productType"]["member"] From 420f3210e894c518041ce083124dccc65cd380cb Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 16 Oct 2024 15:05:13 +0200 Subject: [PATCH 04/69] feat(sw-452): Added correct query params --- .../SelectRate/RoomSelection/index.tsx | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 2eefefe85..efa7f2b3f 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -26,35 +26,21 @@ export default function RoomSelection({ e.preventDefault() const searchParamsObject = getHotelReservationQueryParams(searchParams) - /** - * These are the query params that are used on current web and should come from the Booking Widget Search Submit. - * Might need to be changed when Search Submit in the Booking Widget is implemented. - */ const queryParams = new URLSearchParams(searchParams) searchParamsObject.room.forEach((item, index) => { - queryParams.set(`room[${index}].adults`, item.adults.toString()) - - if (Array.isArray(item.child)) { - item.child.forEach((child, childIndex) => { - queryParams.set( - `room[${index}].child[${childIndex}].age`, - child.age.toString() - ) - queryParams.set(`room[${index}].child[${childIndex}].bed`, child.bed) - }) + if (rateSummary?.roomTypeCode) { + queryParams.set(`room[${index}].roomtype`, rateSummary.roomTypeCode) + } + if (rateSummary?.public?.rateCode) { + queryParams.set(`room[${index}].ratecode`, rateSummary.public.rateCode) + } + if (rateSummary?.member?.rateCode) { + queryParams.set( + `room[${index}].counterratecode`, + rateSummary.member.rateCode + ) } - - queryParams.set( - `room[${index}].roomtypecode`, - rateSummary?.roomTypeCode || "" - ) - queryParams.set( - `room[${index}].ratecode`, - isUserLoggedIn - ? rateSummary?.member?.rateCode || "" - : rateSummary?.public?.rateCode || "" - ) }) router.push(`select-bed?${queryParams}`) From 7b1e2d8a06a3acc72b67bc980ef6026af2abd70e Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 17 Oct 2024 09:36:49 +0200 Subject: [PATCH 05/69] feat(sw-452): get searchParams and fetch availability --- .../(public)/hotelreservation/select-rate/page.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx index fdd5436f3..8767b8ae0 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx @@ -15,6 +15,11 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) + console.log("searchParams", searchParams) + + const roomParams = new URLSearchParams(searchParams) + const adults = roomParams.get("room[0].adults") || "1" // Default to '1' if not found + const [hotelData, roomConfigurations, user] = await Promise.all([ serverClient().hotel.hotelData.get({ hotelId: searchParams.hotel, @@ -23,9 +28,9 @@ export default async function SelectRatePage({ }), serverClient().hotel.availability.rooms({ hotelId: parseInt(searchParams.hotel, 10), - roomStayStartDate: "2024-11-02", - roomStayEndDate: "2024-11-03", - adults: 1, + roomStayStartDate: searchParams.fromDate, + roomStayEndDate: searchParams.toDate, + adults: parseInt(adults, 10), }), getProfileSafely(), ]) From a9674316728b18839a1272dd77fa15f9023771bc Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 17 Oct 2024 09:39:06 +0200 Subject: [PATCH 06/69] feat(sw-452): remove console log --- .../(live)/(public)/hotelreservation/select-rate/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx index 8767b8ae0..4dd231574 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx @@ -15,8 +15,6 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) - console.log("searchParams", searchParams) - const roomParams = new URLSearchParams(searchParams) const adults = roomParams.get("room[0].adults") || "1" // Default to '1' if not found From fce9ef1b6166fdf47a3ca54c47e1d18e7ee03425 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 17 Oct 2024 10:10:28 +0200 Subject: [PATCH 07/69] feat(sw-452): updated typing on util --- .../hotelreservation/select-rate/page.tsx | 9 +++-- .../RoomSelection/FlexibilityOption/index.tsx | 4 +- .../SelectRate/RoomSelection/index.tsx | 2 +- .../SelectRate/RoomSelection/utils.ts | 37 +++++++++++-------- .../hotelReservation/selectRate/selectRate.ts | 2 +- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx index 4dd231574..44f3dbd15 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx @@ -2,6 +2,7 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" +import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" @@ -15,8 +16,10 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) - const roomParams = new URLSearchParams(searchParams) - const adults = roomParams.get("room[0].adults") || "1" // Default to '1' if not found + const selecetRoomParams = new URLSearchParams(searchParams) + const selecetRoomParamsObject = + getHotelReservationQueryParams(selecetRoomParams) + const adults = selecetRoomParamsObject.room[0].adults // TODO: Handle multiple rooms const [hotelData, roomConfigurations, user] = await Promise.all([ serverClient().hotel.hotelData.get({ @@ -28,7 +31,7 @@ export default async function SelectRatePage({ hotelId: parseInt(searchParams.hotel, 10), roomStayStartDate: searchParams.fromDate, roomStayEndDate: searchParams.toDate, - adults: parseInt(adults, 10), + adults: adults, }), getProfileSafely(), ]) diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index 30d5d5264..fc2b4d819 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -47,8 +47,8 @@ export default function FlexibilityOption({ function onChange() { const rate = { - roomTypeCode: roomTypeCode, - roomType: roomType, + roomTypeCode, + roomType, priceName: name, public: publicPrice, member: memberPrice, diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index efa7f2b3f..c4c5e2e87 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -4,7 +4,7 @@ import { useState } from "react" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" -import { getHotelReservationQueryParams } from "./utils" +import getHotelReservationQueryParams from "./utils" import styles from "./roomSelection.module.css" diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index df8562504..e47a0da70 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -1,23 +1,28 @@ import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" function getHotelReservationQueryParams(searchParams: URLSearchParams) { - const searchParamsObject: SelectRateSearchParams = Array.from( + 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) { - nestedAcc[k] = value // Assign value at the last key - } else { - if (!nestedAcc[k]) { - nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as array or object + ).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] - }, acc) - return acc - }, {} as SelectRateSearchParams) - return searchParamsObject + return nestedAcc[k] as Record + }, acc) + return acc + }, + {} as Record + ) + return searchParamsObject as SelectRateSearchParams } -export { getHotelReservationQueryParams } +export default getHotelReservationQueryParams diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index f806223c6..58aa6fbbe 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -17,7 +17,7 @@ export interface SelectRateSearchParams { fromdate: string todate: string room: Room[] - [key: string]: any + [key: string]: string | string[] | Room[] } export interface Rate { From f25af8382eb1f4ab5f75aef5ec347764f4f3a225 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 17 Oct 2024 10:17:57 +0200 Subject: [PATCH 08/69] feat(sw-452): added language to availabily call to get correct currency --- server/routers/hotels/query.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 198219e0e..9c07fd2e3 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -236,6 +236,8 @@ export const hotelQueryRouter = router({ hotels: serviceProcedure .input(getHotelsAvailabilityInputSchema) .query(async ({ input, ctx }) => { + const { lang, uid } = ctx + const apiLang = toApiLang(lang) const { cityId, roomStayStartDate, @@ -255,6 +257,7 @@ export const hotelQueryRouter = router({ promotionCode, reservationProfileType, attachedProfileId, + language: apiLang, } hotelsAvailabilityCounter.add(1, { From 0238e9e07e558d4981b82bf06573f39137672ace Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 17 Oct 2024 10:25:04 +0200 Subject: [PATCH 09/69] feat(sw-452): added children --- .../(live)/(public)/hotelreservation/select-rate/page.tsx | 2 ++ server/routers/hotels/query.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx index 44f3dbd15..a9103d2c4 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx @@ -20,6 +20,7 @@ export default async function SelectRatePage({ const selecetRoomParamsObject = getHotelReservationQueryParams(selecetRoomParams) const adults = selecetRoomParamsObject.room[0].adults // TODO: Handle multiple rooms + const children = selecetRoomParamsObject.room[0].child.length // TODO: Handle multiple rooms const [hotelData, roomConfigurations, user] = await Promise.all([ serverClient().hotel.hotelData.get({ @@ -32,6 +33,7 @@ export default async function SelectRatePage({ roomStayStartDate: searchParams.fromDate, roomStayEndDate: searchParams.toDate, adults: adults, + children: children, }), getProfileSafely(), ]) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 9c07fd2e3..77a739035 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -236,7 +236,7 @@ export const hotelQueryRouter = router({ hotels: serviceProcedure .input(getHotelsAvailabilityInputSchema) .query(async ({ input, ctx }) => { - const { lang, uid } = ctx + const { lang } = ctx const apiLang = toApiLang(lang) const { cityId, From d56def8ed64b2cad2005f0b2b7602d4b32c227b0 Mon Sep 17 00:00:00 2001 From: Michael Zetterberg Date: Thu, 17 Oct 2024 11:39:42 +0200 Subject: [PATCH 10/69] fix: date selection --- .../TempDesignSystem/Form/Date/index.tsx | 59 +++++-------------- 1 file changed, 14 insertions(+), 45 deletions(-) diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index 222bbd1c1..7441ff471 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -28,62 +28,36 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { rules: registerOptions, }) - const [dateSegments, setDateSegment] = useState<{ - year: number | null - month: number | null - date: number | null - daysInMonth: number - }>({ - year: null, - month: null, - date: null, - daysInMonth: 31, - }) - const currentYear = new Date().getFullYear() + const months = rangeArray(1, 12).map((month) => ({ value: month, label: `${month}`, })) + const years = rangeArray(1900, currentYear - 18) .reverse() .map((year) => ({ value: year, label: year.toString() })) + // Ensure the user can't select a date that doesn't exist. + const daysInMonth = dt(currentValue).daysInMonth() + const days = rangeArray(1, daysInMonth).map((day) => ({ + value: day, + label: `${day}`, + })) + function createOnSelect(selector: DateName) { /** * Months are 0 index based and therefore we * must subtract by 1 to get the selected month */ return (select: Key) => { - const value = - selector === DateName.month ? Number(select) - 1 : Number(select) - const newSegments = { ...dateSegments, [selector]: value } - - /** - * Update daysInMonth when year or month changes - * to ensure the user can't select a date that doesn't exist. - */ - if (selector === DateName.year || selector === DateName.month) { - const year = selector === DateName.year ? value : newSegments.year - const month = selector === DateName.month ? value : newSegments.month - if (year !== null && month !== null) { - newSegments.daysInMonth = dt().year(year).month(month).daysInMonth() - } else if (month !== null) { - newSegments.daysInMonth = dt().month(month).daysInMonth() - } + if (selector === DateName.month) { + select = Number(select) - 1 } - - if (Object.values(newSegments).every((val) => val !== null)) { - const newDate = dt() - .utc() - .set("year", newSegments.year!) - .set("month", newSegments.month!) - .set("date", Math.min(newSegments.date!, newSegments.daysInMonth)) - - setValue(name, newDate.format("YYYY-MM-DD")) - trigger(name) - } - setDateSegment(newSegments) + const newDate = dt(currentValue).set(selector, Number(select)) + setValue(name, newDate.format("YYYY-MM-DD")) + trigger(name) } } @@ -117,11 +91,6 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { {(segment) => { switch (segment.type) { case "day": - const maxDays = dateSegments.daysInMonth - const days = rangeArray(1, maxDays).map((day) => ({ - value: day, - label: `${day}`, - })) return (
+ + {label} +
+ {cardNumber ? ( + •••• {cardNumber} + ) : ( + {label} + )} + + ) +} diff --git a/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.module.css b/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.module.css similarity index 100% rename from components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.module.css rename to components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.module.css diff --git a/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.ts b/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts similarity index 65% rename from components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.ts rename to components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts index 5d77f6560..151a18860 100644 --- a/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.ts +++ b/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts @@ -1,10 +1,10 @@ import { RegisterOptions } from "react-hook-form" -import { PaymentMethodEnum } from "@/constants/booking" - export interface PaymentOptionProps { name: string - value: PaymentMethodEnum + value: string label: string + cardNumber?: string registerOptions?: RegisterOptions + onChange?: () => void } diff --git a/components/HotelReservation/SelectRate/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx similarity index 55% rename from components/HotelReservation/SelectRate/Payment/index.tsx rename to components/HotelReservation/EnterDetails/Payment/index.tsx index b1bddd84c..3d582e7e3 100644 --- a/components/HotelReservation/SelectRate/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -23,6 +23,7 @@ import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" import Checkbox from "@/components/TempDesignSystem/Checkbox" import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import { toast } from "@/components/TempDesignSystem/Toasts" import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" @@ -38,7 +39,15 @@ import { PaymentProps } from "@/types/components/hotelReservation/selectRate/sec const maxRetries = 40 const retryInterval = 2000 -export default function Payment({ hotel }: PaymentProps) { +function isPaymentMethodEnum(value: string): value is PaymentMethodEnum { + return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum) +} + +export default function Payment({ + hotelId, + otherPaymentOptions, + savedCreditCards, +}: PaymentProps) { const router = useRouter() const lang = useLang() const intl = useIntl() @@ -46,7 +55,9 @@ export default function Payment({ hotel }: PaymentProps) { const methods = useForm({ defaultValues: { - paymentMethod: PaymentMethodEnum.card, + paymentMethod: savedCreditCards?.length + ? savedCreditCards[0].id + : PaymentMethodEnum.card, smsConfirmation: false, termsAndConditions: false, }, @@ -87,8 +98,17 @@ export default function Payment({ hotel }: PaymentProps) { }, [confirmationNumber, bookingStatus, router]) function handleSubmit(data: PaymentFormData) { + // set payment method to card if saved card is submitted + const paymentMethod = isPaymentMethodEnum(data.paymentMethod) + ? data.paymentMethod + : PaymentMethodEnum.card + + const savedCreditCard = savedCreditCards?.find( + (card) => card.id === data.paymentMethod + ) + initiateBooking.mutate({ - hotelId: hotel.operaId, + hotelId: hotelId, checkInDate: "2024-12-10", checkOutDate: "2024-12-11", rooms: [ @@ -116,7 +136,14 @@ export default function Payment({ hotel }: PaymentProps) { }, ], payment: { - paymentMethod: data.paymentMethod, + paymentMethod, + card: savedCreditCard + ? { + alias: savedCreditCard.alias, + expiryDate: savedCreditCard.expirationDate, + cardType: savedCreditCard.cardType, + } + : undefined, cardHolder: { email: "test.user@scandichotels.com", name: "Test User", @@ -143,65 +170,94 @@ export default function Payment({ hotel }: PaymentProps) { className={styles.paymentContainer} onSubmit={methods.handleSubmit(handleSubmit)} > -
- - {hotel.merchantInformationData.alternatePaymentOptions.map( - (paymentMethod) => ( + {savedCreditCards?.length ? ( +
+ + {intl.formatMessage({ id: "MY SAVED CARDS" })} + +
+ {savedCreditCards?.map((savedCreditCard) => ( + + ))} +
+
+ ) : null} +
+ {savedCreditCards?.length ? ( + + {intl.formatMessage({ id: "OTHER PAYMENT METHODS" })} + + ) : null} +
+ + {otherPaymentOptions.map((paymentMethod) => ( - ) - )} -
- - - {intl.formatMessage({ - id: "I would like to get my booking confirmation via sms", - })} - - + ))} +
+ +
+ + + {intl.formatMessage({ + id: "I would like to get my booking confirmation via sms", + })} + + - - - - {intl.formatMessage( - { - id: "booking.terms", - }, - { - termsLink: (str) => ( - - {str} - - ), - privacyLink: (str) => ( - - {str} - - ), - } - )} - - + + + + {intl.formatMessage( + { + id: "booking.terms", + }, + { + termsLink: (str) => ( + + {str} + + ), + privacyLink: (str) => ( + + {str} + + ), + } + )} + + +
+ ) : null} + + {closeable ? ( + + ) : null} + + ) +} diff --git a/components/TempDesignSystem/Alert/utils.ts b/components/TempDesignSystem/Alert/utils.ts new file mode 100644 index 000000000..06fc9bcfd --- /dev/null +++ b/components/TempDesignSystem/Alert/utils.ts @@ -0,0 +1,17 @@ +import { InfoCircleIcon } from "@/components/Icons" +import CrossCircleIcon from "@/components/Icons/CrossCircle" +import WarningTriangleIcon from "@/components/Icons/WarningTriangle" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export function getIconByAlertType(alertType: AlertTypeEnum) { + switch (alertType) { + case AlertTypeEnum.Alarm: + return CrossCircleIcon + case AlertTypeEnum.Warning: + return WarningTriangleIcon + case AlertTypeEnum.Info: + default: + return InfoCircleIcon + } +} diff --git a/components/TempDesignSystem/Alert/variants.ts b/components/TempDesignSystem/Alert/variants.ts new file mode 100644 index 000000000..91087f17b --- /dev/null +++ b/components/TempDesignSystem/Alert/variants.ts @@ -0,0 +1,20 @@ +import { cva } from "class-variance-authority" + +import styles from "./alert.module.css" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export const alertVariants = cva(styles.alert, { + variants: { + variant: { + inline: styles.inline, + banner: styles.banner, + }, + type: { + [AlertTypeEnum.Info]: styles.info, + [AlertTypeEnum.Warning]: styles.warning, + [AlertTypeEnum.Alarm]: styles.alarm, + }, + }, + defaultVariants: {}, +}) diff --git a/types/trpc/routers/contentstack/siteConfig.ts b/types/trpc/routers/contentstack/siteConfig.ts index 8dc553cb2..7c4160203 100644 --- a/types/trpc/routers/contentstack/siteConfig.ts +++ b/types/trpc/routers/contentstack/siteConfig.ts @@ -12,3 +12,4 @@ export type GetSiteConfigData = z.input export type SiteConfig = z.output export type Alert = z.output +export type SidepeekContent = Alert["sidepeekContent"] From db9f31e2c38e5b72f55de44bb4b40aadd62e0b87 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 17 Oct 2024 11:23:50 +0200 Subject: [PATCH 44/69] feat(SW-498): added sitewide alert --- .../(live)/@sitewidealert/[...paths]/page.tsx | 1 + .../[contentType]/[uid]/page.tsx | 1 + app/[lang]/(live)/@sitewidealert/default.tsx | 1 + .../my-pages/[...path]/page.tsx | 1 + app/[lang]/(live)/@sitewidealert/page.tsx | 17 ++++ app/[lang]/(live)/layout.tsx | 3 + components/SitewideAlert/index.tsx | 33 +++++++ .../SitewideAlert/sitewideAlert.module.css | 9 ++ .../TempDesignSystem/Alert/Sidepeek/index.tsx | 49 +++++++++++ .../Alert/Sidepeek/sidepeek.module.css | 3 + .../Alert/Sidepeek/sidepeek.ts | 6 ++ .../TempDesignSystem/Alert/alert.module.css | 77 ++++++++-------- components/TempDesignSystem/Alert/alert.ts | 12 ++- components/TempDesignSystem/Alert/index.tsx | 87 ++++++++++--------- components/TempDesignSystem/Alert/variants.ts | 5 +- lib/trpc/memoizedRequests/index.ts | 4 + server/routers/contentstack/base/query.ts | 2 +- 17 files changed, 226 insertions(+), 85 deletions(-) create mode 100644 app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx create mode 100644 app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx create mode 100644 app/[lang]/(live)/@sitewidealert/default.tsx create mode 100644 app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx create mode 100644 app/[lang]/(live)/@sitewidealert/page.tsx create mode 100644 components/SitewideAlert/index.tsx create mode 100644 components/SitewideAlert/sitewideAlert.module.css create mode 100644 components/TempDesignSystem/Alert/Sidepeek/index.tsx create mode 100644 components/TempDesignSystem/Alert/Sidepeek/sidepeek.module.css create mode 100644 components/TempDesignSystem/Alert/Sidepeek/sidepeek.ts diff --git a/app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx b/app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx new file mode 100644 index 000000000..2ebaca014 --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../page" diff --git a/app/[lang]/(live)/@sitewidealert/default.tsx b/app/[lang]/(live)/@sitewidealert/default.tsx new file mode 100644 index 000000000..83ec2818e --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/default.tsx @@ -0,0 +1 @@ +export { default } from "./page" diff --git a/app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx b/app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx new file mode 100644 index 000000000..2ebaca014 --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../page" diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx new file mode 100644 index 000000000..618f3b5cb --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -0,0 +1,17 @@ +import { Suspense } from "react" + +import SitewideAlert, { preload } from "@/components/SitewideAlert" +import { setLang } from "@/i18n/serverContext" + +import type { LangParams, PageArgs } from "@/types/params" + +export default function SitewideAlertPage({ params }: PageArgs) { + setLang(params.lang) + preload() + + return ( + + + + ) +} diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 3704178c4..e1c63bbbc 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -22,12 +22,14 @@ export default async function RootLayout({ children, footer, header, + sitewidealert, params, }: React.PropsWithChildren< LayoutArgs & { bookingwidget: React.ReactNode footer: React.ReactNode header: React.ReactNode + sitewidealert: React.ReactNode } >) { setLang(params.lang) @@ -55,6 +57,7 @@ export default async function RootLayout({ + {!env.HIDE_FOR_NEXT_RELEASE && <>{sitewidealert}} {header} {!env.HIDE_FOR_NEXT_RELEASE && <>{bookingwidget}} {children} diff --git a/components/SitewideAlert/index.tsx b/components/SitewideAlert/index.tsx new file mode 100644 index 000000000..2ce624119 --- /dev/null +++ b/components/SitewideAlert/index.tsx @@ -0,0 +1,33 @@ +import { getSiteConfig } from "@/lib/trpc/memoizedRequests" + +import Alert from "../TempDesignSystem/Alert" + +import styles from "./sitewideAlert.module.css" + +export function preload() { + void getSiteConfig() +} + +export default async function SitewideAlert() { + const siteConfig = await getSiteConfig() + + if (!siteConfig?.sitewideAlert) { + return null + } + + const { sitewideAlert } = siteConfig + return ( +
+ +
+ ) +} diff --git a/components/SitewideAlert/sitewideAlert.module.css b/components/SitewideAlert/sitewideAlert.module.css new file mode 100644 index 000000000..f91bf3db5 --- /dev/null +++ b/components/SitewideAlert/sitewideAlert.module.css @@ -0,0 +1,9 @@ +.sitewideAlert { + width: 100%; +} + +.alarm { + position: sticky; + top: 0; + z-index: calc(var(--header-z-index) + 1); +} diff --git a/components/TempDesignSystem/Alert/Sidepeek/index.tsx b/components/TempDesignSystem/Alert/Sidepeek/index.tsx new file mode 100644 index 000000000..db69cf563 --- /dev/null +++ b/components/TempDesignSystem/Alert/Sidepeek/index.tsx @@ -0,0 +1,49 @@ +"use client" + +import { useState } from "react" + +import { ChevronRightIcon } from "@/components/Icons" +import JsonToHtml from "@/components/JsonToHtml" +import Button from "@/components/TempDesignSystem/Button" + +import SidePeek from "../../SidePeek" + +import styles from "./sidepeek.module.css" + +import type { AlertSidepeekProps } from "./sidepeek" + +export default function AlertSidepeek({ + ctaText, + sidePeekContent, +}: AlertSidepeekProps) { + const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false) + const { heading, content } = sidePeekContent + + return ( +
+ + {sidePeekIsOpen ? ( + setSidePeekIsOpen(false)} + > + + + ) : null} +
+ ) +} diff --git a/components/TempDesignSystem/Alert/Sidepeek/sidepeek.module.css b/components/TempDesignSystem/Alert/Sidepeek/sidepeek.module.css new file mode 100644 index 000000000..fccd935aa --- /dev/null +++ b/components/TempDesignSystem/Alert/Sidepeek/sidepeek.module.css @@ -0,0 +1,3 @@ +.alertSidepeek { + flex-shrink: 0; +} diff --git a/components/TempDesignSystem/Alert/Sidepeek/sidepeek.ts b/components/TempDesignSystem/Alert/Sidepeek/sidepeek.ts new file mode 100644 index 000000000..acd7ce35a --- /dev/null +++ b/components/TempDesignSystem/Alert/Sidepeek/sidepeek.ts @@ -0,0 +1,6 @@ +import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConfig" + +export interface AlertSidepeekProps { + ctaText: string + sidePeekContent: NonNullable +} diff --git a/components/TempDesignSystem/Alert/alert.module.css b/components/TempDesignSystem/Alert/alert.module.css index f5ba2f58a..728f7d22d 100644 --- a/components/TempDesignSystem/Alert/alert.module.css +++ b/components/TempDesignSystem/Alert/alert.module.css @@ -1,6 +1,4 @@ .alert { - display: flex; - gap: var(--Spacing-x2); overflow: hidden; } @@ -12,34 +10,26 @@ } .content { + width: 100%; + max-width: var(--max-width-navigation); + margin: 0 auto; display: flex; - justify-content: space-between; - align-items: center; gap: var(--Spacing-x2); - padding: var(--Spacing-x-one-and-half) var(--Spacing-x2) - var(--Spacing-x-one-and-half) 0; +} + +.innerContent { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + gap: var(--Spacing-x1); + padding: var(--Spacing-x2) 0; flex-grow: 1; } .textWrapper { display: grid; gap: var(--Spacing-x-half); - padding: var(--Spacing-x1) 0; -} - -.sidepeekCta { - flex-shrink: 0; -} - -.closeButton { - border-width: 0; - padding: 0; - margin: 0; - background-color: transparent; - display: flex; - align-items: center; - flex-shrink: 0; - cursor: pointer; } /* Intent: inline */ @@ -48,59 +38,64 @@ border: 1px solid var(--Base-Border-Subtle); background-color: var(--Base-Surface-Primary-light-Normal); } +.inline .innerContent { + padding-right: var(--Spacing-x3); +} .inline .iconWrapper { padding: var(--Spacing-x-one-and-half); } .inline.alarm .iconWrapper { - background-color: var(--Main-Red-70); + background-color: var(--UI-Semantic-Error); } .inline.warning .iconWrapper { - background-color: var(--Main-Yellow-60); + background-color: var(--UI-Semantic-Warning); } .inline.info .iconWrapper { - background-color: var(--Scandic-Blue-70); + background-color: var(--UI-Semantic-Information); } .inline .icon, .inline .icon * { fill: var(--Base-Surface-Primary-light-Normal); } -.inline .closeButton { - border-left: 1px solid var(--Base-Border-Subtle); - padding: var(--Spacing-x-one-and-half); -} /* Intent: banner */ .banner { - padding: 0 var(--Spacing-x5); + padding: 0 var(--Spacing-x3); border-left-width: 6px; border-left-style: solid; } .banner.alarm { - border-left-color: var(--Main-Red-70); - background-color: var(--Main-Red-00); + border-left-color: var(--UI-Semantic-Error); + background-color: var(--Scandic-Red-00); } .banner.warning { - border-left-color: var(--Main-Yellow-60); - background-color: var(--Main-Yellow-00); + border-left-color: var(--UI-Semantic-Warning); + background-color: var(--Scandic-Yellow-00); } .banner.info { - border-left-color: var(--Scandic-Blue-70); + border-left-color: var(--UI-Semantic-Information); background-color: var(--Scandic-Blue-00); } .banner.alarm .icon, .banner.alarm .icon * { - fill: var(--Main-Red-70); + fill: var(--UI-Semantic-Error); } .banner.warning .icon, .banner.warning .icon * { - fill: var(--Main-Yellow-60); + fill: var(--UI-Semantic-Warning); } .banner.info .icon, .banner.info .icon * { - fill: var(--Scandic-Blue-70); + fill: var(--UI-Semantic-Information); } -.banner .closeButton { - align-self: center; - padding-left: var(--Spacing-x-one-and-half); +@media screen and (min-width: 768px) { + .banner { + padding: 0 var(--Spacing-x5); + } + .innerContent { + flex-direction: row; + align-items: center; + gap: var(--Spacing-x2); + } } diff --git a/components/TempDesignSystem/Alert/alert.ts b/components/TempDesignSystem/Alert/alert.ts index 95fec9d08..20927d7fc 100644 --- a/components/TempDesignSystem/Alert/alert.ts +++ b/components/TempDesignSystem/Alert/alert.ts @@ -8,9 +8,17 @@ import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConf export interface AlertProps extends VariantProps { className?: string type: AlertTypeEnum - closeable?: boolean heading?: string text: string + phoneContact?: { + displayText: string + phoneNumber?: string + footnote?: string | null + } | null sidepeekContent?: SidepeekContent | null - sidePeekCtaText?: string | null + sidepeekCtaText?: string | null + link?: { + url: string + title: string + } | null } diff --git a/components/TempDesignSystem/Alert/index.tsx b/components/TempDesignSystem/Alert/index.tsx index 57bd0c5c1..6499c7126 100644 --- a/components/TempDesignSystem/Alert/index.tsx +++ b/components/TempDesignSystem/Alert/index.tsx @@ -1,69 +1,76 @@ -import { ChevronRightIcon, CloseLargeIcon } from "@/components/Icons" -import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" -import { getIntl } from "@/i18n" -import { AlertProps } from "./alert" +import Link from "../Link" +import AlertSidepeek from "./Sidepeek" import { getIconByAlertType } from "./utils" import { alertVariants } from "./variants" import styles from "./alert.module.css" -export default async function Alert({ +import type { AlertProps } from "./alert" + +export default function Alert({ className, variant, type, heading, text, - sidePeekCtaText, + link, + phoneContact, + sidepeekCtaText, sidepeekContent, - closeable = false, }: AlertProps) { const classNames = alertVariants({ className, variant, type, }) - const intl = await getIntl() - const Icon = getIconByAlertType(type) return ( -
- - - +
-
- {heading ? ( - -

{heading}

+ + + +
+
+ {heading ? ( + +

{heading}

+ + ) : null} + + {text} + {phoneContact?.phoneNumber ? ( + <> + {phoneContact.displayText} + + {phoneContact.phoneNumber} + + {phoneContact.footnote ? ( + . ({phoneContact.footnote}) + ) : null} + + ) : null} +
+ {link ? ( + + {link.title} + + ) : null} + {!link && sidepeekCtaText && sidepeekContent ? ( + ) : null} - {text}
- {sidePeekCtaText ? ( - - ) : null}
- {closeable ? ( - - ) : null} -
+
) } diff --git a/components/TempDesignSystem/Alert/variants.ts b/components/TempDesignSystem/Alert/variants.ts index 91087f17b..7397ac1e9 100644 --- a/components/TempDesignSystem/Alert/variants.ts +++ b/components/TempDesignSystem/Alert/variants.ts @@ -16,5 +16,8 @@ export const alertVariants = cva(styles.alert, { [AlertTypeEnum.Alarm]: styles.alarm, }, }, - defaultVariants: {}, + defaultVariants: { + variant: "inline", + type: AlertTypeEnum.Info, + }, }) diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index fed1caf99..471d20def 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -45,3 +45,7 @@ export const getLanguageSwitcher = cache( return serverClient().contentstack.languageSwitcher.get() } ) + +export const getSiteConfig = cache(async function getMemoizedSiteConfig() { + return serverClient().contentstack.base.siteConfig() +}) diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index 2a26e980e..3218b8bb6 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -750,7 +750,7 @@ export const baseQueryRouter = router({ sitewideAlert: sitewideAlert ? { ...sitewideAlert, - phone_contact: contactConfig + phoneContact: contactConfig ? getAlertPhoneContactData(sitewideAlert, contactConfig) : null, } From cf3f54d752879b6f086c40d54719279a6871bd38 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Wed, 23 Oct 2024 09:48:16 +0200 Subject: [PATCH 45/69] Added temp loading when clicking staycard #sw-222 --- .../DynamicContent/Stays/StayCard/index.tsx | 17 ++++++++++++++++- .../Stays/StayCard/stay.module.css | 13 +++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx index 270ede97a..9e63c0f77 100644 --- a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx +++ b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx @@ -1,9 +1,12 @@ "use client" +import { useState } from "react" + import { dt } from "@/lib/dt" import { CalendarIcon } from "@/components/Icons" import Image from "@/components/Image" +import LoadingSpinner from "@/components/LoadingSpinner" import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" @@ -16,6 +19,9 @@ import type { StayCardProps } from "@/types/components/myPages/stays/stayCard" export default function StayCard({ stay }: StayCardProps) { const lang = useLang() + // TODO: Temporary loading. Remove when current web is deleted. + const [loading, setLoading] = useState(false) + const { checkinDate, checkoutDate, hotelInformation, bookingUrl } = stay.attributes @@ -27,7 +33,11 @@ export default function StayCard({ stay }: StayCardProps) { const departDateTime = depart.format("YYYY-MM-DD") return ( - + setLoading(true)} + >
+ {loading && ( +
+ +
+ )} ) } diff --git a/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css b/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css index 995aa102a..bfadbad67 100644 --- a/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css +++ b/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css @@ -8,6 +8,7 @@ .link { text-decoration: none; + position: relative; } .stay:hover { @@ -45,3 +46,15 @@ display: flex; gap: var(--Spacing-x-half); } + +.loadingcontainer { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 70px; + background: rgb(255 255 255 / 80%); + display: flex; + align-items: center; + justify-content: center; +} From 445bde8e2e2f565e6055866a2c23affde6e6f179 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 22 Oct 2024 15:45:00 +0200 Subject: [PATCH 46/69] feat(SW-671): Added correct icons to amenities list --- .../AmenitiesList/amenitiesList.module.css | 4 + .../HotelPage/AmenitiesList/index.tsx | 12 +- components/ContentType/HotelPage/data.ts | 292 +++++++++++++++++- .../HotelReservation/HotelCard/index.tsx | 8 +- .../SelectRate/HotelInfoCard/index.tsx | 2 +- components/Icons/Accesories.tsx | 40 +++ components/Icons/Air.tsx | 36 +++ .../Icons/{Coffee.tsx => CoffeeAlt.tsx} | 6 +- components/Icons/ConvenienceStore24h.tsx | 40 +++ components/Icons/Cool.tsx | 36 +++ components/Icons/DoorOpen.tsx | 6 +- components/Icons/Dresser.tsx | 36 +++ components/Icons/ElectricCar.tsx | 40 +++ components/Icons/Fan.tsx | 36 +++ components/Icons/Footstool.tsx | 40 +++ components/Icons/Garage.tsx | 36 +++ components/Icons/Golf.tsx | 36 +++ components/Icons/Groceries.tsx | 40 +++ components/Icons/Hanger.tsx | 36 +++ components/Icons/HangerAlt.tsx | 40 +++ components/Icons/Heat.tsx | 36 +++ components/Icons/Kayaking.tsx | 40 +++ components/Icons/Kettle.tsx | 36 +++ components/Icons/Lamp.tsx | 36 +++ components/Icons/LaundryMachine.tsx | 40 +++ components/Icons/LocalBar.tsx | 40 +++ components/Icons/Nature.tsx | 36 +++ components/Icons/Nightlife.tsx | 40 +++ components/Icons/NoSmoking.tsx | 40 +++ components/Icons/OutdoorFurniture.tsx | 40 +++ components/Icons/RoomService.tsx | 40 +++ components/Icons/Skateboarding.tsx | 40 +++ components/Icons/Smoking.tsx | 36 +++ components/Icons/Spa.tsx | 36 +++ components/Icons/Street.tsx | 36 +++ components/Icons/Swim.tsx | 36 +++ components/Icons/Thermostat.tsx | 40 +++ components/Icons/Tshirt.tsx | 36 +++ components/Icons/TvCasting.tsx | 40 +++ components/Icons/get-icon-by-icon-name.ts | 102 +++++- components/Icons/index.tsx | 34 +- server/routers/hotels/output.ts | 9 +- server/routers/hotels/query.ts | 8 +- types/components/hotelPage/facilities.ts | 8 +- types/components/icon.ts | 40 ++- types/enums/facilities.ts | 262 ++++++++++++++++ utils/facilityCards.ts | 16 +- 47 files changed, 1970 insertions(+), 55 deletions(-) create mode 100644 components/Icons/Accesories.tsx create mode 100644 components/Icons/Air.tsx rename components/Icons/{Coffee.tsx => CoffeeAlt.tsx} (96%) create mode 100644 components/Icons/ConvenienceStore24h.tsx create mode 100644 components/Icons/Cool.tsx create mode 100644 components/Icons/Dresser.tsx create mode 100644 components/Icons/ElectricCar.tsx create mode 100644 components/Icons/Fan.tsx create mode 100644 components/Icons/Footstool.tsx create mode 100644 components/Icons/Garage.tsx create mode 100644 components/Icons/Golf.tsx create mode 100644 components/Icons/Groceries.tsx create mode 100644 components/Icons/Hanger.tsx create mode 100644 components/Icons/HangerAlt.tsx create mode 100644 components/Icons/Heat.tsx create mode 100644 components/Icons/Kayaking.tsx create mode 100644 components/Icons/Kettle.tsx create mode 100644 components/Icons/Lamp.tsx create mode 100644 components/Icons/LaundryMachine.tsx create mode 100644 components/Icons/LocalBar.tsx create mode 100644 components/Icons/Nature.tsx create mode 100644 components/Icons/Nightlife.tsx create mode 100644 components/Icons/NoSmoking.tsx create mode 100644 components/Icons/OutdoorFurniture.tsx create mode 100644 components/Icons/RoomService.tsx create mode 100644 components/Icons/Skateboarding.tsx create mode 100644 components/Icons/Smoking.tsx create mode 100644 components/Icons/Spa.tsx create mode 100644 components/Icons/Street.tsx create mode 100644 components/Icons/Swim.tsx create mode 100644 components/Icons/Thermostat.tsx create mode 100644 components/Icons/Tshirt.tsx create mode 100644 components/Icons/TvCasting.tsx create mode 100644 types/enums/facilities.ts diff --git a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css index f7dcea5fc..e35dc13a6 100644 --- a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css +++ b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css @@ -20,6 +20,10 @@ gap: var(--Spacing-x1); } +.icon { + flex-shrink: 0; +} + .showAllAmenities { width: fit-content; } diff --git a/components/ContentType/HotelPage/AmenitiesList/index.tsx b/components/ContentType/HotelPage/AmenitiesList/index.tsx index 8b6a1977e..8c90f19c2 100644 --- a/components/ContentType/HotelPage/AmenitiesList/index.tsx +++ b/components/ContentType/HotelPage/AmenitiesList/index.tsx @@ -16,9 +16,7 @@ export default async function AmenitiesList({ detailedFacilities, }: AmenitiesListProps) { const intl = await getIntl() - const sortedAmenities = detailedFacilities - .sort((a, b) => b.sortOrder - a.sortOrder) - .slice(0, 5) + const facilities = detailedFacilities.slice(0, 5) const lang = getLang() return (
@@ -26,11 +24,13 @@ export default async function AmenitiesList({ {intl.formatMessage({ id: "At the hotel" })}
- {sortedAmenities.map((facility) => { - const IconComponent = mapFacilityToIcon(facility.name) + {facilities.map((facility) => { + const IconComponent = mapFacilityToIcon(facility.id) return (
- {IconComponent && } + {IconComponent && ( + + )} {facility.name}
) diff --git a/components/ContentType/HotelPage/data.ts b/components/ContentType/HotelPage/data.ts index ace3a0408..e4043c53e 100644 --- a/components/ContentType/HotelPage/data.ts +++ b/components/ContentType/HotelPage/data.ts @@ -3,21 +3,287 @@ import { FC } from "react" import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import { IconName, IconProps } from "@/types/components/icon" +import { FacilityEnum } from "@/types/enums/facilities" -const facilityToIconMap: { [key: string]: IconName } = { - Bar: IconName.Bar, - "Bikes for loan": IconName.Biking, - Gym: IconName.Fitness, - "Free WiFi": IconName.Wifi, - //TODO: Ask design team what icon(s) should be used for meetings. - "Meeting rooms": IconName.People2, - "Meeting / conference facilities": IconName.People2, - "Pet-friendly rooms": IconName.Pets, - Sauna: IconName.Sauna, - Restaurant: IconName.Restaurant, +const facilityToIconMap: Record = { + [FacilityEnum.Bar]: IconName.LocalBar, + [FacilityEnum.Skybar]: IconName.LocalBar, + [FacilityEnum.RooftopBar]: IconName.LocalBar, + [FacilityEnum.BikesForLoan]: IconName.Biking, + [FacilityEnum.Gym]: IconName.Fitness, + [FacilityEnum.GymTrainingFacilities]: IconName.Fitness, + [FacilityEnum.KeyAccessOnlyToHealthClubGym]: IconName.Fitness, + [FacilityEnum.FreeWiFi]: IconName.Wifi, + [FacilityEnum.MeetingRooms]: IconName.People2, + [FacilityEnum.MeetingConferenceFacilities]: IconName.People2, + [FacilityEnum.PetFriendlyRooms]: IconName.Pets, + [FacilityEnum.Sauna]: IconName.Sauna, + [FacilityEnum.Restaurant]: IconName.Restaurant, + [FacilityEnum.ParkingGarage]: IconName.Garage, + [FacilityEnum.ParkingElectricCharging]: IconName.ElectricCar, + [FacilityEnum.ParkingFreeParking]: IconName.Parking, + [FacilityEnum.ParkingOutdoor]: IconName.Parking, + [FacilityEnum.ParkingAdditionalCost]: IconName.Parking, + [FacilityEnum.DisabledParking]: IconName.Parking, + [FacilityEnum.OutdoorTerrace]: IconName.OutdoorFurniture, + [FacilityEnum.RoomService]: IconName.RoomService, + [FacilityEnum.LaundryRoom]: IconName.LaundryMachine, + [FacilityEnum.LaundryService]: IconName.LaundryMachine, + [FacilityEnum.LaundryServiceExpress]: IconName.LaundryMachine, + [FacilityEnum.ScandicShop24Hrs]: IconName.ConvenienceStore24h, + [FacilityEnum.ServesBreakfastAlwaysIncluded]: IconName.CoffeeAlt, + [FacilityEnum.ServesBreakfastNotAlwaysIncluded]: IconName.CoffeeAlt, + [FacilityEnum.ServesOrganicBreakfastAlwaysIncluded]: IconName.CoffeeAlt, + [FacilityEnum.ServesOrganicBreakfastNotAlwaysIncluded]: IconName.CoffeeAlt, + [FacilityEnum.Breakfast]: IconName.CoffeeAlt, + [FacilityEnum.EBikesChargingStation]: IconName.ElectricBike, + [FacilityEnum.Shopping]: IconName.Shopping, + [FacilityEnum.Golf]: IconName.Golf, + [FacilityEnum.GolfCourse0To30Km]: IconName.Golf, + [FacilityEnum.TVWithChromecast1]: IconName.TvCasting, + [FacilityEnum.TVWithChromecast2]: IconName.TvCasting, + [FacilityEnum.DJLiveMusic]: IconName.Nightlife, + [FacilityEnum.DiscoNightClub]: IconName.Nightlife, + [FacilityEnum.CoffeeInReceptionAtCharge]: IconName.CoffeeAlt, + [FacilityEnum.CoffeeShop]: IconName.CoffeeAlt, + [FacilityEnum.CoffeeTeaFacilities]: IconName.CoffeeAlt, + [FacilityEnum.SkateboardsForLoan]: IconName.Skateboarding, + [FacilityEnum.KayaksForLoan]: IconName.Kayaking, + [FacilityEnum.LifestyleConcierge]: IconName.Concierge, + [FacilityEnum.WellnessAndSaunaEntranceFeeAdmission16PlusYears]: + IconName.Sauna, + [FacilityEnum.WellnessPoolSaunaEntranceFeeAdmission16PlusYears]: + IconName.Sauna, + [FacilityEnum.Cafe]: IconName.Restaurant, + [FacilityEnum.Pool]: IconName.Swim, + [FacilityEnum.PoolSwimmingPoolJacuzziAtHotel]: IconName.Swim, + [FacilityEnum.VendingMachineWithNecessities]: IconName.Groceries, + + [FacilityEnum.Jacuzzi]: IconName.StarFilled, + [FacilityEnum.JacuzziInRoom]: IconName.StarFilled, + + [FacilityEnum.AccessibleBathingControls]: IconName.StarFilled, + [FacilityEnum.AccessibleBathtubs]: IconName.StarFilled, + [FacilityEnum.AccessibleElevators]: IconName.StarFilled, + [FacilityEnum.AccessibleLightSwitch]: IconName.StarFilled, + [FacilityEnum.AccessibleRoomsAtHotel1]: IconName.StarFilled, + [FacilityEnum.AccessibleRoomsAtHotel2]: IconName.StarFilled, + [FacilityEnum.AccessibleToilets]: IconName.StarFilled, + [FacilityEnum.AccessibleWashBasins]: IconName.StarFilled, + [FacilityEnum.AdaptedRoomDoors]: IconName.StarFilled, + [FacilityEnum.AdjoiningConventionCentre]: IconName.StarFilled, + [FacilityEnum.AirConAirCooling]: IconName.StarFilled, + [FacilityEnum.AirConditioningInRoom]: IconName.StarFilled, + [FacilityEnum.AirportMaxDistance8Km]: IconName.StarFilled, + [FacilityEnum.AlarmsContinuouslyMonitored]: IconName.StarFilled, + [FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllGuestRooms]: + IconName.StarFilled, + [FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllHallways]: + IconName.StarFilled, + [FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllPublicAreas]: + IconName.StarFilled, + [FacilityEnum.AllAudibleSmokeAlarmsHardwired]: IconName.StarFilled, + [FacilityEnum.AllExteriorDoorsRequireKeyAccessAtNightOrAutomaticallyLock]: + IconName.StarFilled, + [FacilityEnum.AllGuestRoomDoorsHaveViewports]: IconName.StarFilled, + [FacilityEnum.AllGuestRoomDoorsSelfClosing]: IconName.StarFilled, + [FacilityEnum.AllParkingAreasPatrolled]: IconName.StarFilled, + [FacilityEnum.AllParkingAreasWellLit]: IconName.StarFilled, + [FacilityEnum.AllStairsWellsVentilated]: IconName.StarFilled, + [FacilityEnum.ArmchairBed]: IconName.StarFilled, + [FacilityEnum.AudibleAlarms]: IconName.StarFilled, + [FacilityEnum.AudibleSmokeAlarmsInAllHalls]: IconName.StarFilled, + [FacilityEnum.AudibleSmokeAlarmsInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.AudibleSmokeAlarmsInAllRooms]: IconName.StarFilled, + [FacilityEnum.AudioVisualEquipmentAvailable]: IconName.StarFilled, + [FacilityEnum.AutolinkFireDepartment]: IconName.StarFilled, + [FacilityEnum.AutomatedExternalDefibrillatorOnSiteAED]: IconName.StarFilled, + [FacilityEnum.AutomaticFireDoors]: IconName.StarFilled, + [FacilityEnum.AutoRecallElevators]: IconName.StarFilled, + [FacilityEnum.BalconiesAccessibleToAdjoiningRooms]: IconName.StarFilled, + [FacilityEnum.Ballroom]: IconName.StarFilled, + [FacilityEnum.Banquet]: IconName.StarFilled, + [FacilityEnum.BasicMedicalEquipmentOnSite]: IconName.StarFilled, + [FacilityEnum.BathroomsAdaptedForDisabledGuests]: IconName.StarFilled, + [FacilityEnum.Beach]: IconName.StarFilled, + [FacilityEnum.Beach0To1Km]: IconName.StarFilled, + [FacilityEnum.BeautySalon]: IconName.StarFilled, + [FacilityEnum.BedroomsWithWheelchairAccess]: IconName.StarFilled, + [FacilityEnum.Bowling]: IconName.StarFilled, + [FacilityEnum.BrailleLargePrintHotelLiterature]: IconName.StarFilled, + [FacilityEnum.BrailleLargePrintMenus]: IconName.StarFilled, + [FacilityEnum.Business1]: IconName.StarFilled, + [FacilityEnum.Business2]: IconName.StarFilled, + [FacilityEnum.BusinessCentre]: IconName.StarFilled, + [FacilityEnum.CashFree8pmTill6am]: IconName.StarFilled, + [FacilityEnum.CashFreeHotel]: IconName.StarFilled, + [FacilityEnum.ChildrenWelcome]: IconName.StarFilled, + [FacilityEnum.City]: IconName.StarFilled, + [FacilityEnum.ColourTVInRoomsAllScandicHotels]: IconName.StarFilled, + [FacilityEnum.ComplimentaryColdRefreshments]: IconName.StarFilled, + [FacilityEnum.CongressHall]: IconName.StarFilled, + [FacilityEnum.ConventionCentre]: IconName.StarFilled, + [FacilityEnum.Couples]: IconName.StarFilled, + [FacilityEnum.DeadboltsOnConnectingDoors]: IconName.StarFilled, + [FacilityEnum.DeadboltsSecondaryLocksOnAllGuestRoomDoors]: + IconName.StarFilled, + [FacilityEnum.Defibrillator]: IconName.StarFilled, + [FacilityEnum.Desk]: IconName.StarFilled, + [FacilityEnum.DirectDialPhoneInRoomsAllScandic]: IconName.StarFilled, + [FacilityEnum.DisabledEmergencyPlan1]: IconName.StarFilled, + [FacilityEnum.DisabledEmergencyPlan2]: IconName.StarFilled, + [FacilityEnum.DO_NOT_USE_Restaurant]: IconName.StarFilled, + [FacilityEnum.Downtown]: IconName.StarFilled, + [FacilityEnum.DrinkableTapWater]: IconName.StarFilled, + [FacilityEnum.DVDPlayer]: IconName.StarFilled, + [FacilityEnum.ElectronicKeyCards]: IconName.StarFilled, + [FacilityEnum.Elevator]: IconName.StarFilled, + [FacilityEnum.EmergencyBackUpGenerators]: IconName.StarFilled, + [FacilityEnum.EmergencyCallButtonOnPhone]: IconName.StarFilled, + [FacilityEnum.EmergencyCodesOrButtonsInRooms]: IconName.StarFilled, + [FacilityEnum.EmergencyEvacuationPlan1]: IconName.StarFilled, + [FacilityEnum.EmergencyEvacuationPlan2]: IconName.StarFilled, + [FacilityEnum.EmergencyEvaluationDrillFrequency]: IconName.StarFilled, + [FacilityEnum.EmergencyInfoInAllRooms]: IconName.StarFilled, + [FacilityEnum.EmergencyLightingAllScandic]: IconName.StarFilled, + [FacilityEnum.EmergencyLightningInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.EmergencyServiceResponseTimeInMinutes]: IconName.StarFilled, + [FacilityEnum.Entertainment]: IconName.StarFilled, + [FacilityEnum.EventVenue]: IconName.StarFilled, + [FacilityEnum.ExchangeFacility]: IconName.StarFilled, + [FacilityEnum.ExitMapsInRooms]: IconName.StarFilled, + [FacilityEnum.ExitSignsLit]: IconName.StarFilled, + [FacilityEnum.ExtraFamilyFriendly]: IconName.StarFilled, + [FacilityEnum.Families]: IconName.StarFilled, + [FacilityEnum.FaxFacilityInRoom]: IconName.StarFilled, + [FacilityEnum.Financial]: IconName.StarFilled, + [FacilityEnum.FireDetectorsAllScandic]: IconName.StarFilled, + [FacilityEnum.FireDetectorsInAllHalls]: IconName.StarFilled, + [FacilityEnum.FireDetectorsInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.FireDetectorsInAllRooms]: IconName.StarFilled, + [FacilityEnum.FireExtinguishersInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.FireExtinguishersInPublicAreasAllScandic]: IconName.StarFilled, + [FacilityEnum.FireSafetyAllScandic]: IconName.StarFilled, + [FacilityEnum.FirstAidAvailable]: IconName.StarFilled, + [FacilityEnum.FoodDrinks247]: IconName.StarFilled, + [FacilityEnum.GiftShop]: IconName.StarFilled, + [FacilityEnum.GuestRoomDoorsHaveASecondLock]: IconName.StarFilled, + [FacilityEnum.Hairdresser]: IconName.StarFilled, + [FacilityEnum.HairdryerInRoomAllScandic]: IconName.StarFilled, + [FacilityEnum.HandicapFacilities]: IconName.StarFilled, + [FacilityEnum.HandrailsInBathrooms]: IconName.StarFilled, + [FacilityEnum.HearingInductionLoops]: IconName.StarFilled, + [FacilityEnum.Highway1]: IconName.StarFilled, + [FacilityEnum.Highway2]: IconName.StarFilled, + [FacilityEnum.Hiking0To3Km]: IconName.StarFilled, + [FacilityEnum.HotelCompliesWithAAASecurityStandards]: IconName.StarFilled, + [FacilityEnum.HotelIsFollowingScandicsSafetySecurityPolicy]: + IconName.StarFilled, + [FacilityEnum.HotelWorksAccordingToScandicsAccessibilityConcepts]: + IconName.StarFilled, + [FacilityEnum.IceMachine]: IconName.StarFilled, + [FacilityEnum.IceMachineReception]: IconName.StarFilled, + [FacilityEnum.IDRequiredToReplaceAGuestRoomKey]: IconName.StarFilled, + [FacilityEnum.IfNoWhatAreTheHoursUse24ClockEx0000To0600]: IconName.StarFilled, + [FacilityEnum.InCountry]: IconName.StarFilled, + [FacilityEnum.IndustrialPark]: IconName.StarFilled, + [FacilityEnum.InternetHighSpeedInternetConnectionAllScandic]: + IconName.StarFilled, + [FacilityEnum.InternetHotSpotsAllScandic]: IconName.StarFilled, + [FacilityEnum.IroningRoom]: IconName.StarFilled, + [FacilityEnum.IronIroningBoardAllScandic]: IconName.StarFilled, + [FacilityEnum.KeyAccessOnlySecuredFloorsAvailable]: IconName.StarFilled, + [FacilityEnum.KidsPlayRoom]: IconName.StarFilled, + [FacilityEnum.KidsUpToAndIncluding12YearsStayForFree]: IconName.StarFilled, + [FacilityEnum.KitchenInRoom]: IconName.StarFilled, + [FacilityEnum.Lake0To1Km]: IconName.StarFilled, + [FacilityEnum.LakeOrSea0To1Km]: IconName.StarFilled, + [FacilityEnum.LaptopSafe]: IconName.StarFilled, + [FacilityEnum.Leisure]: IconName.StarFilled, + [FacilityEnum.LuggageLockers]: IconName.StarFilled, + [FacilityEnum.Massage]: IconName.StarFilled, + [FacilityEnum.MinibarInRoom]: IconName.StarFilled, + [FacilityEnum.MobileLift]: IconName.StarFilled, + [FacilityEnum.Mountains0To1Km]: IconName.StarFilled, + [FacilityEnum.MovieChannelsInRoomAllScandic]: IconName.StarFilled, + [FacilityEnum.MultipleExitsOnEachFloor]: IconName.StarFilled, + [FacilityEnum.NonSmokingRoomsAllScandic]: IconName.StarFilled, + [FacilityEnum.OnSiteTrainingFacilities]: IconName.StarFilled, + [FacilityEnum.OtherExplainInBriefDescription]: IconName.StarFilled, + [FacilityEnum.OvernightSecurity]: IconName.StarFilled, + [FacilityEnum.ParkingAttendant]: IconName.StarFilled, + [FacilityEnum.PCHookUpInRoom]: IconName.StarFilled, + [FacilityEnum.PillowAlarmsAvailable]: IconName.StarFilled, + [FacilityEnum.PlayStationInPlayArea]: IconName.StarFilled, + [FacilityEnum.PrintingService]: IconName.StarFilled, + [FacilityEnum.PropertyMeetsRequirementsFireSafety]: IconName.StarFilled, + [FacilityEnum.PublicAddressSystem]: IconName.StarFilled, + [FacilityEnum.RelaxationSuite]: IconName.StarFilled, + [FacilityEnum.RestrictedRoomAccessAllScandic]: IconName.StarFilled, + [FacilityEnum.RoomsAccessibleFromTheInterior]: IconName.StarFilled, + [FacilityEnum.RoomWindowsOpen]: IconName.StarFilled, + [FacilityEnum.RoomWindowsThatOpenHaveLockingDevice]: IconName.StarFilled, + [FacilityEnum.Rural1]: IconName.StarFilled, + [FacilityEnum.Rural2]: IconName.StarFilled, + [FacilityEnum.SafeDepositBoxInRoomsAllScandic]: IconName.StarFilled, + [FacilityEnum.SafeDepositBoxInRoomsCanHoldA17InchLaptop]: IconName.StarFilled, + [FacilityEnum.SafeDepositBoxInRoomsCannotHoldALaptop]: IconName.StarFilled, + [FacilityEnum.SafetyChainsOnGuestRoomDoor]: IconName.StarFilled, + [FacilityEnum.SecondaryLocksOnSlidingGlassDoors]: IconName.StarFilled, + [FacilityEnum.SecondaryLocksOnWindows]: IconName.StarFilled, + [FacilityEnum.Security24Hours]: IconName.StarFilled, + [FacilityEnum.SecurityEscortsAvailableOnRequest]: IconName.StarFilled, + [FacilityEnum.SecurityPersonnelOnSite]: IconName.StarFilled, + [FacilityEnum.SeparateFloorsForWomen]: IconName.StarFilled, + [FacilityEnum.ServiceGuideDogsAllowed]: IconName.StarFilled, + [FacilityEnum.ServiceSecurity24Hrs]: IconName.StarFilled, + [FacilityEnum.Skiing0To1Km]: IconName.StarFilled, + [FacilityEnum.SmokeDetectorsAllScandic]: IconName.StarFilled, + [FacilityEnum.Solarium]: IconName.StarFilled, + [FacilityEnum.SpecialNeedsMenus]: IconName.StarFilled, + [FacilityEnum.Sports]: IconName.StarFilled, + [FacilityEnum.SprinklersAllScandic]: IconName.StarFilled, + [FacilityEnum.SprinklersInAllHalls]: IconName.StarFilled, + [FacilityEnum.SprinklersInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.SprinklersInAllRooms]: IconName.StarFilled, + [FacilityEnum.StaffInDuplicateKeys]: IconName.StarFilled, + [FacilityEnum.StaffRedCrossCertifiedInCPR]: IconName.StarFilled, + [FacilityEnum.StaffTrainedForDisabledGuests]: IconName.StarFilled, + [FacilityEnum.StaffTrainedInAutomatedExternalDefibrillatorUsageAED]: + IconName.StarFilled, + [FacilityEnum.StaffTrainedInCPR]: IconName.StarFilled, + [FacilityEnum.StaffTrainedInFirstAid]: IconName.StarFilled, + [FacilityEnum.StaffTrainedInFirstAidTechniques]: IconName.StarFilled, + [FacilityEnum.StaffTrainedToCaterForDisabledGuestsAllScandic]: + IconName.StarFilled, + [FacilityEnum.Suburbs]: IconName.StarFilled, + [FacilityEnum.SwingboltLock]: IconName.StarFilled, + [FacilityEnum.TeleConferencingFacilitiesAvailable]: IconName.StarFilled, + [FacilityEnum.TelevisionsWithSubtitlesOrClosedCaptions]: IconName.StarFilled, + [FacilityEnum.Tennis1]: IconName.StarFilled, + [FacilityEnum.Tennis2]: IconName.StarFilled, + [FacilityEnum.TennisPadel]: IconName.StarFilled, + [FacilityEnum.Theatre]: IconName.StarFilled, + [FacilityEnum.TrouserPress]: IconName.StarFilled, + [FacilityEnum.UniformSecurityOnPremises]: IconName.StarFilled, + [FacilityEnum.UtilityRoomForIroning]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceInHallways]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceInPublicAreas]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceMonitored24HrsADay]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceOfAllParkingAreas]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceOfExteriorFrontEntrance]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceRecorded24HrsADayParkingArea]: + IconName.StarFilled, + [FacilityEnum.WallMountedCycleRack]: IconName.StarFilled, + [FacilityEnum.WellLitWalkways]: IconName.StarFilled, + [FacilityEnum.WheelchairAccess]: IconName.StarFilled, + [FacilityEnum.WideCorridors]: IconName.StarFilled, + [FacilityEnum.WideEntrance]: IconName.StarFilled, + [FacilityEnum.WideRestaurantEntrance]: IconName.StarFilled, + [FacilityEnum.WiFiWirelessInternetAccessAllScandic]: IconName.StarFilled, } -export function mapFacilityToIcon(facilityName: string): FC | null { - const iconName = facilityToIconMap[facilityName] +export function mapFacilityToIcon(id: FacilityEnum): FC | null { + const iconName = facilityToIconMap[id] return getIconByIconName(iconName) || null } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index ea3eb29b7..67ce49f17 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -22,9 +22,7 @@ export default async function HotelCard({ hotel }: HotelCardProps) { const { hotelData } = hotel const { price } = hotel - const sortedAmenities = hotelData.detailedFacilities - .sort((a, b) => b.sortOrder - a.sortOrder) - .slice(0, 5) + const amenities = hotelData.detailedFacilities.slice(0, 5) return (
@@ -57,8 +55,8 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
- {sortedAmenities.map((facility) => { - const IconComponent = mapFacilityToIcon(facility.name) + {amenities.map((facility) => { + const IconComponent = mapFacilityToIcon(facility.id) return (
{IconComponent && } diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 5e0a24623..1ec58dd07 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -67,7 +67,7 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { {intl.formatMessage({ id: "At the hotel" })} {sortedFacilities?.map((facility) => { - const IconComponent = mapFacilityToIcon(facility.name) + const IconComponent = mapFacilityToIcon(facility.id) return (
{IconComponent && ( diff --git a/components/Icons/Accesories.tsx b/components/Icons/Accesories.tsx new file mode 100644 index 000000000..9aaf0c894 --- /dev/null +++ b/components/Icons/Accesories.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function AccesoriesIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Air.tsx b/components/Icons/Air.tsx new file mode 100644 index 000000000..239566515 --- /dev/null +++ b/components/Icons/Air.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function AirIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Coffee.tsx b/components/Icons/CoffeeAlt.tsx similarity index 96% rename from components/Icons/Coffee.tsx rename to components/Icons/CoffeeAlt.tsx index 840f78b0d..e76da5126 100644 --- a/components/Icons/Coffee.tsx +++ b/components/Icons/CoffeeAlt.tsx @@ -2,7 +2,11 @@ import { iconVariants } from "./variants" import type { IconProps } from "@/types/components/icon" -export default function CoffeeIcon({ className, color, ...props }: IconProps) { +export default function CoffeeAltIcon({ + className, + color, + ...props +}: IconProps) { const classNames = iconVariants({ className, color }) return ( + + + + + + + + ) +} diff --git a/components/Icons/Cool.tsx b/components/Icons/Cool.tsx new file mode 100644 index 000000000..efa5ae67f --- /dev/null +++ b/components/Icons/Cool.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function CoolIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/DoorOpen.tsx b/components/Icons/DoorOpen.tsx index f70c29859..93bc2caf4 100644 --- a/components/Icons/DoorOpen.tsx +++ b/components/Icons/DoorOpen.tsx @@ -2,7 +2,11 @@ import { iconVariants } from "./variants" import type { IconProps } from "@/types/components/icon" -export default function CoffeeIcon({ className, color, ...props }: IconProps) { +export default function DoorOpenIcon({ + className, + color, + ...props +}: IconProps) { const classNames = iconVariants({ className, color }) return ( + + + + + + + + ) +} diff --git a/components/Icons/ElectricCar.tsx b/components/Icons/ElectricCar.tsx new file mode 100644 index 000000000..1f9500b64 --- /dev/null +++ b/components/Icons/ElectricCar.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ElectricCarIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Fan.tsx b/components/Icons/Fan.tsx new file mode 100644 index 000000000..128200104 --- /dev/null +++ b/components/Icons/Fan.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function FanIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Footstool.tsx b/components/Icons/Footstool.tsx new file mode 100644 index 000000000..becb0fd80 --- /dev/null +++ b/components/Icons/Footstool.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function FootstoolIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Garage.tsx b/components/Icons/Garage.tsx new file mode 100644 index 000000000..b715532cc --- /dev/null +++ b/components/Icons/Garage.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function GarageIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Golf.tsx b/components/Icons/Golf.tsx new file mode 100644 index 000000000..027c906d7 --- /dev/null +++ b/components/Icons/Golf.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function GolfIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Groceries.tsx b/components/Icons/Groceries.tsx new file mode 100644 index 000000000..404f09f7b --- /dev/null +++ b/components/Icons/Groceries.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function GroceriesIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Hanger.tsx b/components/Icons/Hanger.tsx new file mode 100644 index 000000000..63c869619 --- /dev/null +++ b/components/Icons/Hanger.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function HangerIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/HangerAlt.tsx b/components/Icons/HangerAlt.tsx new file mode 100644 index 000000000..732656e60 --- /dev/null +++ b/components/Icons/HangerAlt.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function HangerAltIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Heat.tsx b/components/Icons/Heat.tsx new file mode 100644 index 000000000..ef093e955 --- /dev/null +++ b/components/Icons/Heat.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function HeatIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Kayaking.tsx b/components/Icons/Kayaking.tsx new file mode 100644 index 000000000..1d4061a3a --- /dev/null +++ b/components/Icons/Kayaking.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function KayakingIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Kettle.tsx b/components/Icons/Kettle.tsx new file mode 100644 index 000000000..28713719a --- /dev/null +++ b/components/Icons/Kettle.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function KettleIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Lamp.tsx b/components/Icons/Lamp.tsx new file mode 100644 index 000000000..bde8af3dd --- /dev/null +++ b/components/Icons/Lamp.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function LampIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/LaundryMachine.tsx b/components/Icons/LaundryMachine.tsx new file mode 100644 index 000000000..b4225197d --- /dev/null +++ b/components/Icons/LaundryMachine.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function LaundryMachineIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/LocalBar.tsx b/components/Icons/LocalBar.tsx new file mode 100644 index 000000000..7a5b3bb51 --- /dev/null +++ b/components/Icons/LocalBar.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function LocalBarIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Nature.tsx b/components/Icons/Nature.tsx new file mode 100644 index 000000000..4c48c377f --- /dev/null +++ b/components/Icons/Nature.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function NatureIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Nightlife.tsx b/components/Icons/Nightlife.tsx new file mode 100644 index 000000000..093099cb9 --- /dev/null +++ b/components/Icons/Nightlife.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function NightlifeIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/NoSmoking.tsx b/components/Icons/NoSmoking.tsx new file mode 100644 index 000000000..bdaa7d3f3 --- /dev/null +++ b/components/Icons/NoSmoking.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function NoSmokingIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/OutdoorFurniture.tsx b/components/Icons/OutdoorFurniture.tsx new file mode 100644 index 000000000..3ddac9f38 --- /dev/null +++ b/components/Icons/OutdoorFurniture.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function OutdoorFurnitureIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/RoomService.tsx b/components/Icons/RoomService.tsx new file mode 100644 index 000000000..00aadd7d0 --- /dev/null +++ b/components/Icons/RoomService.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function RoomServiceIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Skateboarding.tsx b/components/Icons/Skateboarding.tsx new file mode 100644 index 000000000..6c0106ff3 --- /dev/null +++ b/components/Icons/Skateboarding.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function SkateboardingIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Smoking.tsx b/components/Icons/Smoking.tsx new file mode 100644 index 000000000..58c30abce --- /dev/null +++ b/components/Icons/Smoking.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function SmokingIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Spa.tsx b/components/Icons/Spa.tsx new file mode 100644 index 000000000..f3141a4e4 --- /dev/null +++ b/components/Icons/Spa.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function SpaIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Street.tsx b/components/Icons/Street.tsx new file mode 100644 index 000000000..414df197c --- /dev/null +++ b/components/Icons/Street.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function StreetIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Swim.tsx b/components/Icons/Swim.tsx new file mode 100644 index 000000000..abd2bd29a --- /dev/null +++ b/components/Icons/Swim.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function SwimIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Thermostat.tsx b/components/Icons/Thermostat.tsx new file mode 100644 index 000000000..2fd3ebe97 --- /dev/null +++ b/components/Icons/Thermostat.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ThermostatIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Tshirt.tsx b/components/Icons/Tshirt.tsx new file mode 100644 index 000000000..e6643725b --- /dev/null +++ b/components/Icons/Tshirt.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function TshirtIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/TvCasting.tsx b/components/Icons/TvCasting.tsx new file mode 100644 index 000000000..ea6e7b90d --- /dev/null +++ b/components/Icons/TvCasting.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function TvCastingIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 15752b50c..f5410421f 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -4,8 +4,10 @@ import FacebookIcon from "./Facebook" import InstagramIcon from "./Instagram" import TripAdvisorIcon from "./TripAdvisor" import { + AccesoriesIcon, AccessibilityIcon, AccountCircleIcon, + AirIcon, AirplaneIcon, ArrowRightIcon, BarIcon, @@ -22,27 +24,48 @@ import { ChevronRightSmallIcon, CloseIcon, CloseLargeIcon, - CoffeeIcon, + CoffeeAltIcon, ConciergeIcon, + ConvenienceStore24hIcon, + CoolIcon, CrossCircle, CulturalIcon, DoorOpenIcon, + DresserIcon, ElectricBikeIcon, + ElectricCarIcon, EmailIcon, EyeHideIcon, EyeShowIcon, + FanIcon, FitnessIcon, + FootstoolIcon, GalleryIcon, + GarageIcon, GiftIcon, GlobeIcon, + GolfIcon, + GroceriesIcon, + HangerAltIcon, + HangerIcon, + HeatIcon, HouseIcon, ImageIcon, InfoCircleIcon, + KayakingIcon, + KettleIcon, + LampIcon, + LaundryMachineIcon, + LocalBarIcon, LocationIcon, LockIcon, MapIcon, MinusIcon, MuseumIcon, + NatureIcon, + NightlifeIcon, + NoSmokingIcon, + OutdoorFurnitureIcon, ParkingIcon, People2Icon, PersonIcon, @@ -51,14 +74,23 @@ import { PlusCircleIcon, PlusIcon, RestaurantIcon, + RoomServiceIcon, SaunaIcon, SearchIcon, ServiceIcon, ShoppingIcon, + SkateboardingIcon, + SmokingIcon, SnowflakeIcon, + SpaIcon, StarFilledIcon, + StreetIcon, + SwimIcon, + ThermostatIcon, TrainIcon, + TshirtIcon, TshirtWashIcon, + TvCastingIcon, WarningTriangle, WifiIcon, } from "." @@ -67,10 +99,14 @@ import { IconName, IconProps } from "@/types/components/icon" export function getIconByIconName(icon?: IconName): FC | null { switch (icon) { + case IconName.Accesories: + return AccesoriesIcon case IconName.Accessibility: return AccessibilityIcon case IconName.AccountCircle: return AccountCircleIcon + case IconName.Air: + return AirIcon case IconName.Airplane: return AirplaneIcon case IconName.ArrowRight: @@ -105,32 +141,56 @@ export function getIconByIconName(icon?: IconName): FC | null { return CloseIcon case IconName.CloseLarge: return CloseLargeIcon - case IconName.Coffee: - return CoffeeIcon + case IconName.ConvenienceStore24h: + return ConvenienceStore24hIcon + case IconName.Cool: + return CoolIcon + case IconName.CoffeeAlt: + return CoffeeAltIcon case IconName.Concierge: return ConciergeIcon case IconName.Cultural: return CulturalIcon case IconName.DoorOpen: return DoorOpenIcon + case IconName.Dresser: + return DresserIcon case IconName.ElectricBike: return ElectricBikeIcon + case IconName.ElectricCar: + return ElectricCarIcon case IconName.Email: return EmailIcon case IconName.EyeHide: return EyeHideIcon case IconName.EyeShow: return EyeShowIcon + case IconName.Fan: + return FanIcon case IconName.Facebook: return FacebookIcon case IconName.Fitness: return FitnessIcon + case IconName.Footstool: + return FootstoolIcon case IconName.Gallery: return GalleryIcon + case IconName.Garage: + return GarageIcon case IconName.Gift: return GiftIcon case IconName.Globe: return GlobeIcon + case IconName.Golf: + return GolfIcon + case IconName.Groceries: + return GroceriesIcon + case IconName.Hanger: + return HangerIcon + case IconName.HangerAlt: + return HangerAltIcon + case IconName.Heat: + return HeatIcon case IconName.House: return HouseIcon case IconName.Image: @@ -139,6 +199,16 @@ export function getIconByIconName(icon?: IconName): FC | null { return InfoCircleIcon case IconName.Instagram: return InstagramIcon + case IconName.Kayaking: + return KayakingIcon + case IconName.Kettle: + return KettleIcon + case IconName.Lamp: + return LampIcon + case IconName.LaundryMachine: + return LaundryMachineIcon + case IconName.LocalBar: + return LocalBarIcon case IconName.Location: return LocationIcon case IconName.Lock: @@ -149,6 +219,14 @@ export function getIconByIconName(icon?: IconName): FC | null { return MinusIcon case IconName.Museum: return MuseumIcon + case IconName.Nature: + return NatureIcon + case IconName.Nightlife: + return NightlifeIcon + case IconName.NoSmoking: + return NoSmokingIcon + case IconName.OutdoorFurniture: + return OutdoorFurnitureIcon case IconName.Parking: return ParkingIcon case IconName.Person: @@ -165,6 +243,12 @@ export function getIconByIconName(icon?: IconName): FC | null { return PlusCircleIcon case IconName.Restaurant: return RestaurantIcon + case IconName.RoomService: + return RoomServiceIcon + case IconName.Smoking: + return SmokingIcon + case IconName.Spa: + return SpaIcon case IconName.Sauna: return SaunaIcon case IconName.Search: @@ -173,16 +257,28 @@ export function getIconByIconName(icon?: IconName): FC | null { return ServiceIcon case IconName.Shopping: return ShoppingIcon + case IconName.Skateboarding: + return SkateboardingIcon case IconName.Snowflake: return SnowflakeIcon case IconName.StarFilled: return StarFilledIcon + case IconName.Street: + return StreetIcon + case IconName.Swim: + return SwimIcon + case IconName.Thermostat: + return ThermostatIcon + case IconName.Tshirt: + return TshirtIcon case IconName.Train: return TrainIcon case IconName.Tripadvisor: return TripAdvisorIcon case IconName.TshirtWash: return TshirtWashIcon + case IconName.TvCasting: + return TvCastingIcon case IconName.WarningTriangle: return WarningTriangle case IconName.Wifi: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index e8a0b6433..8a7764dd6 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -1,5 +1,7 @@ +export { default as AccesoriesIcon } from "./Accesories" 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 ArrowRightIcon } from "./ArrowRight" export { default as BarIcon } from "./Bar" @@ -17,34 +19,55 @@ export { default as ChevronRightIcon } from "./ChevronRight" export { default as ChevronRightSmallIcon } from "./ChevronRightSmall" export { default as CloseIcon } from "./Close" export { default as CloseLargeIcon } from "./CloseLarge" -export { default as CoffeeIcon } from "./Coffee" +export { default as CoffeeAltIcon } from "./CoffeeAlt" export { default as ConciergeIcon } from "./Concierge" +export { default as ConvenienceStore24hIcon } from "./ConvenienceStore24h" +export { default as CoolIcon } from "./Cool" export { default as CreditCard } from "./CreditCard" export { default as CrossCircle } from "./CrossCircle" export { default as CulturalIcon } from "./Cultural" export { default as DeleteIcon } from "./Delete" export { default as DoorOpenIcon } from "./DoorOpen" +export { default as DresserIcon } from "./Dresser" export { default as EditIcon } from "./Edit" export { default as ElectricBikeIcon } from "./ElectricBike" +export { default as ElectricCarIcon } from "./ElectricCar" export { default as EmailIcon } from "./Email" export { default as ErrorCircleIcon } from "./ErrorCircle" export { default as EyeHideIcon } from "./EyeHide" export { default as EyeShowIcon } from "./EyeShow" +export { default as FanIcon } from "./Fan" export { default as FitnessIcon } from "./Fitness" +export { default as FootstoolIcon } from "./Footstool" export { default as GalleryIcon } from "./Gallery" +export { default as GarageIcon } from "./Garage" export { default as GiftIcon } from "./Gift" export { default as GlobeIcon } from "./Globe" +export { default as GolfIcon } from "./Golf" +export { default as GroceriesIcon } from "./Groceries" +export { default as HangerIcon } from "./Hanger" +export { default as HangerAltIcon } from "./HangerAlt" export { default as HeartIcon } from "./Heart" +export { default as HeatIcon } from "./Heat" export { default as HouseIcon } from "./House" export { default as ImageIcon } from "./Image" export { default as InfoCircleIcon } from "./InfoCircle" +export { default as KayakingIcon } from "./Kayaking" +export { default as KettleIcon } from "./Kettle" export { default as KingBedIcon } from "./KingBed" +export { default as LampIcon } from "./Lamp" +export { default as LaundryMachineIcon } from "./LaundryMachine" +export { default as LocalBarIcon } from "./LocalBar" export { default as LocationIcon } from "./Location" export { default as LockIcon } from "./Lock" export { default as MapIcon } from "./Map" export { default as MinusIcon } from "./Minus" export { default as MuseumIcon } from "./Museum" +export { default as NatureIcon } from "./Nature" +export { default as NightlifeIcon } from "./Nightlife" export { default as NoBreakfastIcon } from "./NoBreakfast" +export { default as NoSmokingIcon } from "./NoSmoking" +export { default as OutdoorFurnitureIcon } from "./OutdoorFurniture" export { default as ParkingIcon } from "./Parking" export { default as People2Icon } from "./People2" export { default as PersonIcon } from "./Person" @@ -54,14 +77,23 @@ export { default as PlusIcon } from "./Plus" export { default as PlusCircleIcon } from "./PlusCircle" export { default as PriceTagIcon } from "./PriceTag" export { default as RestaurantIcon } from "./Restaurant" +export { default as RoomServiceIcon } from "./RoomService" export { default as SaunaIcon } from "./Sauna" export { default as ScandicLogoIcon } from "./ScandicLogo" export { default as SearchIcon } from "./Search" export { default as ServiceIcon } from "./Service" export { default as ShoppingIcon } from "./Shopping" +export { default as SkateboardingIcon } from "./Skateboarding" +export { default as SmokingIcon } from "./Smoking" export { default as SnowflakeIcon } from "./Snowflake" +export { default as SpaIcon } from "./Spa" export { default as StarFilledIcon } from "./StarFilled" +export { default as StreetIcon } from "./Street" +export { default as SwimIcon } from "./Swim" +export { default as ThermostatIcon } from "./Thermostat" export { default as TrainIcon } from "./Train" +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 WifiIcon } from "./Wifi" diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 5692497f1..89e286185 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -6,6 +6,7 @@ import { imageMetaDataSchema, imageSizesSchema } from "./schemas/image" import { roomSchema } from "./schemas/room" import { getPoiGroupByCategoryName } from "./utils" +import { FacilityEnum } from "@/types/enums/facilities" import { PointOfInterestCategoryNameEnum } from "@/types/hotel" const ratingsSchema = z @@ -142,7 +143,7 @@ const hotelContentSchema = z.object({ }) const detailedFacilitySchema = z.object({ - id: z.number(), + id: z.nativeEnum(FacilityEnum), name: z.string(), public: z.boolean(), sortOrder: z.number(), @@ -402,7 +403,11 @@ export const getHotelDataSchema = z.object({ }), location: locationSchema, hotelContent: hotelContentSchema, - detailedFacilities: z.array(detailedFacilitySchema), + detailedFacilities: z + .array(detailedFacilitySchema) + .transform((facilities) => + facilities.sort((a, b) => b.sortOrder - a.sortOrder) + ), healthFacilities: z.array(healthFacilitySchema), merchantInformationData: merchantInformationSchema, rewardNight: rewardNightSchema, diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 4b3f1a84e..2fa4cfbe8 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -46,7 +46,7 @@ import { TWENTYFOUR_HOURS, } from "./utils" -import { FacilityEnum } from "@/types/components/hotelPage/facilities" +import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { Facility } from "@/types/hotel" @@ -231,15 +231,15 @@ export const hotelQueryRouter = router({ const facilities: Facility[] = [ { ...apiJson.data.attributes.restaurantImages, - id: FacilityEnum.restaurant, + id: FacilityCardTypeEnum.restaurant, }, { ...apiJson.data.attributes.conferencesAndMeetings, - id: FacilityEnum.conference, + id: FacilityCardTypeEnum.conference, }, { ...apiJson.data.attributes.healthAndWellness, - id: FacilityEnum.wellness, + id: FacilityCardTypeEnum.wellness, }, ] diff --git a/types/components/hotelPage/facilities.ts b/types/components/hotelPage/facilities.ts index 98a6a444d..333313fe0 100644 --- a/types/components/hotelPage/facilities.ts +++ b/types/components/hotelPage/facilities.ts @@ -34,7 +34,7 @@ export type CardGridProps = { facilitiesCardGrid: FacilityGrid } -export enum FacilityEnum { +export enum FacilityCardTypeEnum { wellness = "wellness-and-exercise", conference = "meetings-and-conferences", restaurant = "restaurant-and-bar", @@ -46,9 +46,3 @@ export enum RestaurantHeadings { restaurant = "Restaurant", breakfastRestaurant = "Breakfast restaurant", } - -export enum FacilityIds { - bar = 1606, - rooftopBar = 1014, - restaurant = 1383, -} diff --git a/types/components/icon.ts b/types/components/icon.ts index f50a79259..290c9ed32 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -7,8 +7,10 @@ export interface IconProps VariantProps {} export enum IconName { + Accesories = "Accesories", Accessibility = "Accessibility", AccountCircle = "AccountCircle", + Air = "Air", Airplane = "Airplane", ArrowRight = "ArrowRight", Bar = "Bar", @@ -18,7 +20,6 @@ export enum IconName { Camera = "Camera", Cellphone = "Cellphone", Check = "Check", - CrossCircle = "CrossCircle", CheckCircle = "CheckCircle", ChevronDown = "ChevronDown", ChevronLeft = "ChevronLeft", @@ -26,45 +27,76 @@ export enum IconName { ChevronRightSmall = "ChevronRightSmall", Close = "Close", CloseLarge = "CloseLarge", - Coffee = "Coffee", + CoffeeAlt = "CoffeeAlt", Concierge = "Concierge", + ConvenienceStore24h = "ConvenienceStore24h", + Cool = "Cool", + CrossCircle = "CrossCircle", Cultural = "Cultural", DoorOpen = "DoorOpen", + Dresser = "Dresser", ElectricBike = "ElectricBike", + ElectricCar = "ElectricCar", Email = "Email", EyeHide = "EyeHide", EyeShow = "EyeShow", Facebook = "Facebook", + Fan = "Fan", Fitness = "Fitness", + Footstool = "Footstool", Gallery = "Gallery", + Garage = "Garage", Gift = "Gift", Globe = "Globe", + Golf = "Golf", + Groceries = "Groceries", + Hanger = "Hanger", + HangerAlt = "HangerAlt", + Heat = "Heat", House = "House", Image = "Image", InfoCircle = "InfoCircle", Instagram = "Instagram", + Kayaking = "Kayaking", + Kettle = "Kettle", + Lamp = "Lamp", + LaundryMachine = "LaundryMachine", + LocalBar = "LocalBar", Location = "Location", Lock = "Lock", Map = "Map", Minus = "Minus", Museum = "Museum", + Nature = "Nature", + Nightlife = "Nightlife", + NoSmoking = "NoSmoking", + OutdoorFurniture = "OutdoorFurniture", Parking = "Parking", - Person = "Person", People2 = "People2", + Person = "Person", Pets = "Pets", Phone = "Phone", Plus = "Plus", PlusCircle = "PlusCircle", Restaurant = "Restaurant", + RoomService = "RoomService", Sauna = "Sauna", Search = "Search", Service = "Service", Shopping = "Shopping", + Skateboarding = "Skateboarding", + Smoking = "Smoking", Snowflake = "Snowflake", + Spa = "Spa", StarFilled = "StarFilled", + Street = "Street", + Swim = "Swim", + Thermostat = "Thermostat", Train = "Train", Tripadvisor = "Tripadvisor", + Tshirt = "Tshirt", TshirtWash = "TshirtWash", - Wifi = "Wifi", + TvCasting = "TvCasting", WarningTriangle = "WarningTriangle", + Wifi = "Wifi", } diff --git a/types/enums/facilities.ts b/types/enums/facilities.ts new file mode 100644 index 000000000..169aad6c0 --- /dev/null +++ b/types/enums/facilities.ts @@ -0,0 +1,262 @@ +export enum FacilityEnum { + AccessibleBathingControls = 2065, + AccessibleBathtubs = 2062, + AccessibleElevators = 2067, + AccessibleLightSwitch = 2066, + AccessibleRoomsAtHotel1 = 2659, + AccessibleRoomsAtHotel2 = 3010, + AccessibleToilets = 2068, + AccessibleWashBasins = 2063, + AdaptedRoomDoors = 2080, + AdjoiningConventionCentre = 1560, + AirConAirCooling = 2660, + AirConditioningInRoom = 5763, + AirportMaxDistance8Km = 1856, + AlarmsContinuouslyMonitored = 1876, + AlarmsHaveStrobeLightsForDeafHardHearingInAllGuestRooms = 1877, + AlarmsHaveStrobeLightsForDeafHardHearingInAllHallways = 1878, + AlarmsHaveStrobeLightsForDeafHardHearingInAllPublicAreas = 1879, + AllAudibleSmokeAlarmsHardwired = 1884, + AllExteriorDoorsRequireKeyAccessAtNightOrAutomaticallyLock = 2004, + AllGuestRoomDoorsHaveViewports = 2006, + AllGuestRoomDoorsSelfClosing = 2007, + AllParkingAreasPatrolled = 1933, + AllParkingAreasWellLit = 2013, + AllStairsWellsVentilated = 2053, + ArmchairBed = 104126, + AudibleAlarms = 1880, + AudibleSmokeAlarmsInAllHalls = 1881, + AudibleSmokeAlarmsInAllPublicAreas = 1882, + AudibleSmokeAlarmsInAllRooms = 1883, + AudioVisualEquipmentAvailable = 961, + AutolinkFireDepartment = 1886, + AutomatedExternalDefibrillatorOnSiteAED = 1917, + AutomaticFireDoors = 1887, + AutoRecallElevators = 1885, + BalconiesAccessibleToAdjoiningRooms = 1962, + Ballroom = 1609, + Banquet = 1557, + Bar = 1014, + BasicMedicalEquipmentOnSite = 1920, + BathroomsAdaptedForDisabledGuests = 2064, + Beach = 1827, + Beach0To1Km = 1019, + BeautySalon = 1015, + BedroomsWithWheelchairAccess = 2081, + BikesForLoan = 5550, + Bowling = 185105, + BrailleLargePrintHotelLiterature = 2069, + BrailleLargePrintMenus = 2070, + Breakfast = 5807, + Business1 = 1385, + Business2 = 83715, + BusinessCentre = 962, + Cafe = 1381, + CashFree8pmTill6am = 327877, + CashFreeHotel = 345180, + ChildrenWelcome = 1828, + City = 1857, + CoffeeInReceptionAtCharge = 332224, + CoffeeShop = 956, + CoffeeTeaFacilities = 5776, + ColourTVInRoomsAllScandicHotels = 5773, + ComplimentaryColdRefreshments = 157965, + CongressHall = 1558, + ConventionCentre = 963, + Couples = 2663, + DeadboltsOnConnectingDoors = 1990, + DeadboltsSecondaryLocksOnAllGuestRoomDoors = 1985, + Defibrillator = 99872, + Desk = 5777, + DirectDialPhoneInRoomsAllScandic = 5772, + DisabledEmergencyPlan1 = 1888, + DisabledEmergencyPlan2 = 2074, + DisabledParking = 2072, + DiscoNightClub = 958, + DJLiveMusic = 162587, + DO_NOT_USE_Restaurant = 1377, + Downtown = 969, + DrinkableTapWater = 5553, + DVDPlayer = 5778, + EBikesChargingStation = 265711, + ElectronicKeyCards = 1994, + Elevator = 959, + EmergencyBackUpGenerators = 1889, + EmergencyCallButtonOnPhone = 1998, + EmergencyCodesOrButtonsInRooms = 2075, + EmergencyEvacuationPlan1 = 1890, + EmergencyEvacuationPlan2 = 1895, + EmergencyEvaluationDrillFrequency = 1896, + EmergencyInfoInAllRooms = 1897, + EmergencyLightingAllScandic = 952, + EmergencyLightningInAllPublicAreas = 1898, + EmergencyServiceResponseTimeInMinutes = 1899, + Entertainment = 970, + EventVenue = 1559, + ExchangeFacility = 1605, + ExitMapsInRooms = 1900, + ExitSignsLit = 1901, + ExtraFamilyFriendly = 242920, + Families = 2664, + FaxFacilityInRoom = 5764, + Financial = 1409, + FireDetectorsAllScandic = 1869, + FireDetectorsInAllHalls = 1903, + FireDetectorsInAllPublicAreas = 1905, + FireDetectorsInAllRooms = 1906, + FireExtinguishersInAllPublicAreas = 1907, + FireExtinguishersInPublicAreasAllScandic = 1870, + FireSafetyAllScandic = 1871, + FirstAidAvailable = 1915, + FoodDrinks247 = 324100, + FreeWiFi = 1833, + GiftShop = 1376, + Golf = 1016, + GolfCourse0To30Km = 1607, + GuestRoomDoorsHaveASecondLock = 2005, + Gym = 1829, + GymTrainingFacilities = 2669, + Hairdresser = 348860, + HairdryerInRoomAllScandic = 5765, + HandicapFacilities = 2076, + HandrailsInBathrooms = 2078, + HearingInductionLoops = 2077, + Highway1 = 1858, + Highway2 = 1864, + Hiking0To3Km = 239346, + HotelCompliesWithAAASecurityStandards = 2011, + HotelIsFollowingScandicsSafetySecurityPolicy = 5559, + HotelWorksAccordingToScandicsAccessibilityConcepts = 5560, + IceMachine = 1405, + IceMachineReception = 332194, + IDRequiredToReplaceAGuestRoomKey = 2024, + IfNoWhatAreTheHoursUse24ClockEx0000To0600 = 1912, + InCountry = 1867, + IndustrialPark = 1859, + InternetHighSpeedInternetConnectionAllScandic = 5804, + InternetHotSpotsAllScandic = 1832, + IroningRoom = 238849, + IronIroningBoardAllScandic = 5780, + Jacuzzi = 162573, + JacuzziInRoom = 5766, + KayaksForLoan = 162585, + KeyAccessOnlySecuredFloorsAvailable = 2050, + KeyAccessOnlyToHealthClubGym = 2061, + KidsPlayRoom = 239349, + KidsUpToAndIncluding12YearsStayForFree = 5561, + KitchenInRoom = 5767, + Lake0To1Km = 1865, + LakeOrSea0To1Km = 245437, + LaptopSafe = 5283, + LaundryRoom = 326031, + LaundryService = 1834, + LaundryServiceExpress = 162583, + Leisure = 83716, + LifestyleConcierge = 162584, + LuggageLockers = 324098, + Massage = 348859, + MeetingConferenceFacilities = 5806, + MeetingRooms = 1017, + MinibarInRoom = 5768, + MobileLift = 113185, + Mountains0To1Km = 1866, + MovieChannelsInRoomAllScandic = 5770, + MultipleExitsOnEachFloor = 2012, + NonSmokingRoomsAllScandic = 5771, + OnSiteTrainingFacilities = 3014, + OtherExplainInBriefDescription = 1608, + OutdoorTerrace = 1382, + OvernightSecurity = 1913, + ParkingAdditionalCost = 1406, + ParkingAttendant = 1914, + ParkingElectricCharging = 5554, + ParkingFreeParking = 5562, + ParkingGarage = 2665, + ParkingOutdoor = 162574, + PCHookUpInRoom = 5769, + PetFriendlyRooms = 1835, + PillowAlarmsAvailable = 2079, + PlayStationInPlayArea = 175449, + Pool = 1831, + PoolSwimmingPoolJacuzziAtHotel = 2667, + PrintingService = 1380, + PropertyMeetsRequirementsFireSafety = 1875, + PublicAddressSystem = 2014, + RelaxationSuite = 5564, + Restaurant = 1383, + RestrictedRoomAccessAllScandic = 1872, + RooftopBar = 239348, + RoomsAccessibleFromTheInterior = 950, + RoomService = 1378, + RoomWindowsOpen = 2016, + RoomWindowsThatOpenHaveLockingDevice = 2020, + Rural1 = 1861, + Rural2 = 1868, + SafeDepositBoxInRoomsAllScandic = 5775, + SafeDepositBoxInRoomsCanHoldA17InchLaptop = 200124, + SafeDepositBoxInRoomsCannotHoldALaptop = 200123, + SafetyChainsOnGuestRoomDoor = 2047, + Sauna = 1379, + ScandicShop24Hrs = 1408, + SecondaryLocksOnSlidingGlassDoors = 2048, + SecondaryLocksOnWindows = 2049, + Security24Hours = 1911, + SecurityEscortsAvailableOnRequest = 1936, + SecurityPersonnelOnSite = 1934, + SeparateFloorsForWomen = 2051, + ServesBreakfastAlwaysIncluded = 1407, + ServesBreakfastNotAlwaysIncluded = 5556, + ServesOrganicBreakfastAlwaysIncluded = 5557, + ServesOrganicBreakfastNotAlwaysIncluded = 5558, + ServiceGuideDogsAllowed = 2071, + ServiceSecurity24Hrs = 326033, + Shopping = 971, + SkateboardsForLoan = 162586, + Skiing0To1Km = 245031, + Skybar = 1606, + SmokeDetectorsAllScandic = 1873, + Solarium = 1830, + SpecialNeedsMenus = 2082, + Sports = 83717, + SprinklersAllScandic = 1874, + SprinklersInAllHalls = 1908, + SprinklersInAllPublicAreas = 1909, + SprinklersInAllRooms = 1910, + StaffInDuplicateKeys = 1942, + StaffRedCrossCertifiedInCPR = 1941, + StaffTrainedForDisabledGuests = 1928, + StaffTrainedInAutomatedExternalDefibrillatorUsageAED = 1940, + StaffTrainedInCPR = 1923, + StaffTrainedInFirstAid = 1939, + StaffTrainedInFirstAidTechniques = 951, + StaffTrainedToCaterForDisabledGuestsAllScandic = 2073, + Suburbs = 1860, + SwingboltLock = 2052, + TeleConferencingFacilitiesAvailable = 1018, + TelevisionsWithSubtitlesOrClosedCaptions = 2083, + Tennis1 = 1836, + Tennis2 = 1838, + TennisPadel = 239350, + Theatre = 1862, + TrouserPress = 5779, + TVWithChromecast1 = 229127, + TVWithChromecast2 = 229144, + UniformSecurityOnPremises = 1935, + UtilityRoomForIroning = 324097, + VendingMachineWithNecessities = 324099, + VideoSurveillanceInHallways = 2056, + VideoSurveillanceInPublicAreas = 1386, + VideoSurveillanceMonitored24HrsADay = 2058, + VideoSurveillanceOfAllParkingAreas = 2055, + VideoSurveillanceOfExteriorFrontEntrance = 2054, + VideoSurveillanceRecorded24HrsADayParkingArea = 2059, + WallMountedCycleRack = 199642, + WellLitWalkways = 2060, + WellnessAndSaunaEntranceFeeAdmission16PlusYears = 267806, + WellnessPoolSaunaEntranceFeeAdmission16PlusYears = 307754, + WheelchairAccess = 2084, + WideCorridors = 2086, + WideEntrance = 2085, + WideRestaurantEntrance = 2087, + WiFiWirelessInternetAccessAllScandic = 5774, +} diff --git a/utils/facilityCards.ts b/utils/facilityCards.ts index dd8c0a1a4..2ff48b946 100644 --- a/utils/facilityCards.ts +++ b/utils/facilityCards.ts @@ -10,12 +10,12 @@ import { type Facilities, type FacilityCard, type FacilityCardType, - FacilityEnum, + FacilityCardTypeEnum, type FacilityGrid, - FacilityIds, type FacilityImage, RestaurantHeadings, } from "@/types/components/hotelPage/facilities" +import { FacilityEnum } from "@/types/enums/facilities" import type { Amenities, Facility } from "@/types/hotel" import type { CardProps } from "@/components/TempDesignSystem/Card/card" @@ -72,7 +72,7 @@ export function setFacilityCardGrids(facilities: Facility[]): Facilities { }) switch (facility.id) { - case FacilityEnum.wellness: + case FacilityCardTypeEnum.wellness: card = setCardProps( "one", "Sauna and gym", @@ -83,7 +83,7 @@ export function setFacilityCardGrids(facilities: Facility[]): Facilities { grid.unshift(card) break - case FacilityEnum.conference: + case FacilityCardTypeEnum.conference: card = setCardProps( "primaryDim", "Events that make an impression", @@ -94,7 +94,7 @@ export function setFacilityCardGrids(facilities: Facility[]): Facilities { grid.push(card) break - case FacilityEnum.restaurant: + case FacilityCardTypeEnum.restaurant: //const title = getRestaurantHeading(amenities) // TODO will be used later card = setCardProps( "primaryDark", @@ -114,10 +114,12 @@ export function setFacilityCardGrids(facilities: Facility[]): Facilities { export function getRestaurantHeading(amenities: Amenities): RestaurantHeadings { const hasBar = amenities.some( (facility) => - facility.id === FacilityIds.bar || facility.id === FacilityIds.rooftopBar + facility.id === FacilityEnum.Bar || + facility.id === FacilityEnum.RooftopBar || + facility.id === FacilityEnum.Skybar ) const hasRestaurant = amenities.some( - (facility) => facility.id === FacilityIds.restaurant + (facility) => facility.id === FacilityEnum.Restaurant ) if (hasBar && hasRestaurant) { From 2d23f9bbf3083dbff281a60125ccb3eb3a9f9406 Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Tue, 22 Oct 2024 11:43:08 +0200 Subject: [PATCH 47/69] feat: booking confirmation page with hardcoded data --- .../booking-confirmation/loading.tsx | 0 .../booking-confirmation/page.module.css | 157 ++++++++++ .../booking-confirmation/page.tsx | 279 ++++++++++++++++++ .../(confirmation)/layout.module.css | 5 + .../(confirmation)/layout.tsx | 15 + .../{ => (standard)}/[step]/layout.module.css | 0 .../{ => (standard)}/[step]/layout.tsx | 0 .../{ => (standard)}/[step]/page.tsx | 0 .../{ => (standard)}/layout.module.css | 0 .../{ => (standard)}/layout.tsx | 0 .../{ => (standard)}/page.tsx | 0 .../select-hotel/map/page.module.css | 0 .../select-hotel/map/page.tsx | 2 +- .../select-hotel/page.module.css | 0 .../{ => (standard)}/select-hotel/page.tsx | 2 +- .../{ => (standard)}/select-hotel/utils.ts | 0 .../select-rate/page.module.css | 0 .../{ => (standard)}/select-rate/page.tsx | 0 .../booking-confirmation/page.module.css | 16 - .../booking-confirmation/page.tsx | 67 ----- .../hotelreservation/[...paths]/page.tsx | 1 + .../booking-confirmation/page.tsx | 3 + .../@bookingwidget/hotelreservation/page.tsx | 1 + .../IntroSection/index.tsx | 58 ---- .../IntroSection/introSection.module.css | 26 -- .../BookingConfirmation/StaySection/index.tsx | 81 ----- .../StaySection/staySection.module.css | 78 ----- .../SummarySection/index.tsx | 40 --- .../SummarySection/summarySection.module.css | 13 - .../tempConfirmationData.ts | 27 -- .../EnterDetails/Payment/index.tsx | 2 +- components/Icons/Download.tsx | 40 +++ components/Icons/Printer.tsx | 36 +++ components/Icons/index.tsx | 2 + constants/booking.ts | 7 + i18n/dictionaries/en.json | 93 +++--- i18n/index.ts | 2 +- lib/dt.ts | 2 + server/routers/booking/input.ts | 6 +- server/routers/booking/mutation.ts | 163 +++++----- server/routers/booking/output.ts | 47 ++- server/routers/booking/query.ts | 121 +++++++- 42 files changed, 859 insertions(+), 533 deletions(-) rename app/[lang]/(live)/(public)/hotelreservation/{ => (confirmation)}/booking-confirmation/loading.tsx (100%) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/[step]/layout.module.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/[step]/layout.tsx (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/[step]/page.tsx (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/layout.module.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/layout.tsx (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/page.tsx (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/select-hotel/map/page.module.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/select-hotel/map/page.tsx (95%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/select-hotel/page.module.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/select-hotel/page.tsx (95%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/select-hotel/utils.ts (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/select-rate/page.module.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/{ => (standard)}/select-rate/page.tsx (100%) delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx create mode 100644 app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx create mode 100644 app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx create mode 100644 app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx delete mode 100644 components/HotelReservation/BookingConfirmation/IntroSection/index.tsx delete mode 100644 components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css delete mode 100644 components/HotelReservation/BookingConfirmation/StaySection/index.tsx delete mode 100644 components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css delete mode 100644 components/HotelReservation/BookingConfirmation/SummarySection/index.tsx delete mode 100644 components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css delete mode 100644 components/HotelReservation/BookingConfirmation/tempConfirmationData.ts create mode 100644 components/Icons/Download.tsx create mode 100644 components/Icons/Printer.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/loading.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/loading.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/loading.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css new file mode 100644 index 000000000..06b39ef41 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css @@ -0,0 +1,157 @@ +.details, +.guest, +.header, +.hgroup, +.hotel, +.list, +.main, +.section, +.receipt, +.total { + display: flex; + flex-direction: column; +} + +.main { + gap: var(--Spacing-x5); + margin: 0 auto; + width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 708px); +} + +.header, +.hgroup { + align-items: center; +} + +.header { + gap: var(--Spacing-x3); +} + +.hgroup { + gap: var(--Spacing-x-half); +} + +.body { + max-width: 560px; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x9); +} + +.booking { + display: grid; + gap: var(--Spacing-x-one-and-half); + grid-template-areas: + "image" + "details" + "actions"; +} + +.actions, +.details { + background-color: var(--Base-Surface-Subtle-Normal); + border-radius: var(--Corner-radius-Medium); +} + +.details { + gap: var(--Spacing-x3); + grid-area: details; + padding: var(--Spacing-x2); +} + +.tempImage { + align-items: center; + background-color: lightgrey; + border-radius: var(--Corner-radius-Medium); + display: flex; + grid-area: image; + justify-content: center; +} + +.actions { + display: grid; + grid-area: actions; + padding: var(--Spacing-x1) var(--Spacing-x2); +} + +.list { + gap: var(--Spacing-x-one-and-half); + list-style: none; + margin: 0; + padding: 0; +} + +.listItem { + align-items: center; + display: flex; + gap: var(--Spacing-x1); + justify-content: space-between; +} + +.summary { + display: grid; + gap: var(--Spacing-x3); +} + +.guest, +.hotel { + gap: var(--Spacing-x-half); +} + +.receipt, +.total { + gap: var(--Spacing-x2); +} + +.divider { + grid-column: 1 / -1; +} + +@media screen and (max-width: 767px) { + .actions { + & > button[class*="btn"][class*="icon"][class*="small"] { + border-bottom: 1px solid var(--Base-Border-Subtle); + border-radius: 0; + justify-content: space-between; + + &:last-of-type { + border-bottom: none; + } + + & > svg { + order: 2; + } + } + } + + .tempImage { + min-height: 250px; + } +} + +@media screen and (min-width: 768px) { + .booking { + grid-template-areas: + "details image" + "actions actions"; + grid-template-columns: 1fr minmax(256px, min(256px, 100%)); + } + + .actions { + gap: var(--Spacing-x7); + grid-template-columns: repeat(auto-fit, minmax(50px, auto)); + justify-content: center; + padding: var(--Spacing-x1) var(--Spacing-x3); + } + + .details { + padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x2); + } + + .summary { + grid-template-columns: 1fr 1fr; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx new file mode 100644 index 000000000..23b5270d4 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -0,0 +1,279 @@ +import { dt } from "@/lib/dt" +import { serverClient } from "@/lib/trpc/server" + +import { + CalendarIcon, + DownloadIcon, + ImageIcon, + PrinterIcon, +} from "@/components/Icons" +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 Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./page.module.css" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function BookingConfirmationPage({ + params, +}: PageArgs) { + const booking = await serverClient().booking.confirmation({ + confirmationNumber: "991697377", + }) + + if (!booking) { + return null + } + + const intl = await getIntl() + const text = intl.formatMessage( + { id: "booking.confirmation.text" }, + { + emailLink: (str) => ( + + {str} + + ), + } + ) + + const fromDate = dt(booking.temp.fromDate).locale(params.lang) + const toDate = dt(booking.temp.toDate).locale(params.lang) + const nights = intl.formatMessage( + { id: "booking.nights" }, + { + totalNights: dt(toDate.format("YYYY-MM-DD")).diff( + dt(fromDate.format("YYYY-MM-DD")), + "days" + ), + } + ) + + return ( +
+
+
+ + {intl.formatMessage({ id: "booking.confirmation.title" })} + + + {booking.hotel.name} + +
+ + {text} + +
+
+
+
+
+ + {intl.formatMessage( + { id: "Reference #{bookingNr}" }, + { bookingNr: "A92320VV" } + )} + +
+
    +
  • + {intl.formatMessage({ id: "Check-in" })} + + {`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`} + +
  • +
  • + {intl.formatMessage({ id: "Check-out" })} + + {`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`} + +
  • +
  • + {intl.formatMessage({ id: "Breakfast" })} + + {booking.temp.breakfastFrom} - {booking.temp.breakfastTo} + +
  • +
  • + {intl.formatMessage({ id: "Cancellation policy" })} + + {intl.formatMessage({ id: booking.temp.cancelPolicy })} + +
  • +
  • + {intl.formatMessage({ id: "Rebooking" })} + {`${intl.formatMessage({ id: "Free until" })} ${fromDate.subtract(3, "day").format("ddd, D MMM")}`} +
  • +
+
+ +
+ + + +
+
+
+
+ + {intl.formatMessage({ id: "Guest" })} + +
+ + {`${booking.guest.firstName} ${booking.guest.lastName}${booking.guest.memberbershipNumber ? ` (${intl.formatMessage({ id: "member no" })} ${booking.guest.memberbershipNumber})` : ""}`} + + {booking.guest.email} + + {booking.guest.phoneNumber} + +
+
+
+ + {intl.formatMessage({ id: "Your hotel" })} + +
+ + {booking.hotel.name} + + {booking.hotel.email} + + {booking.hotel.phoneNumber} + +
+
+ +
+
+ + {`${booking.temp.room.type}, ${nights}`} + + {booking.temp.room.price} +
+ {booking.temp.packages.map((pkg) => ( +
+ + {pkg.name} + + {pkg.price} +
+ ))} +
+
+
+ + {intl.formatMessage({ id: "VAT" })} + + {booking.temp.room.vat} +
+
+ + {intl.formatMessage({ id: "Total cost" })} + + {booking.temp.total} + + {`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`} + +
+
+ +
+ + {`${intl.formatMessage({ id: "Payment received" })} ${dt(booking.temp.payment).locale(params.lang).format("D MMM YYYY, h:mm z")}`} + + + {intl.formatMessage( + { id: "{card} ending with {cardno}" }, + { + card: "Mastercard", + cardno: "2202", + } + )} + +
+
+
+
+ ) +} + +// const { email, hotel, stay, summary } = tempConfirmationData + +// const confirmationNumber = useMemo(() => { +// if (typeof window === "undefined") return "" + +// const storedConfirmationNumber = sessionStorage.getItem( +// BOOKING_CONFIRMATION_NUMBER +// ) +// TODO: cleanup stored values +// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER) +// return storedConfirmationNumber +// }, []) + +// const bookingStatus = useHandleBookingStatus( +// confirmationNumber, +// BookingStatusEnum.BookingCompleted, +// maxRetries, +// retryInterval +// ) + +// if ( +// confirmationNumber === null || +// bookingStatus.isError || +// (bookingStatus.isFetched && !bookingStatus.data) +// ) { +// // TODO: handle error +// throw new Error("Error fetching booking status") +// } + +// if ( +// bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted +// ) { +// return diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css new file mode 100644 index 000000000..3f89e6f51 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css @@ -0,0 +1,5 @@ +.layout { + background-color: var(--Base-Surface-Primary-light-Normal); + min-height: 100dvh; + padding: 80px 0 160px; +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx new file mode 100644 index 000000000..971c66e0d --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx @@ -0,0 +1,15 @@ +import { notFound } from "next/navigation" + +import { env } from "@/env/server" + +import styles from "./layout.module.css" + +// route groups needed as layouts have different bgc +export default function ConfirmedBookingLayout({ + children, +}: React.PropsWithChildren) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + return
{children}
+} diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/layout.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/layout.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx similarity index 95% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index 89f4e62ca..8dca8f4c7 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -3,7 +3,7 @@ import { env } from "@/env/server" import { fetchAvailableHotels, getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils" +} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" import { setLang } from "@/i18n/serverContext" diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx similarity index 95% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index b6548122c..7dcec9130 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -3,7 +3,7 @@ import { selectHotelMap } from "@/constants/routes/hotelReservation" import { fetchAvailableHotels, getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils" +} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" import { ChevronRightIcon } from "@/components/Icons" diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils.ts rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css deleted file mode 100644 index 2771aef75..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.main { - display: flex; - justify-content: center; - padding: var(--Spacing-x4); - background-color: var(--Scandic-Brand-Warm-White); - min-height: 100dvh; - max-width: var(--max-width); - margin: 0 auto; -} - -.section { - display: flex; - flex-direction: column; - gap: var(--Spacing-x4); - width: 100%; -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx deleted file mode 100644 index 0a22c5bc0..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client" - -import { useMemo } from "react" - -import { - BOOKING_CONFIRMATION_NUMBER, - BookingStatusEnum, -} from "@/constants/booking" - -import IntroSection from "@/components/HotelReservation/BookingConfirmation/IntroSection" -import StaySection from "@/components/HotelReservation/BookingConfirmation/StaySection" -import SummarySection from "@/components/HotelReservation/BookingConfirmation/SummarySection" -import { tempConfirmationData } from "@/components/HotelReservation/BookingConfirmation/tempConfirmationData" -import LoadingSpinner from "@/components/LoadingSpinner" -import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" - -import styles from "./page.module.css" - -const maxRetries = 10 -const retryInterval = 2000 - -export default function BookingConfirmationPage() { - const { email, hotel, stay, summary } = tempConfirmationData - - const confirmationNumber = useMemo(() => { - if (typeof window === "undefined") return "" - - const storedConfirmationNumber = sessionStorage.getItem( - BOOKING_CONFIRMATION_NUMBER - ) - // TODO: cleanup stored values - // sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER) - return storedConfirmationNumber - }, []) - - const bookingStatus = useHandleBookingStatus( - confirmationNumber, - BookingStatusEnum.BookingCompleted, - maxRetries, - retryInterval - ) - - if ( - confirmationNumber === null || - bookingStatus.isError || - (bookingStatus.isFetched && !bookingStatus.data) - ) { - // TODO: handle error - throw new Error("Error fetching booking status") - } - - if ( - bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted - ) { - return ( -
-
- - - -
-
- ) - } - - return -} diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx new file mode 100644 index 000000000..2ebaca014 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../page" diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx new file mode 100644 index 000000000..e0ff199ee --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx @@ -0,0 +1,3 @@ +export default function ConfirmedBookingSlot() { + return null +} diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx b/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx deleted file mode 100644 index 448dc82e1..000000000 --- a/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useIntl } from "react-intl" - -import Button from "@/components/TempDesignSystem/Button" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import Title from "@/components/TempDesignSystem/Text/Title" - -import styles from "./introSection.module.css" - -import { IntroSectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export default function IntroSection({ email }: IntroSectionProps) { - const intl = useIntl() - - return ( -
-
- - {intl.formatMessage({ id: "Thank you" })} - - - {intl.formatMessage({ id: "We look forward to your visit!" })} - -
- - {intl.formatMessage({ - id: "We have sent a detailed confirmation of your booking to your email: ", - })} - {email} - -
- - -
-
- ) -} diff --git a/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css b/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css deleted file mode 100644 index 5b79c3796..000000000 --- a/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.section { - display: flex; - flex-direction: column; - gap: var(--Spacing-x3); - width: 100%; -} - -.buttons { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--Spacing-x2); -} - -.button { - width: 100%; - max-width: 240px; - justify-content: center; -} - -@media screen and (min-width: 1367px) { - .buttons { - flex-direction: row; - justify-content: space-around; - } -} diff --git a/components/HotelReservation/BookingConfirmation/StaySection/index.tsx b/components/HotelReservation/BookingConfirmation/StaySection/index.tsx deleted file mode 100644 index 7907ac191..000000000 --- a/components/HotelReservation/BookingConfirmation/StaySection/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useIntl } from "react-intl" - -import { ArrowRightIcon, ScandicLogoIcon } from "@/components/Icons" -import Image from "@/components/Image" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Title from "@/components/TempDesignSystem/Text/Title" - -import styles from "./staySection.module.css" - -import { StaySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export default function StaySection({ hotel, stay }: StaySectionProps) { - const intl = useIntl() - - const nightsText = - stay.nights > 1 - ? intl.formatMessage({ id: "nights" }) - : intl.formatMessage({ id: "night" }) - - return ( - <> -
- -
-
- - - {hotel.name} - - - {hotel.address} - {hotel.phone} - -
- - {`${stay.nights} ${nightsText}`} - - {stay.start} - - {stay.end} - - -
-
-
-
- - {intl.formatMessage({ id: "Breakfast" })} - - - {`${intl.formatMessage({ id: "Weekdays" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`} - {`${intl.formatMessage({ id: "Weekends" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`} - -
-
- {intl.formatMessage({ id: "Check in" })} - - {intl.formatMessage({ id: "From" })} - {hotel.checkIn} - -
-
- - {intl.formatMessage({ id: "Check out" })} - - - {intl.formatMessage({ id: "At latest" })} - {hotel.checkOut} - -
-
- - ) -} diff --git a/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css b/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css deleted file mode 100644 index 1eae5c732..000000000 --- a/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css +++ /dev/null @@ -1,78 +0,0 @@ -.card { - display: flex; - width: 100%; - background-color: var(--Base-Surface-Primary-light-Normal); - border: 1px solid var(--Base-Border-Subtle); - border-radius: var(--Corner-radius-Small); - overflow: hidden; -} - -.image { - height: 100%; - width: 105px; - object-fit: cover; -} - -.info { - display: flex; - flex-direction: column; - width: 100%; - gap: var(--Spacing-x1); - padding: var(--Spacing-x2); -} - -.hotel, -.stay { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-half); -} - -.caption { - display: flex; - flex-direction: column; -} - -.dates { - display: flex; - align-items: center; - gap: var(--Spacing-x-half); -} - -.table { - display: flex; - justify-content: space-between; - padding: var(--Spacing-x2); - border-radius: var(--Corner-radius-Small); - background-color: var(--Base-Surface-Primary-dark-Normal); - width: 100%; -} - -.breakfast, -.checkIn, -.checkOut { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-half); -} - -@media screen and (min-width: 1367px) { - .card { - flex-direction: column; - } - .image { - width: 100%; - max-height: 195px; - } - - .info { - flex-direction: row; - justify-content: space-between; - } - - .hotel, - .stay { - width: 100%; - max-width: 230px; - } -} diff --git a/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx b/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx deleted file mode 100644 index 16eb84330..000000000 --- a/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useIntl } from "react-intl" - -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Title from "@/components/TempDesignSystem/Text/Title" - -import styles from "./summarySection.module.css" - -import { SummarySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export default function SummarySection({ summary }: SummarySectionProps) { - const intl = useIntl() - const roomType = `${intl.formatMessage({ id: "Type of room" })}: ${summary.roomType}` - const bedType = `${intl.formatMessage({ id: "Type of bed" })}: ${summary.bedType}` - const breakfast = `${intl.formatMessage({ id: "Breakfast" })}: ${summary.breakfast}` - const flexibility = `${intl.formatMessage({ id: "Flexibility" })}: ${summary.flexibility}` - - return ( -
- - {intl.formatMessage({ id: "Summary" })} - - - {roomType} - 1648 SEK - - - {bedType} - 0 SEK - - - {breakfast} - 198 SEK - - - {flexibility} - 200 SEK - -
- ) -} diff --git a/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css b/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css deleted file mode 100644 index b65d92e76..000000000 --- a/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.section { - width: 100%; -} - -.summary { - display: flex; - justify-content: space-between; - border-bottom: 1px solid var(--Base-Border-Subtle); -} - -.summary span { - padding: var(--Spacing-x2) var(--Spacing-x0); -} diff --git a/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts b/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts deleted file mode 100644 index 2dbf572e7..000000000 --- a/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { BookingConfirmation } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export const tempConfirmationData: BookingConfirmation = { - email: "lisa.andersson@outlook.com", - hotel: { - name: "Helsinki Hub", - address: "Kaisaniemenkatu 7, Helsinki", - location: "Helsinki", - phone: "+358 300 870680", - image: - "https://test3.scandichotels.com/imagevault/publishedmedia/i11isd60bh119s9486b7/downtown-camper-by-scandic-lobby-reception-desk-ch.jpg?w=640", - checkIn: "15.00", - checkOut: "12.00", - breakfast: { start: "06:30", end: "10:00" }, - }, - stay: { - nights: 1, - start: "2024.03.09", - end: "2024.03.10", - }, - summary: { - roomType: "Standard Room", - bedType: "King size", - breakfast: "Yes", - flexibility: "Yes", - }, -} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 3d582e7e3..5ad6bdaf9 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -66,7 +66,7 @@ export default function Payment({ resolver: zodResolver(paymentSchema), }) - const initiateBooking = trpc.booking.booking.create.useMutation({ + const initiateBooking = trpc.booking.create.useMutation({ onSuccess: (result) => { if (result?.confirmationNumber) { setConfirmationNumber(result.confirmationNumber) diff --git a/components/Icons/Download.tsx b/components/Icons/Download.tsx new file mode 100644 index 000000000..7c1e9017a --- /dev/null +++ b/components/Icons/Download.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function DownloadIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Printer.tsx b/components/Icons/Printer.tsx new file mode 100644 index 000000000..d703940da --- /dev/null +++ b/components/Icons/Printer.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function PrinterIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 8a7764dd6..e7547b178 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -28,6 +28,7 @@ export { default as CrossCircle } from "./CrossCircle" export { default as CulturalIcon } from "./Cultural" export { default as DeleteIcon } from "./Delete" export { default as DoorOpenIcon } from "./DoorOpen" +export { default as DownloadIcon } from "./Download" export { default as DresserIcon } from "./Dresser" export { default as EditIcon } from "./Edit" export { default as ElectricBikeIcon } from "./ElectricBike" @@ -76,6 +77,7 @@ export { default as PhoneIcon } from "./Phone" export { default as PlusIcon } from "./Plus" export { default as PlusCircleIcon } from "./PlusCircle" export { default as PriceTagIcon } from "./PriceTag" +export { default as PrinterIcon } from "./Printer" export { default as RestaurantIcon } from "./Restaurant" export { default as RoomServiceIcon } from "./RoomService" export { default as SaunaIcon } from "./Sauna" diff --git a/constants/booking.ts b/constants/booking.ts index 9f5b6fed9..81e23cd23 100644 --- a/constants/booking.ts +++ b/constants/booking.ts @@ -4,6 +4,13 @@ export enum BookingStatusEnum { BookingCompleted = "BookingCompleted", } +export enum BedTypeEnum { + Crib = "Crib", + ExtraBed = "ExtraBed", + ParentsBed = "ParentsBed", + Unknown = "Unknown", +} + export const BOOKING_CONFIRMATION_NUMBER = "bookingConfirmationNumber" export enum PaymentMethodEnum { diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 759c424d0..3500a8d01 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -9,6 +9,7 @@ "Add Room": "Add room", "Add code": "Add code", "Add new card": "Add new card", + "Add to calendar": "Add to calendar", "Address": "Address", "Adults": "Adults", "Age": "Age", @@ -25,7 +26,6 @@ "Approx.": "Approx.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?", "Arrival date": "Arrival date", - "as of today": "as of today", "As our": "As our {level}", "As our Close Friend": "As our Close Friend", "At latest": "At latest", @@ -39,12 +39,6 @@ "Book": "Book", "Book reward night": "Book reward night", "Booking number": "Booking number", - "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", - "booking.children": "{totalChildren, plural, one {# child} other {# children}}", - "booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}", - "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", - "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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.", "Breakfast": "Breakfast", "Breakfast buffet": "Breakfast buffet", "Breakfast excluded": "Breakfast excluded", @@ -53,12 +47,13 @@ "Breakfast selection in next step.": "Breakfast selection in next step.", "Bus terminal": "Bus terminal", "Business": "Business", - "by": "by", "Cancel": "Cancel", - "characters": "characters", + "Cancellation policy": "Cancellation policy", "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", @@ -100,6 +95,7 @@ "Distance to city centre": "{number}km to city centre", "Do you want to start the day with Scandics famous breakfast buffé?": "Do you want to start the day with Scandics famous breakfast buffé?", "Done": "Done", + "Download invoice": "Download invoice", "Download the Scandic app": "Download the Scandic app", "Driving directions": "Driving directions", "Earn bonus nights & points": "Earn bonus nights & points", @@ -114,9 +110,9 @@ "Explore all levels and benefits": "Explore all levels and benefits", "Explore nearby": "Explore nearby", "Extras to your booking": "Extras to your booking", + "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.", "Fair": "Fair", - "FAQ": "FAQ", "Find booking": "Find booking", "Find hotels": "Find hotels", "First name": "First name", @@ -125,14 +121,14 @@ "Former Scandic Hotel": "Former Scandic Hotel", "Free cancellation": "Free cancellation", "Free rebooking": "Free rebooking", + "Free until": "Free until", "From": "From", "Get inspired": "Get inspired", "Get member benefits & offers": "Get member benefits & offers", "Go back to edit": "Go back to edit", "Go back to overview": "Go back to overview", - "guest": "guest", + "Guest": "Guest", "Guest information": "Guest information", - "guests": "guests", "Guests & Rooms": "Guests & Rooms", "Hi": "Hi", "Highest level": "Highest level", @@ -140,9 +136,6 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel facilities", "Hotel surroundings": "Hotel surroundings", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "persons", - "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", "Hotels": "Hotels", "How do you want to sleep?": "How do you want to sleep?", "How it works": "How it works", @@ -153,10 +146,9 @@ "In extra bed": "In extra bed", "Included": "Included", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", - "Join at no cost": "Join at no cost", "Join Scandic Friends": "Join Scandic Friends", + "Join at no cost": "Join at no cost", "King bed": "King bed", - "km to city center": "km to city center", "Language": "Language", "Last name": "Last name", "Latest searches": "Latest searches", @@ -176,7 +168,7 @@ "Log in here": "Log in here", "Log in/Join": "Log in/Join", "Log out": "Log out", - "lowercase letter": "lowercase letter", + "MY SAVED CARDS": "MY SAVED CARDS", "Main menu": "Main menu", "Manage preferences": "Manage preferences", "Map": "Map", @@ -186,9 +178,9 @@ "Member price": "Member price", "Member price from": "Member price from", "Members": "Members", - "Membership cards": "Membership cards", "Membership ID": "Membership ID", "Membership ID copied to clipboard": "Membership ID copied to clipboard", + "Membership cards": "Membership cards", "Menu": "Menu", "Modify": "Modify", "Month": "Month", @@ -198,16 +190,11 @@ "My pages": "My pages", "My pages menu": "My pages menu", "My payment cards": "My payment cards", - "MY SAVED CARDS": "MY SAVED CARDS", "My wishes": "My wishes", - "n/a": "n/a", "Nearby": "Nearby", "Nearby companies": "Nearby companies", "New password": "New password", "Next": "Next", - "next level:": "next level:", - "night": "night", - "nights": "nights", "Nights needed to level up": "Nights needed to level up", "No breakfast": "No breakfast", "No content published": "No content published", @@ -220,14 +207,12 @@ "Nordic Swan Ecolabel": "Nordic Swan Ecolabel", "Not found": "Not found", "Nr night, nr adult": "{nights, number} night, {adults, number} adult", - "number": "number", + "OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS", "On your journey": "On your journey", "Open": "Open", "Open language menu": "Open language menu", "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", - "or": "or", - "OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS", "Overview": "Overview", "Parking": "Parking", "Parking / Garage": "Parking / Garage", @@ -235,11 +220,11 @@ "Pay later": "Pay later", "Pay now": "Pay now", "Payment info": "Payment info", + "Payment received": "Payment received", "Phone": "Phone", "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", - "points": "Points", "Points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", @@ -247,6 +232,7 @@ "Points needed to level up": "Points needed to level up", "Points needed to stay on level": "Points needed to stay on level", "Previous victories": "Previous victories", + "Print confirmation": "Print confirmation", "Proceed to login": "Proceed to login", "Proceed to payment method": "Proceed to payment method", "Public price from": "Public price from", @@ -256,6 +242,8 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Read more about the hotel", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Rebooking": "Rebooking", + "Reference #{bookingNr}": "Reference #{bookingNr}", "Remove card from member profile": "Remove card from member profile", "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -301,34 +289,32 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.", "Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.", "Something went wrong!": "Something went wrong!", - "special character": "special character", - "spendable points expiring by": "{points} spendable points expiring by {date}", "Sports": "Sports", "Standard price": "Standard price", "Street": "Street", "Successfully updated profile!": "Successfully updated profile!", "Summary": "Summary", + "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.", "Terms and conditions": "Terms and conditions", "Thank you": "Thank you", "Theatre": "Theatre", "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", - "to": "to", - "Total incl VAT": "Total incl VAT", "Total Points": "Total Points", + "Total cost": "Total cost", + "Total incl VAT": "Total incl VAT", "Tourist": "Tourist", "Transaction date": "Transaction date", "Transactions": "Transactions", "Transportations": "Transportations", "Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)", - "TUI Points": "TUI Points", "Type of bed": "Type of bed", "Type of room": "Type of room", - "uppercase letter": "uppercase letter", "Use bonus cheque": "Use bonus cheque", "Use code/voucher": "Use code/voucher", "User information": "User information", + "VAT": "VAT", "View as list": "View as list", "View as map": "View as map", "View your booking": "View your booking", @@ -347,18 +333,19 @@ "Where to": "Where to", "Which room class suits you the best?": "Which room class suits you the best?", "Year": "Year", - "Yes, discard changes": "Yes, discard changes", "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", + "Yes, discard changes": "Yes, discard changes", "Yes, remove my card": "Yes, remove my card", "You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.", "You canceled adding a new credit card.": "You canceled adding a new credit card.", "You have no previous stays.": "You have no previous stays.", "You have no upcoming stays.": "You have no upcoming stays.", + "Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!", "Your card was successfully removed!": "Your card was successfully removed!", "Your card was successfully saved!": "Your card was successfully saved!", - "Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!", "Your current level": "Your current level", "Your details": "Your details", + "Your hotel": "Your hotel", "Your level": "Your level", "Your points to spend": "Your points to spend", "Your room": "Your room", @@ -366,7 +353,39 @@ "Zoo": "Zoo", "Zoom in": "Zoom in", "Zoom out": "Zoom out", + "as of today": "as of today", + "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", + "booking.children": "{totalChildren, plural, one {# child} other {# children}}", + "booking.confirmation.text": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please email us.", + "booking.confirmation.title": "Your booking is confirmed", + "booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}", + "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", + "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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.", + "by": "by", + "characters": "characters", + "from": "from", + "guest": "guest", + "guests": "guests", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "persons", + "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", + "km to city center": "km to city center", + "lowercase letter": "lowercase letter", + "member no": "member no", + "n/a": "n/a", + "next level:": "next level:", + "night": "night", + "nights": "nights", + "number": "number", + "or": "or", + "points": "Points", + "special character": "special character", + "spendable points expiring by": "{points} spendable points expiring by {date}", + "to": "to", + "uppercase letter": "uppercase letter", "{amount} {currency}": "{amount} {currency}", + "{card} ending with {cardno}": "{card} ending with {cardno}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" -} +} \ No newline at end of file diff --git a/i18n/index.ts b/i18n/index.ts index be2d744bb..0a8ebb67d 100644 --- a/i18n/index.ts +++ b/i18n/index.ts @@ -8,7 +8,7 @@ import { Lang } from "@/constants/languages" const cache = createIntlCache() async function initIntl(lang: Lang) { - return createIntl( + return createIntl( { defaultLocale: Lang.en, locale: lang, diff --git a/lib/dt.ts b/lib/dt.ts index b90938475..5cbe77692 100644 --- a/lib/dt.ts +++ b/lib/dt.ts @@ -7,6 +7,7 @@ import d from "dayjs" import advancedFormat from "dayjs/plugin/advancedFormat" import isToday from "dayjs/plugin/isToday" import relativeTime from "dayjs/plugin/relativeTime" +import timezone from "dayjs/plugin/timezone" import utc from "dayjs/plugin/utc" /** @@ -59,6 +60,7 @@ d.locale("no", { d.extend(advancedFormat) d.extend(isToday) d.extend(relativeTime) +d.extend(timezone) d.extend(utc) export const dt = d diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index 7e9b9b2c8..cbda7d3ef 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -63,6 +63,10 @@ export const createBookingInput = z.object({ }) // Query -export const getBookingStatusInput = z.object({ +const confirmationNumberInput = z.object({ confirmationNumber: z.string(), }) + +export const bookingConfirmationInput = confirmationNumberInput + +export const getBookingStatusInput = confirmationNumberInput diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 1ff3422af..9e5677d32 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -35,96 +35,95 @@ async function getMembershipNumber( } export const bookingMutationRouter = router({ - booking: router({ - create: serviceProcedure - .input(createBookingInput) - .mutation(async function ({ ctx, input }) { - const { checkInDate, checkOutDate, hotelId } = input + create: serviceProcedure.input(createBookingInput).mutation(async function ({ + ctx, + input, + }) { + const { checkInDate, checkOutDate, hotelId } = input - // TODO: add support for user token OR service token in procedure - // then we can fetch membership number if user token exists - const loggingAttributes = { - // membershipNumber: await getMembershipNumber(ctx.session), - checkInDate, - checkOutDate, - hotelId, - } + // TODO: add support for user token OR service token in procedure + // then we can fetch membership number if user token exists + const loggingAttributes = { + // membershipNumber: await getMembershipNumber(ctx.session), + checkInDate, + checkOutDate, + hotelId, + } - createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate }) + createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate }) - console.info( - "api.booking.booking.create start", - JSON.stringify({ - query: loggingAttributes, - }) - ) - const headers = { - Authorization: `Bearer ${ctx.serviceToken}`, - } + console.info( + "api.booking.create start", + JSON.stringify({ + query: loggingAttributes, + }) + ) + const headers = { + Authorization: `Bearer ${ctx.serviceToken}`, + } - const apiResponse = await api.post(api.endpoints.v1.booking, { - headers, - body: input, + const apiResponse = await api.post(api.endpoints.v1.booking, { + headers, + body: input, + }) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + createBookingFailCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + }), + }) + console.error( + "api.booking.create error", + JSON.stringify({ + query: loggingAttributes, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + error: text, + }, }) + ) + return null + } - if (!apiResponse.ok) { - const text = await apiResponse.text() - createBookingFailCounter.add(1, { - hotelId, - checkInDate, - checkOutDate, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - }), - }) - console.error( - "api.booking.booking.create error", - JSON.stringify({ - query: loggingAttributes, - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - error: text, - }, - }) - ) - return null - } + const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + createBookingFailCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + error_type: "validation_error", + }) - const apiJson = await apiResponse.json() - const verifiedData = createBookingSchema.safeParse(apiJson) - if (!verifiedData.success) { - createBookingFailCounter.add(1, { - hotelId, - checkInDate, - checkOutDate, - error_type: "validation_error", - }) - - console.error( - "api.booking.booking.create validation error", - JSON.stringify({ - query: loggingAttributes, - error: verifiedData.error, - }) - ) - return null - } - - createBookingSuccessCounter.add(1, { - hotelId, - checkInDate, - checkOutDate, + console.error( + "api.booking.create validation error", + JSON.stringify({ + query: loggingAttributes, + error: verifiedData.error, }) + ) + return null + } - console.info( - "api.booking.booking.create success", - JSON.stringify({ - query: loggingAttributes, - }) - ) - return verifiedData.data - }), + createBookingSuccessCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + }) + + console.info( + "api.booking.create success", + JSON.stringify({ + query: loggingAttributes, + }) + ) + return verifiedData.data }), }) diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index 7535aac6e..aacf1ca6b 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -1,5 +1,8 @@ import { z } from "zod" +import { BedTypeEnum } from "@/constants/booking" + +// MUTATION export const createBookingSchema = z .object({ data: z.object({ @@ -32,4 +35,46 @@ export const createBookingSchema = z paymentUrl: d.data.attributes.paymentUrl, })) -type CreateBookingData = z.infer +// QUERY +const childrenAgesSchema = z.object({ + age: z.number(), + bedType: z.nativeEnum(BedTypeEnum), +}) + +const guestSchema = z.object({ + firstName: z.string(), + lastName: z.string(), +}) + +const packagesSchema = z.object({ + accessibility: z.boolean(), + allergyFriendly: z.boolean(), + breakfast: z.boolean(), + petFriendly: z.boolean(), +}) + +export const bookingConfirmationSchema = z + .object({ + data: z.object({ + attributes: z.object({ + adults: z.number(), + checkInDate: z.date({ coerce: true }), + checkOutDate: z.date({ coerce: true }), + createDateTime: z.date({ coerce: true }), + childrenAges: z.array(childrenAgesSchema), + computedReservationStatus: z.string(), + confirmationNumber: z.string(), + currencyCode: z.string(), + guest: guestSchema, + hasPayRouting: z.boolean(), + hotelId: z.string(), + packages: packagesSchema, + rateCode: z.string(), + reservationStatus: z.string(), + totalPrice: z.number(), + }), + id: z.string(), + type: z.literal("booking"), + }), + }) + .transform(({ data }) => data.attributes) diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index d053782bd..76b874ba3 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -4,10 +4,20 @@ import * as api from "@/lib/api" import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" import { router, serviceProcedure } from "@/server/trpc" -import { getBookingStatusInput } from "./input" -import { createBookingSchema } from "./output" +import { bookingConfirmationInput, getBookingStatusInput } from "./input" +import { bookingConfirmationSchema, createBookingSchema } from "./output" const meter = metrics.getMeter("trpc.booking") +const getBookingConfirmationCounter = meter.createCounter( + "trpc.booking.confirmation" +) +const getBookingConfirmationSuccessCounter = meter.createCounter( + "trpc.booking.confirmation-success" +) +const getBookingConfirmationFailCounter = meter.createCounter( + "trpc.booking.confirmation-fail" +) + const getBookingStatusCounter = meter.createCounter("trpc.booking.status") const getBookingStatusSuccessCounter = meter.createCounter( "trpc.booking.status-success" @@ -17,6 +27,113 @@ const getBookingStatusFailCounter = meter.createCounter( ) export const bookingQueryRouter = router({ + confirmation: serviceProcedure + .input(bookingConfirmationInput) + .query(async function ({ ctx, input: { confirmationNumber } }) { + getBookingConfirmationCounter.add(1, { confirmationNumber }) + + const apiResponse = await api.get( + `${api.endpoints.v1.booking}/${confirmationNumber}`, + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + } + ) + + if (!apiResponse.ok) { + const responseMessage = await apiResponse.text() + getBookingConfirmationFailCounter.add(1, { + confirmationNumber, + error_type: "http_error", + error: responseMessage, + }) + console.error( + "api.booking.confirmation error", + JSON.stringify({ + query: { confirmationNumber }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text: responseMessage, + }, + }) + ) + + throw serverErrorByStatus(apiResponse.status, apiResponse) + } + + const apiJson = await apiResponse.json() + const booking = bookingConfirmationSchema.safeParse(apiJson) + if (!booking.success) { + getBookingConfirmationFailCounter.add(1, { + confirmationNumber, + error_type: "validation_error", + error: JSON.stringify(booking.error), + }) + console.error( + "api.booking.confirmation validation error", + JSON.stringify({ + query: { confirmationNumber }, + error: booking.error, + }) + ) + throw badRequestError() + } + + getBookingConfirmationSuccessCounter.add(1, { confirmationNumber }) + console.info( + "api.booking.confirmation success", + JSON.stringify({ + query: { confirmationNumber }, + }) + ) + + return { + ...booking.data, + temp: { + breakfastFrom: "06:30", + breakfastTo: "11:00", + cancelPolicy: "Free rebooking", + fromDate: "2024-10-21 14:00", + packages: [ + { + name: "Breakfast buffet", + price: "150 SEK", + }, + { + name: "Member discount", + price: "-297 SEK", + }, + { + name: "Points used / remaining", + price: "0 / 1044", + }, + ], + payment: "2024-08-09 1:47", + room: { + price: "2 589 SEK", + type: "Cozy Cabin", + vat: "684,79 SEK", + }, + toDate: "2024-10-22 11:00", + total: "2 739 SEK", + totalInEuro: "265 EUR", + }, + guest: { + email: "sarah.obrian@gmail.com", + firstName: "Sarah", + lastName: "O'Brian", + memberbershipNumber: "19822", + phoneNumber: "+46702446688", + }, + hotel: { + email: "bookings@scandichotels.com", + name: "Downtown Camper by Scandic", + phoneNumber: "+4689001350", + }, + } + }), status: serviceProcedure.input(getBookingStatusInput).query(async function ({ ctx, input, From d5abe13a7e323a3a20f52c66394ae993065b1e98 Mon Sep 17 00:00:00 2001 From: Michael Zetterberg Date: Wed, 23 Oct 2024 12:18:08 +0200 Subject: [PATCH 48/69] fix: add feature flag for service token scopes --- server/tokenManager.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/tokenManager.ts b/server/tokenManager.ts index cdd6078dc..24180d017 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -70,7 +70,12 @@ async function fetchServiceToken(scopes: string[]) { } export async function getServiceToken() { - const scopes = ["profile", "hotel", "booking"] + let scopes: string[] = [] + if (env.HIDE_FOR_NEXT_RELEASE) { + scopes = ["profile"] + } else { + scopes = ["profile", "hotel", "booking"] + } const tag = generateServiceTokenTag(scopes) const getCachedJwt = unstable_cache( async (scopes) => { From 52c5f6d29ecfce9f0766432096f0891ce7d1c933 Mon Sep 17 00:00:00 2001 From: Michael Zetterberg Date: Wed, 23 Oct 2024 12:18:55 +0200 Subject: [PATCH 49/69] fix: feature flag booking widget and sitewidealert --- app/[lang]/(live)/@bookingwidget/loading.tsx | 6 ++++++ app/[lang]/(live)/@bookingwidget/page.tsx | 5 +++++ app/[lang]/(live)/@sitewidealert/page.tsx | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/app/[lang]/(live)/@bookingwidget/loading.tsx b/app/[lang]/(live)/@bookingwidget/loading.tsx index 2c203967d..5e05ba68c 100644 --- a/app/[lang]/(live)/@bookingwidget/loading.tsx +++ b/app/[lang]/(live)/@bookingwidget/loading.tsx @@ -1,8 +1,14 @@ +import { env } from "@/env/server" + import LoadingSpinner from "@/components/LoadingSpinner" import styles from "./loading.module.css" export default function LoadingBookingWidget() { + if (env.HIDE_FOR_NEXT_RELEASE) { + return null + } + return (
diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 13a414cba..7e197d0fa 100644 --- a/app/[lang]/(live)/@bookingwidget/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -1,8 +1,13 @@ +import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" import BookingWidget, { preload } from "@/components/BookingWidget" export default async function BookingWidgetPage() { + if (env.HIDE_FOR_NEXT_RELEASE) { + return null + } + preload() // Get the booking widget show/hide status based on page specific settings diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx index 618f3b5cb..be7ae2256 100644 --- a/app/[lang]/(live)/@sitewidealert/page.tsx +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -1,11 +1,17 @@ import { Suspense } from "react" +import { env } from "@/env/server" + import SitewideAlert, { preload } from "@/components/SitewideAlert" import { setLang } from "@/i18n/serverContext" import type { LangParams, PageArgs } from "@/types/params" export default function SitewideAlertPage({ params }: PageArgs) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return null + } + setLang(params.lang) preload() From 025c5c8291e5a1a4d0b09e03ac902ab8ade6e6a1 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 23 Oct 2024 11:51:44 +0000 Subject: [PATCH 50/69] Merged in feat/SW-659-payment-send-query-params (pull request #727) feat(SW-659): Receive query params from Planet callbacks * feat(SW-659): read confirmation number from url and update callback url if dev * fix(SW-659): moved callback url into env variable Approved-by: Simon.Emanuelsson --- .../booking-confirmation/page.tsx | 6 ++- .../payment-callback/[lang]/[status]/route.ts | 16 ++++++-- .../EnterDetails/Payment/index.tsx | 39 +++++++++++-------- constants/booking.ts | 2 +- env/client.ts | 4 ++ 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx index 23b5270d4..ef7e818c4 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -22,9 +22,11 @@ import type { LangParams, PageArgs } from "@/types/params" export default async function BookingConfirmationPage({ params, -}: PageArgs) { + searchParams, +}: PageArgs) { + const confirmationNumber = searchParams.confirmationNumber const booking = await serverClient().booking.confirmation({ - confirmationNumber: "991697377", + confirmationNumber, }) if (!booking) { diff --git a/app/api/web/payment-callback/[lang]/[status]/route.ts b/app/api/web/payment-callback/[lang]/[status]/route.ts index 4154aff14..baa8e4b8a 100644 --- a/app/api/web/payment-callback/[lang]/[status]/route.ts +++ b/app/api/web/payment-callback/[lang]/[status]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server" -import { env } from "process" +import { BOOKING_CONFIRMATION_NUMBER } from "@/constants/booking" import { Lang } from "@/constants/languages" import { bookingConfirmation, @@ -17,14 +17,24 @@ export async function GET( console.log(`[payment-callback] callback started`) const lang = params.lang as Lang const status = params.status - const returnUrl = new URL(`${publicURL}/${payment[lang]}`) - if (status === "success") { + const queryParams = request.nextUrl.searchParams + const confirmationNumber = queryParams.get(BOOKING_CONFIRMATION_NUMBER) + + if (status === "success" && confirmationNumber) { const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation[lang]}`) + confirmationUrl.searchParams.set( + BOOKING_CONFIRMATION_NUMBER, + confirmationNumber + ) + console.log(`[payment-callback] redirecting to: ${confirmationUrl}`) return NextResponse.redirect(confirmationUrl) } + const returnUrl = new URL(`${publicURL}/${payment[lang]}`) + returnUrl.search = queryParams.toString() + if (status === "cancel") { returnUrl.searchParams.set("cancel", "true") } diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 5ad6bdaf9..0da2b79e2 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -1,14 +1,13 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useRouter } from "next/navigation" -import { useEffect, useState } from "react" +import { useRouter, useSearchParams } from "next/navigation" +import { useEffect, useMemo, useState } from "react" import { Label as AriaLabel } from "react-aria-components" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { - BOOKING_CONFIRMATION_NUMBER, BookingStatusEnum, PAYMENT_METHOD_TITLES, PaymentMethodEnum, @@ -17,7 +16,9 @@ import { bookingTermsAndConditions, privacyPolicy, } from "@/constants/currentWebHrefs" +import { env } from "@/env/client" import { trpc } from "@/lib/trpc/client" +import { useEnterDetailsStore } from "@/stores/enter-details" import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" @@ -51,6 +52,9 @@ export default function Payment({ const router = useRouter() const lang = useLang() const intl = useIntl() + const queryParams = useSearchParams() + const { firstName, lastName, email, phoneNumber, countryCode } = + useEnterDetailsStore((state) => state.data) const [confirmationNumber, setConfirmationNumber] = useState("") const methods = useForm({ @@ -90,14 +94,15 @@ export default function Payment({ ) useEffect(() => { - if (confirmationNumber && bookingStatus?.data?.paymentUrl) { - // Planet doesn't support query params so we have to store values in session storage - sessionStorage.setItem(BOOKING_CONFIRMATION_NUMBER, confirmationNumber) + if (bookingStatus?.data?.paymentUrl) { router.push(bookingStatus.data.paymentUrl) } - }, [confirmationNumber, bookingStatus, router]) + }, [bookingStatus, router]) function handleSubmit(data: PaymentFormData) { + const allQueryParams = + queryParams.size > 0 ? `?${queryParams.toString()}` : "" + // set payment method to card if saved card is submitted const paymentMethod = isPaymentMethodEnum(data.paymentMethod) ? data.paymentMethod @@ -118,13 +123,13 @@ export default function Payment({ rateCode: "SAVEEU", roomTypeCode: "QC", guest: { - title: "Mr", - firstName: "Test", - lastName: "User", - email: "test.user@scandichotels.com", - phoneCountryCodePrefix: "string", - phoneNumber: "string", - countryCode: "string", + title: "Mr", // TODO: do we need title? + firstName, + lastName, + email, + phoneCountryCodePrefix: phoneNumber.slice(0, 3), + phoneNumber: phoneNumber.slice(3), + countryCode, }, packages: { breakfast: true, @@ -150,9 +155,9 @@ export default function Payment({ phoneCountryCode: "", phoneSubscriber: "", }, - success: `api/web/payment-callback/${lang}/success`, - error: `api/web/payment-callback/${lang}/error`, - cancel: `api/web/payment-callback/${lang}/cancel`, + success: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/success`, + error: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/error${allQueryParams}`, + cancel: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/cancel${allQueryParams}`, }, }) } diff --git a/constants/booking.ts b/constants/booking.ts index 81e23cd23..8f5acb120 100644 --- a/constants/booking.ts +++ b/constants/booking.ts @@ -11,7 +11,7 @@ export enum BedTypeEnum { Unknown = "Unknown", } -export const BOOKING_CONFIRMATION_NUMBER = "bookingConfirmationNumber" +export const BOOKING_CONFIRMATION_NUMBER = "confirmationNumber" export enum PaymentMethodEnum { card = "card", diff --git a/env/client.ts b/env/client.ts index 467100c01..4eafd5592 100644 --- a/env/client.ts +++ b/env/client.ts @@ -5,10 +5,14 @@ export const env = createEnv({ client: { NEXT_PUBLIC_NODE_ENV: z.enum(["development", "test", "production"]), NEXT_PUBLIC_PORT: z.string().default("3000"), + NEXT_PUBLIC_PAYMENT_CALLBACK_URL: z + .string() + .default("/api/web/payment-callback"), }, emptyStringAsUndefined: true, runtimeEnv: { NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT, + NEXT_PUBLIC_PAYMENT_CALLBACK_URL: `${process.env.NODE_ENV === "development" ? `http://localhost:${process.env.NEXT_PUBLIC_PORT}` : ""}/api/web/payment-callback`, }, }) From d7676a22cc8565a323b0b9aa2663edda5251d914 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Wed, 23 Oct 2024 14:31:02 +0200 Subject: [PATCH 51/69] Added onClick to anchor tag as well #SW-222 --- components/TempDesignSystem/Link/index.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/components/TempDesignSystem/Link/index.tsx b/components/TempDesignSystem/Link/index.tsx index bae4ab9b1..716221b5b 100644 --- a/components/TempDesignSystem/Link/index.tsx +++ b/components/TempDesignSystem/Link/index.tsx @@ -75,7 +75,15 @@ export default function Link({ } return isExternal ? ( - + { + if (onClick) { + onClick(e) + } + }} + /> ) : ( Date: Thu, 17 Oct 2024 11:23:50 +0200 Subject: [PATCH 52/69] feat(SW-498): added sitewide alert --- app/[lang]/(live)/@sitewidealert/page.tsx | 10 ++------ components/SitewideAlert/index.tsx | 28 +++++++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx index be7ae2256..0f0c75eba 100644 --- a/app/[lang]/(live)/@sitewidealert/page.tsx +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -1,11 +1,9 @@ -import { Suspense } from "react" - import { env } from "@/env/server" import SitewideAlert, { preload } from "@/components/SitewideAlert" import { setLang } from "@/i18n/serverContext" -import type { LangParams, PageArgs } from "@/types/params" +import { LangParams, PageArgs } from "@/types/params" export default function SitewideAlertPage({ params }: PageArgs) { if (env.HIDE_FOR_NEXT_RELEASE) { @@ -15,9 +13,5 @@ export default function SitewideAlertPage({ params }: PageArgs) { setLang(params.lang) preload() - return ( - - - - ) + return } diff --git a/components/SitewideAlert/index.tsx b/components/SitewideAlert/index.tsx index 2ce624119..c2cb6e728 100644 --- a/components/SitewideAlert/index.tsx +++ b/components/SitewideAlert/index.tsx @@ -1,3 +1,5 @@ +import { Suspense } from "react" + import { getSiteConfig } from "@/lib/trpc/memoizedRequests" import Alert from "../TempDesignSystem/Alert" @@ -17,17 +19,19 @@ export default async function SitewideAlert() { const { sitewideAlert } = siteConfig return ( -
- -
+ +
+ +
+
) } From a29657a6b20488fe495f96f82392047ca61803a9 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 22 Oct 2024 10:49:01 +0200 Subject: [PATCH 53/69] feat(SW-508): Added hotel alerts --- app/[lang]/(live)/@sitewidealert/page.tsx | 9 +++-- .../HotelPage/hotelPage.module.css | 16 +++++---- components/ContentType/HotelPage/index.tsx | 34 ++++++++++++++----- components/SitewideAlert/index.tsx | 28 +++++++-------- components/TempDesignSystem/Alert/alert.ts | 4 +-- components/TempDesignSystem/Alert/index.tsx | 4 +++ server/routers/hotels/output.ts | 21 +++++++++++- server/routers/hotels/query.ts | 2 ++ 8 files changed, 82 insertions(+), 36 deletions(-) diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx index 0f0c75eba..b0bbf8765 100644 --- a/app/[lang]/(live)/@sitewidealert/page.tsx +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -1,9 +1,10 @@ import { env } from "@/env/server" +import { Suspense } from "react" import SitewideAlert, { preload } from "@/components/SitewideAlert" import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default function SitewideAlertPage({ params }: PageArgs) { if (env.HIDE_FOR_NEXT_RELEASE) { @@ -13,5 +14,9 @@ export default function SitewideAlertPage({ params }: PageArgs) { setLang(params.lang) preload() - return + return ( + + + + ) } diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index 771c4e76e..091444fe2 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -29,6 +29,11 @@ display: none; } +.overview { + display: grid; + gap: var(--Spacing-x3); +} + .introContainer { display: flex; flex-wrap: wrap; @@ -37,6 +42,11 @@ scroll-margin-top: var(--hotel-page-scroll-margin-top); } +.alertsContainer { + display: grid; + gap: var(--Spacing-x2); +} + @media screen and (min-width: 1367px) { .pageContainer { grid-template-areas: @@ -76,10 +86,4 @@ padding-left: var(--Spacing-x5); padding-right: var(--Spacing-x5); } - .introContainer { - grid-template-columns: 38rem minmax(max-content, 16rem); - justify-content: space-between; - gap: var(--Spacing-x2); - align-items: end; - } } diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 5b0139d6f..d10244040 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -4,6 +4,7 @@ import { serverClient } from "@/lib/trpc/server" import AccordionSection from "@/components/Blocks/Accordion" import SidePeekProvider from "@/components/SidePeekProvider" +import Alert from "@/components/TempDesignSystem/Alert" import SidePeek from "@/components/TempDesignSystem/SidePeek" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" @@ -49,6 +50,7 @@ export default async function HotelPage() { pointsOfInterest, facilities, faq, + alerts, } = hotelData const topThreePois = pointsOfInterest.slice(0, 3) @@ -69,16 +71,30 @@ export default async function HotelPage() { hasFAQ={!!faq} />
-
- +
+
+ - + +
+ {alerts.length ? ( +
+ {alerts.map((alert) => ( + + ))} +
+ ) : null}
diff --git a/components/SitewideAlert/index.tsx b/components/SitewideAlert/index.tsx index c2cb6e728..2ce624119 100644 --- a/components/SitewideAlert/index.tsx +++ b/components/SitewideAlert/index.tsx @@ -1,5 +1,3 @@ -import { Suspense } from "react" - import { getSiteConfig } from "@/lib/trpc/memoizedRequests" import Alert from "../TempDesignSystem/Alert" @@ -19,19 +17,17 @@ export default async function SitewideAlert() { const { sitewideAlert } = siteConfig return ( - -
- -
-
+
+ +
) } diff --git a/components/TempDesignSystem/Alert/alert.ts b/components/TempDesignSystem/Alert/alert.ts index 20927d7fc..e63873c66 100644 --- a/components/TempDesignSystem/Alert/alert.ts +++ b/components/TempDesignSystem/Alert/alert.ts @@ -8,8 +8,8 @@ import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConf export interface AlertProps extends VariantProps { className?: string type: AlertTypeEnum - heading?: string - text: string + heading?: string | null + text?: string | null phoneContact?: { displayText: string phoneNumber?: string diff --git a/components/TempDesignSystem/Alert/index.tsx b/components/TempDesignSystem/Alert/index.tsx index 6499c7126..2f1e97b6a 100644 --- a/components/TempDesignSystem/Alert/index.tsx +++ b/components/TempDesignSystem/Alert/index.tsx @@ -27,6 +27,10 @@ export default function Alert({ }) const Icon = getIconByAlertType(type) + if (!text && !heading) { + return null + } + return (
diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 89e286185..803ae012f 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -1,11 +1,13 @@ import { z } from "zod" +import { dt } from "@/lib/dt" import { toLang } from "@/server/utils" import { imageMetaDataSchema, imageSizesSchema } from "./schemas/image" import { roomSchema } from "./schemas/room" import { getPoiGroupByCategoryName } from "./utils" +import { AlertTypeEnum } from "@/types/enums/alert" import { FacilityEnum } from "@/types/enums/facilities" import { PointOfInterestCategoryNameEnum } from "@/types/hotel" @@ -321,6 +323,7 @@ const socialMediaSchema = z.object({ const metaSpecialAlertSchema = z.object({ type: z.string(), + title: z.string().optional(), description: z.string().optional(), displayInBookingFlow: z.boolean(), startDate: z.string(), @@ -328,7 +331,23 @@ const metaSpecialAlertSchema = z.object({ }) const metaSchema = z.object({ - specialAlerts: z.array(metaSpecialAlertSchema), + specialAlerts: z + .array(metaSpecialAlertSchema) + .transform((data) => { + const now = dt().utc().format("YYYY-MM-DD") + const filteredAlerts = data.filter((alert) => { + const shouldShowNow = alert.startDate <= now && alert.endDate >= now + const hasText = alert.description || alert.title + return shouldShowNow && hasText + }) + return filteredAlerts.map((alert, idx) => ({ + id: `alert-${alert.type}-${idx}`, + type: AlertTypeEnum.Info, + heading: alert.title || null, + text: alert.description || null, + })) + }) + .default([]), }) const relationshipsSchema = z.object({ diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 2fa4cfbe8..b5b5ff34b 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -219,6 +219,7 @@ export const hotelQueryRouter = router({ const hotelAttributes = validatedHotelData.data.data.attributes const images = extractHotelImages(hotelAttributes) + const hotelAlerts = hotelAttributes.meta?.specialAlerts || [] const roomCategories = included ? included.filter((item) => item.type === "roomcategories") @@ -262,6 +263,7 @@ export const hotelQueryRouter = router({ roomCategories, activitiesCard: activities?.upcoming_activities_card, facilities, + alerts: hotelAlerts, faq: contentstackData?.faq, } }), From d94c55a46daead7c320c97e40cec02994cca649c Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 24 Oct 2024 10:24:57 +0200 Subject: [PATCH 54/69] fix: import order fix --- app/[lang]/(live)/@sitewidealert/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx index b0bbf8765..be7ae2256 100644 --- a/app/[lang]/(live)/@sitewidealert/page.tsx +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -1,6 +1,7 @@ -import { env } from "@/env/server" import { Suspense } from "react" +import { env } from "@/env/server" + import SitewideAlert, { preload } from "@/components/SitewideAlert" import { setLang } from "@/i18n/serverContext" From 748021cdabcfb2e042ea740e1340c3810f4af16d Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Thu, 24 Oct 2024 08:41:26 +0000 Subject: [PATCH 55/69] Merged in feat/SW-673-galleryicon-hotel-lightbox (pull request #734) Feat/SW-673 galleryicon hotel lightbox * feat(SW-673): add galleryChip to trigger lightbox * feat(SW-673): add updated design galleryIcon * feat(SW-673): add first image from hotelContent and heroImages * feat(SW-673): fix import type * feat(SW-673): fix css variables * feat(SW-673): change component to include image that trigger lightbox * feat(SW-673): refactor name to imageGallery Approved-by: Niclas Edenvin --- .../HotelCard/hotelCard.module.css | 13 ++++--- .../HotelReservation/HotelCard/index.tsx | 19 +++++----- .../HotelInfoCard/hotelInfoCard.module.css | 8 ++--- .../SelectRate/HotelInfoCard/index.tsx | 17 +++++---- .../ImageGallery/imageGallery.module.css | 17 +++++++++ .../SelectRate/ImageGallery/index.tsx | 36 +++++++++++++++++++ .../RoomSelection/RoomCard/index.tsx | 29 +++------------ .../RoomCard/roomCard.module.css | 14 -------- server/routers/hotels/output.ts | 16 +++++++++ .../selectRate/imageGallery.ts | 3 ++ types/hotel.ts | 4 ++- 11 files changed, 111 insertions(+), 65 deletions(-) create mode 100644 components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css create mode 100644 components/HotelReservation/SelectRate/ImageGallery/index.tsx create mode 100644 types/components/hotelReservation/selectRate/imageGallery.ts diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 0ffa5b150..587feb29a 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -14,15 +14,16 @@ .imageContainer { grid-area: image; + position: relative; + height: 100%; + width: 116px; } .tripAdvisor { display: none; } -.image { - height: 100%; - width: 116px; +.imageContainer img { object-fit: cover; } @@ -77,6 +78,8 @@ .imageContainer { position: relative; + min-height: 200px; + width: 518px; } .tripAdvisor { @@ -86,10 +89,6 @@ top: 7px; } - .image { - width: 518px; - } - .hotelInformation { padding-top: var(--Spacing-x2); padding-right: var(--Spacing-x2); diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 67ce49f17..9300771e9 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -11,10 +11,11 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import ReadMore from "../ReadMore" +import ImageGallery from "../SelectRate/ImageGallery" import styles from "./hotelCard.module.css" -import { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" +import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" export default async function HotelCard({ hotel }: HotelCardProps) { const intl = await getIntl() @@ -27,13 +28,15 @@ export default async function HotelCard({ hotel }: HotelCardProps) { return (
- {hotelData.hotelContent.images.metaData.altText} + {hotelData.gallery && ( + + )}
diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css index 1482bf4c9..aacdf0449 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css +++ b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css @@ -12,10 +12,6 @@ gap: var(--Spacing-x2); } -.image { - border-radius: var(--Corner-radius-Medium); -} - .imageWrapper { position: relative; overflow: hidden; @@ -24,6 +20,10 @@ width: 100%; } +.imageWrapper img { + border-radius: var(--Corner-radius-Medium); +} + .tripAdvisor { display: flex; align-items: center; diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 1ec58dd07..120c0385d 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -10,6 +10,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" import ReadMore from "../../ReadMore" +import ImageGallery from "../ImageGallery" import styles from "./hotelInfoCard.module.css" @@ -28,12 +29,6 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { {hotelAttributes && (
- {hotelAttributes.hotelContent.images.metaData.altText} {hotelAttributes.ratings?.tripAdvisor && (
@@ -42,7 +37,15 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
)} - {/* TODO: gallery icon and image carousel */} + {hotelAttributes.gallery && ( + + )}
diff --git a/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css b/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css new file mode 100644 index 000000000..5d156f77a --- /dev/null +++ b/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css @@ -0,0 +1,17 @@ +.galleryIcon { + position: absolute; + bottom: 16px; + right: 16px; + max-height: 32px; + width: 48px; + background-color: rgba(0, 0, 0, 0.6); + padding: var(--Spacing-x-quarter) var(--Spacing-x-half); + border-radius: var(--Corner-radius-Small); + display: flex; + align-items: center; + gap: var(--Spacing-x-quarter); +} + +.triggerArea { + cursor: pointer; +} diff --git a/components/HotelReservation/SelectRate/ImageGallery/index.tsx b/components/HotelReservation/SelectRate/ImageGallery/index.tsx new file mode 100644 index 000000000..778c2d45e --- /dev/null +++ b/components/HotelReservation/SelectRate/ImageGallery/index.tsx @@ -0,0 +1,36 @@ +import { GalleryIcon } from "@/components/Icons" +import Image from "@/components/Image" +import Lightbox from "@/components/Lightbox" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" + +import styles from "./imageGallery.module.css" + +import type { ImageGalleryProps } from "@/types/components/hotelReservation/selectRate/imageGallery" + +export default function ImageGallery({ images, title }: ImageGalleryProps) { + return ( + ({ + url: image.imageSizes.small, + alt: image.metaData.altText, + title: image.metaData.title, + }))} + dialogTitle={title} + > +
+ {images[0].metaData.altText} +
+ + + {images.length} + +
+
+
+ ) +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 87b0143b5..444968ce1 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -5,18 +5,18 @@ import { useIntl } from "react-intl" import { RateDefinition } from "@/server/routers/hotels/output" import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption" -import { ChevronRightSmallIcon, GalleryIcon } from "@/components/Icons" -import Image from "@/components/Image" -import Lightbox from "@/components/Lightbox" +import { ChevronRightSmallIcon } from "@/components/Icons" 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 ImageGallery from "../../ImageGallery" + import styles from "./roomCard.module.css" -import { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard" +import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard" export default function RoomCard({ rateDefinitions, @@ -25,7 +25,6 @@ export default function RoomCard({ handleSelectRate, }: RoomCardProps) { const intl = useIntl() - const saveRate = rateDefinitions.find( // TODO: Update string when API has decided (rate) => rate.cancellationRule === "NonCancellable" @@ -153,26 +152,8 @@ export default function RoomCard({ )} {/*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. */} - {mainImage.metaData.altText} {images && ( - ({ - url: image.imageSizes.small, - alt: image.metaData.altText, - title: image.metaData.title, - }))} - dialogTitle={roomConfiguration.roomType} - > -
- - {images.length} -
-
+ )}
)} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index 25add23b5..ef5d9b8fc 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -77,17 +77,3 @@ min-height: 185px; position: relative; } - -.galleryIcon { - position: absolute; - bottom: 16px; - right: 16px; - height: 24px; - background-color: rgba(64, 57, 55, 0.9); - padding: 0 var(--Spacing-x-half); - border-radius: var(--Corner-radius-Small); - cursor: pointer; - display: flex; - align-items: center; - gap: var(--Spacing-x-quarter); -} diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 803ae012f..0f545d673 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -162,6 +162,21 @@ export const facilitySchema = z.object({ ), }) +export const gallerySchema = z.object({ + heroImages: z.array( + z.object({ + metaData: imageMetaDataSchema, + imageSizes: imageSizesSchema, + }) + ), + smallerImages: z.array( + z.object({ + metaData: imageMetaDataSchema, + imageSizes: imageSizesSchema, + }) + ), +}) + const healthFacilitySchema = z.object({ type: z.string(), content: z.object({ @@ -441,6 +456,7 @@ export const getHotelDataSchema = z.object({ conferencesAndMeetings: facilitySchema.optional(), healthAndWellness: facilitySchema.optional(), restaurantImages: facilitySchema.optional(), + gallery: gallerySchema.optional(), }), relationships: relationshipsSchema, }), diff --git a/types/components/hotelReservation/selectRate/imageGallery.ts b/types/components/hotelReservation/selectRate/imageGallery.ts new file mode 100644 index 000000000..333ff2d94 --- /dev/null +++ b/types/components/hotelReservation/selectRate/imageGallery.ts @@ -0,0 +1,3 @@ +import type { GalleryImages } from "@/types/hotel" + +export type ImageGalleryProps = { images: GalleryImages; title: string } diff --git a/types/hotel.ts b/types/hotel.ts index a60204720..972c6459e 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -2,6 +2,7 @@ import { z } from "zod" import { facilitySchema, + gallerySchema, getHotelDataSchema, parkingSchema, pointOfInterestSchema, @@ -13,7 +14,6 @@ export type HotelData = z.infer export type Hotel = HotelData["data"]["attributes"] export type HotelAddress = HotelData["data"]["attributes"]["address"] export type HotelLocation = HotelData["data"]["attributes"]["location"] - export type Amenities = HotelData["data"]["attributes"]["detailedFacilities"] type HotelRatings = HotelData["data"]["attributes"]["ratings"] @@ -22,6 +22,8 @@ export type HotelTripAdvisor = | undefined export type RoomData = z.infer +export type GallerySchema = z.infer +export type GalleryImages = GallerySchema["heroImages"] export type PointOfInterest = z.output From 8d490e14f2ed757d85b8b9a4f2ec52bd6e1ebb7a Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Thu, 24 Oct 2024 08:49:06 +0000 Subject: [PATCH 56/69] fix(SW-690): use correct naming of search params in booking Approved-by: Bianca Widstam Approved-by: Linus Flood --- .../(standard)/select-rate/page.tsx | 14 +++++------ components/BookingWidget/Client.tsx | 4 ++-- components/DatePicker/index.tsx | 24 +++++++++---------- .../Forms/BookingWidget/FormContent/index.tsx | 2 +- components/Forms/BookingWidget/schema.ts | 4 ++-- .../HotelReservation/HotelCard/index.tsx | 6 ++++- components/TempDesignSystem/Link/index.tsx | 7 +++--- .../hotelReservation/selectRate/selectRate.ts | 6 ++--- 8 files changed, 36 insertions(+), 31 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 532228434..2d80356ac 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -17,11 +17,11 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) - const selecetRoomParams = new URLSearchParams(searchParams) - const selecetRoomParamsObject = - getHotelReservationQueryParams(selecetRoomParams) - const adults = selecetRoomParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selecetRoomParamsObject.room[0].child.length // TODO: Handle multiple rooms + 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 [hotelData, roomConfigurations, user] = await Promise.all([ serverClient().hotel.hotelData.get({ @@ -33,8 +33,8 @@ export default async function SelectRatePage({ hotelId: parseInt(searchParams.hotel, 10), roomStayStartDate: searchParams.fromDate, roomStayEndDate: searchParams.toDate, - adults: adults, - children: children, + adults, + children, }), getProfileSafely(), ]) diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index eded233f0..68a6a1fc2 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -42,8 +42,8 @@ export default function BookingWidgetClient({ date: { // 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. - from: dt().utc().format("YYYY-MM-DD"), - to: dt().utc().add(1, "day").format("YYYY-MM-DD"), + fromDate: dt().utc().format("YYYY-MM-DD"), + toDate: dt().utc().add(1, "day").format("YYYY-MM-DD"), }, bookingCode: "", redemption: false, diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx index 503c0c6ac..b07b1a0d8 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/index.tsx @@ -44,22 +44,22 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { function handleSelectDate(selected: Date) { if (isSelectingFrom) { setValue(name, { - from: dt(selected).format("YYYY-MM-DD"), - to: undefined, + fromDate: dt(selected).format("YYYY-MM-DD"), + toDate: undefined, }) setIsSelectingFrom(false) } else { - const fromDate = dt(selectedDate.from) + const fromDate = dt(selectedDate.fromDate) const toDate = dt(selected) if (toDate.isAfter(fromDate)) { setValue(name, { - from: selectedDate.from, - to: toDate.format("YYYY-MM-DD"), + fromDate: selectedDate.fromDate, + toDate: toDate.format("YYYY-MM-DD"), }) } else { setValue(name, { - from: toDate.format("YYYY-MM-DD"), - to: selectedDate.from, + fromDate: toDate.format("YYYY-MM-DD"), + toDate: selectedDate.fromDate, }) } setIsSelectingFrom(true) @@ -79,11 +79,11 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { } }, [setIsOpen]) - const selectedFromDate = dt(selectedDate.from) + const selectedFromDate = dt(selectedDate.fromDate) .locale(lang) .format("ddd D MMM") - const selectedToDate = !!selectedDate.to - ? dt(selectedDate.to).locale(lang).format("ddd D MMM") + const selectedToDate = !!selectedDate.toDate + ? dt(selectedDate.toDate).locale(lang).format("ddd D MMM") : "" return ( @@ -93,8 +93,8 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { {selectedFromDate} - {selectedToDate} - - + +
diff --git a/components/Forms/BookingWidget/schema.ts b/components/Forms/BookingWidget/schema.ts index cfa3cb03f..aa42b542d 100644 --- a/components/Forms/BookingWidget/schema.ts +++ b/components/Forms/BookingWidget/schema.ts @@ -18,8 +18,8 @@ export const bookingWidgetSchema = z.object({ bookingCode: z.string(), // Update this as required when working with booking codes component date: z.object({ // Update this as required once started working with Date picker in Nights component - from: z.string(), - to: z.string(), + fromDate: z.string(), + toDate: z.string(), }), location: z.string().refine( (value) => { diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 9300771e9..2bea7c895 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -105,7 +105,11 @@ export default async function HotelCard({ hotel }: HotelCardProps) { className={styles.button} > {/* TODO: Localize link and also use correct search params */} - + {intl.formatMessage({ id: "See rooms" })} diff --git a/components/TempDesignSystem/Link/index.tsx b/components/TempDesignSystem/Link/index.tsx index 716221b5b..2c46c8b6f 100644 --- a/components/TempDesignSystem/Link/index.tsx +++ b/components/TempDesignSystem/Link/index.tsx @@ -51,9 +51,10 @@ export default function Link({ const router = useRouter() const fullUrl = useMemo(() => { - const search = - keepSearchParams && searchParams.size ? `?${searchParams}` : "" - return `${href}${search}` + if (!keepSearchParams || !searchParams.size) return href + + const delimiter = href.includes("?") ? "&" : "?" + return `${href}${delimiter}${searchParams}` }, [href, searchParams, keepSearchParams]) // TODO: Remove this check (and hook) and only return when current web is deleted diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index 58aa6fbbe..16b3f5dcf 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -7,9 +7,9 @@ interface Child { interface Room { adults: number - roomtypecode: string - ratecode: string - child: Child[] + roomcode?: string + ratecode?: string + child?: Child[] } export interface SelectRateSearchParams { From 12d4be59d574224389d8f567e294dbb421a727c8 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 15 Oct 2024 09:05:20 +0200 Subject: [PATCH 57/69] chore: add a commit to enable pr for testing --- constants/routes/signup.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/constants/routes/signup.ts b/constants/routes/signup.ts index 82a39ee41..9ef09c4a0 100644 --- a/constants/routes/signup.ts +++ b/constants/routes/signup.ts @@ -17,3 +17,11 @@ export const signupVerify: LangRoute = { da: `${signup.da}/bekraeft`, de: `${signup.de}/verifizieren`, } + +export const isSignupPage = (path: string): boolean => { + return Object.values(signup).includes(path) +} + +export const isSignupVerifyPage = (path: string): boolean => { + return Object.values(signupVerify).includes(path) +} From 19049ce22c119c51ab452db4f8e7eef9a21e99cf Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 22 Oct 2024 10:46:11 +0200 Subject: [PATCH 58/69] fix(SW-360): signup cta ui + name lable formatting --- components/Forms/Register/form.module.css | 4 ++++ components/Forms/Register/index.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/Forms/Register/form.module.css b/components/Forms/Register/form.module.css index 612cbc8f8..dc913b792 100644 --- a/components/Forms/Register/form.module.css +++ b/components/Forms/Register/form.module.css @@ -46,4 +46,8 @@ .nameInputs { grid-template-columns: 1fr 1fr; } + + .signUpButton { + width: fit-content; + } } diff --git a/components/Forms/Register/index.tsx b/components/Forms/Register/index.tsx index ed82617b0..a8f3f6524 100644 --- a/components/Forms/Register/index.tsx +++ b/components/Forms/Register/index.tsx @@ -94,12 +94,12 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
@@ -171,7 +171,9 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
From eaf9c2f5f2231c539700703e0e5940b1aab1e328 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Thu, 24 Oct 2024 08:46:14 +0200 Subject: [PATCH 64/69] refactor(SW-360): trigger validation on submnission --- components/Forms/Signup/index.tsx | 55 +++++++++++++++++-------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index d9a273560..39d389e91 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -1,7 +1,6 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useEffect, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" @@ -37,15 +36,13 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { const phoneNumber = intl.formatMessage({ id: "Phone number" }) const zipCode = intl.formatMessage({ id: "Zip code" }) - const [isSubmitAttempted, setIsSubmitAttempted] = useState(false) - const methods = useForm({ defaultValues: { firstName: "", lastName: "", email: "", phoneNumber: "", - dateOfBirth: "", + dateOfBirth: "1995-01-01", address: { countryCode: "", zipCode: "", @@ -59,16 +56,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { reValidateMode: "onChange", }) - // Trigger validation for all fields upon invalid submissions. - useEffect(() => { - if ( - isSubmitAttempted && - (!methods.formState.isValid || !methods.formState.isSubmitting) - ) { - methods.trigger() - } - }, [isSubmitAttempted, methods]) - async function onSubmit(data: SignUpSchema) { try { const result = await registerUser(data) @@ -184,17 +171,35 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
- + + {/* + We use this approach to trigger validation for all invalid inputs upon submission. + To handle this programmatically in the future, we would need some major refactoring + of the Input component, which is out of scope for now. + */} + {!methods.formState.isValid ? ( + + ) : ( + + )}
From 62eb5a9bb5dd2a94c86a1f2e4df51c58bac0a57c Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Thu, 24 Oct 2024 08:49:53 +0200 Subject: [PATCH 65/69] fix(SW-360): remove default date --- components/Forms/Signup/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index 39d389e91..a64569f80 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -42,7 +42,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { lastName: "", email: "", phoneNumber: "", - dateOfBirth: "1995-01-01", + dateOfBirth: "", address: { countryCode: "", zipCode: "", From e25a974426cbb9a4befb85468a6fe584ae9fb7d3 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Thu, 24 Oct 2024 09:19:14 +0200 Subject: [PATCH 66/69] fix(SW-360): more context to how we handle triggers --- components/Forms/Signup/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index a64569f80..9f061f614 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -173,9 +173,10 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
{/* - We use this approach to trigger validation for all invalid inputs upon submission. - To handle this programmatically in the future, we would need some major refactoring - of the Input component, which is out of scope for now. + This is a manual validation trigger workaround: + - The Controller component (which Input uses) doesn't re-render on submit, + which prevents automatic error display. + - Future fix requires Input component refactoring (out of scope for now). */} {!methods.formState.isValid ? (