{heading && (
diff --git a/components/TempDesignSystem/Tooltip/tooltip.module.css b/components/TempDesignSystem/Tooltip/tooltip.module.css
index da8e50cbd..e25433f7c 100644
--- a/components/TempDesignSystem/Tooltip/tooltip.module.css
+++ b/components/TempDesignSystem/Tooltip/tooltip.module.css
@@ -1,6 +1,6 @@
.tooltipContainer {
position: relative;
- display: inline-block;
+ display: flex;
}
.tooltip {
@@ -140,3 +140,15 @@
border-width: 7px 8px 7px 0;
border-color: transparent var(--UI-Text-Active) transparent transparent;
}
+
+@media screen and (max-width: 768px) {
+ .tooltipContainer[data-active="true"] .tooltip {
+ visibility: visible;
+ opacity: 1;
+ }
+
+ .tooltipContainer[data-active="false"] .tooltip {
+ visibility: hidden;
+ opacity: 0;
+ }
+}
diff --git a/constants/routes/signup.ts b/constants/routes/signup.ts
index 82a39ee41..4c63cc47d 100644
--- a/constants/routes/signup.ts
+++ b/constants/routes/signup.ts
@@ -17,3 +17,8 @@ export const signupVerify: LangRoute = {
da: `${signup.da}/bekraeft`,
de: `${signup.de}/verifizieren`,
}
+
+export function isSignupPage(path: string): boolean {
+ const signupPaths = [...Object.values(signup), ...Object.values(signupVerify)]
+ return signupPaths.some((signupPath) => signupPath.includes(path))
+}
diff --git a/env/server.ts b/env/server.ts
index 46adf2ae5..8b810d265 100644
--- a/env/server.ts
+++ b/env/server.ts
@@ -67,6 +67,13 @@ export const env = createEnv({
SEAMLESS_LOGOUT_FI: z.string(),
SEAMLESS_LOGOUT_NO: z.string(),
SEAMLESS_LOGOUT_SV: z.string(),
+ SHOW_SIGNUP_FLOW: z
+ .string()
+ // only allow "true" or "false"
+ .refine((s) => s === "true" || s === "false")
+ // transform to boolean
+ .transform((s) => s === "true")
+ .default("false"),
WEBVIEW_ENCRYPTION_KEY: z.string(),
BOOKING_ENCRYPTION_KEY: z.string(),
GOOGLE_STATIC_MAP_KEY: z.string(),
@@ -79,6 +86,13 @@ export const env = createEnv({
.refine((s) => s === "true" || s === "false")
// transform to boolean
.transform((s) => s === "true"),
+ USE_NEW_REWARDS_ENDPOINT: z
+ .string()
+ // only allow "true" or "false"
+ .refine((s) => s === "true" || s === "false")
+ // transform to boolean
+ .transform((s) => s === "true")
+ .default("false"),
},
emptyStringAsUndefined: true,
runtimeEnv: {
@@ -126,6 +140,7 @@ export const env = createEnv({
SEAMLESS_LOGOUT_FI: process.env.SEAMLESS_LOGOUT_FI,
SEAMLESS_LOGOUT_NO: process.env.SEAMLESS_LOGOUT_NO,
SEAMLESS_LOGOUT_SV: process.env.SEAMLESS_LOGOUT_SV,
+ SHOW_SIGNUP_FLOW: process.env.SHOW_SIGNUP_FLOW,
WEBVIEW_ENCRYPTION_KEY: process.env.WEBVIEW_ENCRYPTION_KEY,
BOOKING_ENCRYPTION_KEY: process.env.BOOKING_ENCRYPTION_KEY,
GOOGLE_STATIC_MAP_KEY: process.env.GOOGLE_STATIC_MAP_KEY,
@@ -134,5 +149,6 @@ export const env = createEnv({
GOOGLE_STATIC_MAP_ID: process.env.GOOGLE_STATIC_MAP_ID,
GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID,
HIDE_FOR_NEXT_RELEASE: process.env.HIDE_FOR_NEXT_RELEASE,
+ USE_NEW_REWARDS_ENDPOINT: process.env.USE_NEW_REWARDS_ENDPOINT,
},
})
diff --git a/hooks/useMediaQuery.ts b/hooks/useMediaQuery.ts
deleted file mode 100644
index bbb9eabac..000000000
--- a/hooks/useMediaQuery.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useEffect, useState } from "react"
-
-function useMediaQuery(query: string) {
- const [isMatch, setIsMatch] = useState(false)
-
- useEffect(() => {
- const media = window.matchMedia(query)
- if (media.matches !== isMatch) {
- setIsMatch(media.matches)
- }
-
- const listener = () => setIsMatch(media.matches)
- media.addEventListener("change", listener)
-
- return () => media.removeEventListener("change", listener)
- }, [isMatch, query])
-
- return isMatch
-}
-
-export default useMediaQuery
diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json
index a40e050ed..7cd40f4c5 100644
--- a/i18n/dictionaries/da.json
+++ b/i18n/dictionaries/da.json
@@ -7,7 +7,7 @@
"ACCE": "Tilgængelighed",
"ALLG": "Allergi",
"About meetings & conferences": "About meetings & conferences",
- "About the hotel": "About the hotel",
+ "About the hotel": "Om hotellet",
"Accessibility": "Tilgængelighed",
"Accessible Room": "Tilgængelighedsrum",
"Activities": "Aktiviteter",
@@ -53,6 +53,7 @@
"Bus terminal": "Busstation",
"Business": "Forretning",
"Cancel": "Afbestille",
+ "Change room": "Skift værelse",
"Check in": "Check ind",
"Check out": "Check ud",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Bookingkoder / voucher",
"Coming up": "Er lige om hjørnet",
"Compare all levels": "Sammenlign alle niveauer",
+ "Complete booking": "Fuldfør bookingen",
"Complete booking & go to payment": "Udfyld booking & gå til betaling",
"Complete the booking": "Fuldfør bookingen",
"Contact information": "Kontaktoplysninger",
@@ -96,7 +98,8 @@
"Disabled booking options text": "Koder, checks og bonusnætter er endnu ikke tilgængelige på den nye hjemmeside.",
"Discard changes": "Kassér ændringer",
"Discard unsaved changes?": "Slette ændringer, der ikke er gemt?",
- "Distance to city centre": "{number}km til centrum",
+ "Distance in km to city centre": "{number} km til centrum",
+ "Distance to city centre": "Afstand til centrum",
"Distance to hotel": "Afstand til hotel",
"Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte morgenbuffet?",
"Done": "Færdig",
@@ -118,6 +121,7 @@
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
"Fair": "Messe",
"Filter": "Filter",
+ "Filter by": "Filtrer efter",
"Find booking": "Find booking",
"Find hotels": "Find hotel",
"First name": "Fornavn",
@@ -149,7 +153,7 @@
"How it works": "Hvordan det virker",
"Hurry up and use them before they expire!": "Skynd dig og brug dem, før de udløber!",
"I would like to get my booking confirmation via sms": "Jeg vil gerne få min booking bekræftelse via SMS",
- "Image gallery": "Billedgalleri",
+ "Image gallery": "{name} - Billedgalleri",
"In adults bed": "i de voksnes seng",
"In crib": "i tremmeseng",
"In extra bed": "i ekstra seng",
@@ -157,6 +161,7 @@
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.",
"Join Scandic Friends": "Tilmeld dig Scandic Friends",
"Join at no cost": "Tilmeld dig uden omkostninger",
+ "Join or log in while booking for member pricing.": "Tilmeld dig eller log ind under booking for medlemspris.",
"King bed": "Kingsize-seng",
"Language": "Sprog",
"Last name": "Efternavn",
@@ -201,12 +206,14 @@
"My pages menu": "Mine sider menu",
"My payment cards": "Mine betalingskort",
"My wishes": "Mine ønsker",
+ "Name": "Navn",
"Nearby": "I nærheden",
"Nearby companies": "Nærliggende virksomheder",
"New password": "Nyt kodeord",
"Next": "Næste",
"Nights needed to level up": "Nætter nødvendige for at komme i niveau",
"No": "Nej",
+ "No availability": "Ingen tilgængelighed",
"No breakfast": "Ingen morgenmad",
"No content published": "Intet indhold offentliggjort",
"No matching location found": "Der blev ikke fundet nogen matchende placering",
@@ -224,6 +231,7 @@
"On your journey": "På din rejse",
"Open": "Åben",
"Open gift(s)": "Åbne {amount, plural, one {gave} other {gaver}}",
+ "Open image gallery": "Åbn billedgalleri",
"Open language menu": "Åbn sprogmenuen",
"Open menu": "Åbn menuen",
"Open my pages menu": "Åbn mine sider menuen",
@@ -254,6 +262,7 @@
"Practical information": "Praktisk information",
"Previous": "Forudgående",
"Previous victories": "Tidligere sejre",
+ "Price": "Pris",
"Price details": "Prisoplysninger",
"Proceed to login": "Fortsæt til login",
"Proceed to payment method": "Fortsæt til betalingsmetode",
@@ -285,8 +294,10 @@
"Search": "Søge",
"See all FAQ": "Se alle FAQ",
"See all photos": "Se alle billeder",
+ "See details": "Se detaljer",
"See hotel details": "Se hoteloplysninger",
"See less FAQ": "Se mindre FAQ",
+ "See map": "Vis kort",
"See on map": "Se på kort",
"See room details": "Se værelsesdetaljer",
"See rooms": "Se værelser",
@@ -304,7 +315,6 @@
"Show all amenities": "Vis alle faciliteter",
"Show less": "Vis mindre",
"Show less rooms": "Vise færre rum",
- "Show map": "Vis kort",
"Show more": "Vis mere",
"Show more rooms": "Vise flere rum",
"Sign up bonus": "Velkomstbonus",
@@ -325,8 +335,12 @@
"Terms and conditions": "Vilkår og betingelser",
"Thank you": "Tak",
"Theatre": "Teater",
+ "There are no rooms available that match your request": "Der er ingen ledige værelser, der matcher din anmodning",
+ "There are no rooms available that match your request.": "Der er ingen værelser tilgængelige, der matcher din forespørgsel.",
"There are no transactions to display": "Der er ingen transaktioner at vise",
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
+ "This room is not available": "Dette værelse er ikke tilgængeligt",
+ "To get the member price {amount} {currency}, log in or join when completing the booking.": "For at få medlemsprisen {amount} {currency}, log ind eller tilmeld dig, når du udfylder bookingen.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.",
"Total Points": "Samlet antal point",
"Total price": "Samlet pris",
@@ -335,6 +349,7 @@
"Transaction date": "Overførselsdato",
"Transactions": "Transaktioner",
"Transportations": "Transport",
+ "TripAdvisor rating": "TripAdvisor vurdering",
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
"Type of bed": "Sengtype",
"Type of room": "Værelsestype",
@@ -404,7 +419,7 @@
"guaranteeing": "garanti",
"guest": "gæst",
"guests": "gæster",
- "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}",
+ "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personer}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer",
"km to city center": "km til byens centrum",
"lowercase letter": "lille bogstav",
diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json
index abe5d9431..2d28e2626 100644
--- a/i18n/dictionaries/de.json
+++ b/i18n/dictionaries/de.json
@@ -53,6 +53,7 @@
"Bus terminal": "Busbahnhof",
"Business": "Geschäft",
"Cancel": "Stornieren",
+ "Change room": "Zimmer ändern",
"Check in": "Einchecken",
"Check out": "Auschecken",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Buchungscodes / Gutscheine",
"Coming up": "Demnächst",
"Compare all levels": "Vergleichen Sie alle Levels",
+ "Complete booking": "Buchung abschließen",
"Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen",
"Complete the booking": "Buchung abschließen",
"Contact information": "Kontaktinformationen",
@@ -96,7 +98,8 @@
"Disabled booking options text": "Codes, Schecks und Bonusnächte sind auf der neuen Website noch nicht verfügbar.",
"Discard changes": "Änderungen verwerfen",
"Discard unsaved changes?": "Nicht gespeicherte Änderungen verwerfen?",
- "Distance to city centre": "{number}km zum Stadtzentrum",
+ "Distance in km to city centre": "{number} km zum Stadtzentrum",
+ "Distance to city centre": "Entfernung zum Stadtzentrum",
"Distance to hotel": "Entfernung zum Hotel",
"Do you want to start the day with Scandics famous breakfast buffé?": "Möchten Sie den Tag mit Scandics berühmtem Frühstücksbuffet beginnen?",
"Done": "Fertig",
@@ -118,6 +121,7 @@
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
"Fair": "Messe",
"Filter": "Filter",
+ "Filter by": "Filtern nach",
"Find booking": "Buchung finden",
"Find hotels": "Hotels finden",
"First name": "Vorname",
@@ -149,7 +153,7 @@
"How it works": "Wie es funktioniert",
"Hurry up and use them before they expire!": "Beeilen Sie sich und nutzen Sie sie, bevor sie ablaufen!",
"I would like to get my booking confirmation via sms": "Ich möchte meine Buchungsbestätigung per SMS erhalten",
- "Image gallery": "Bildergalerie",
+ "Image gallery": "{name} - Bildergalerie",
"In adults bed": "Im Bett der Eltern",
"In crib": "im Kinderbett",
"In extra bed": "im zusätzlichen Bett",
@@ -157,6 +161,7 @@
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.",
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
"Join at no cost": "Kostenlos beitreten",
+ "Join or log in while booking for member pricing.": "Treten Sie Scandic Friends bei oder loggen Sie sich ein, um den Mitgliederpreis zu erhalten.",
"King bed": "Kingsize-Bett",
"Language": "Sprache",
"Last name": "Nachname",
@@ -199,12 +204,14 @@
"My pages menu": "Meine Seite Menü",
"My payment cards": "Meine Zahlungskarten",
"My wishes": "Meine Wünsche",
+ "Name": "Name",
"Nearby": "In der Nähe",
"Nearby companies": "Nahe gelegene Unternehmen",
"New password": "Neues Kennwort",
"Next": "Nächste",
"Nights needed to level up": "Nächte, die zum Levelaufstieg benötigt werden",
"No": "Nein",
+ "No availability": "Keine Verfügbarkeit",
"No breakfast": "Kein Frühstück",
"No content published": "Kein Inhalt veröffentlicht",
"No matching location found": "Kein passender Standort gefunden",
@@ -222,6 +229,7 @@
"On your journey": "Auf deiner Reise",
"Open": "Offen",
"Open gift(s)": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen",
+ "Open image gallery": "Bildergalerie öffnen",
"Open language menu": "Sprachmenü öffnen",
"Open menu": "Menü öffnen",
"Open my pages menu": "Meine Seiten Menü öffnen",
@@ -252,6 +260,7 @@
"Practical information": "Praktische Informationen",
"Previous": "Früher",
"Previous victories": "Bisherige Siege",
+ "Price": "Preis",
"Price details": "Preisdetails",
"Proceed to login": "Weiter zum Login",
"Proceed to payment method": "Weiter zur Zahlungsmethode",
@@ -284,8 +293,10 @@
"Search": "Suchen",
"See all FAQ": "Siehe alle FAQ",
"See all photos": "Alle Fotos ansehen",
+ "See details": "Siehe Einzelheiten",
"See hotel details": "Hotelinformationen ansehen",
"See less FAQ": "Weniger anzeigen FAQ",
+ "See map": "Karte anzeigen",
"See on map": "Karte ansehen",
"See room details": "Zimmerdetails ansehen",
"See rooms": "Zimmer ansehen",
@@ -303,7 +314,6 @@
"Show all amenities": "Alle Annehmlichkeiten anzeigen",
"Show less": "Weniger anzeigen",
"Show less rooms": "Weniger Zimmer anzeigen",
- "Show map": "Karte anzeigen",
"Show more": "Mehr anzeigen",
"Show more rooms": "Weitere Räume anzeigen",
"Sign up bonus": "Anmelde-Bonus",
@@ -324,8 +334,12 @@
"Terms and conditions": "Geschäftsbedingungen",
"Thank you": "Danke",
"Theatre": "Theater",
+ "There are no rooms available that match your request": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen",
+ "There are no rooms available that match your request.": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen.",
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
"Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}",
+ "This room is not available": "Dieses Zimmer ist nicht verfügbar",
+ "To get the member price {amount} {currency}, log in or join when completing the booking.": "Um den Mitgliederpreis von {amount} {currency} zu erhalten, loggen Sie sich ein oder treten Sie Scandic Friends bei, wenn Sie die Buchung abschließen.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.",
"Total Points": "Gesamtpunktzahl",
"Total price": "Gesamtpreis",
@@ -334,6 +348,7 @@
"Transaction date": "Transaktionsdatum",
"Transactions": "Transaktionen",
"Transportations": "Transportmittel",
+ "TripAdvisor rating": "TripAdvisor-Bewertung",
"Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)",
"Type of bed": "Bettentyp",
"Type of room": "Zimmerart",
@@ -403,7 +418,7 @@
"guaranteeing": "garantiert",
"guest": "gast",
"guests": "gäste",
- "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personen}}",
+ "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personen}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen",
"km to city center": "km bis zum Stadtzentrum",
"lowercase letter": "Kleinbuchstabe",
diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json
index 75abdc4d4..d5ab06e41 100644
--- a/i18n/dictionaries/en.json
+++ b/i18n/dictionaries/en.json
@@ -80,6 +80,7 @@
"Code / Voucher": "Code / Voucher",
"Coming up": "Coming up",
"Compare all levels": "Compare all levels",
+ "Complete booking": "Complete booking",
"Complete booking & go to payment": "Complete booking & go to payment",
"Complete the booking": "Complete the booking",
"Contact information": "Contact information",
@@ -105,7 +106,8 @@
"Disabled booking options text": "Codes, cheques and reward nights aren't available on the new website yet.",
"Discard changes": "Discard changes",
"Discard unsaved changes?": "Discard unsaved changes?",
- "Distance to city centre": "{number}km to city centre",
+ "Distance in km to city centre": "{number} km to city centre",
+ "Distance to city centre": "Distance to city centre",
"Distance to hotel": "Distance to hotel",
"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",
@@ -128,6 +130,7 @@
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
"Fair": "Fair",
"Filter": "Filter",
+ "Filter by": "Filter by",
"Find booking": "Find booking",
"Find hotels": "Find hotels",
"First name": "First name",
@@ -162,7 +165,7 @@
"How it works": "How it works",
"Hurry up and use them before they expire!": "Hurry up and use them before they expire!",
"I would like to get my booking confirmation via sms": "I would like to get my booking confirmation via sms",
- "Image gallery": "Image gallery",
+ "Image gallery": "{name} - Image gallery",
"In adults bed": "In adults bed",
"In crib": "In crib",
"In extra bed": "In extra bed",
@@ -170,6 +173,7 @@
"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 Scandic Friends": "Join Scandic Friends",
"Join at no cost": "Join at no cost",
+ "Join or log in while booking for member pricing.": "Join or log in while booking for member pricing.",
"King bed": "King bed",
"Language": "Language",
"Last name": "Last name",
@@ -219,12 +223,14 @@
"My pages menu": "My pages menu",
"My payment cards": "My payment cards",
"My wishes": "My wishes",
+ "Name": "Name",
"Nearby": "Nearby",
"Nearby companies": "Nearby companies",
"New password": "New password",
"Next": "Next",
"Nights needed to level up": "Nights needed to level up",
"No": "No",
+ "No availability": "No availability",
"No breakfast": "No breakfast",
"No content published": "No content published",
"No matching location found": "No matching location found",
@@ -242,6 +248,7 @@
"On your journey": "On your journey",
"Open": "Open",
"Open gift(s)": "Open {amount, plural, one {gift} other {gifts}}",
+ "Open image gallery": "Open image gallery",
"Open language menu": "Open language menu",
"Open menu": "Open menu",
"Open my pages menu": "Open my pages menu",
@@ -276,6 +283,7 @@
"Practical information": "Practial information",
"Previous": "Previous",
"Previous victories": "Previous victories",
+ "Price": "Price",
"Price details": "Price details",
"Price excl VAT": "Price excl VAT",
"Price incl VAT": "Price incl VAT",
@@ -314,8 +322,10 @@
"Search": "Search",
"See all FAQ": "See all FAQ",
"See all photos": "See all photos",
+ "See details": "See details",
"See hotel details": "See hotel details",
"See less FAQ": "See less FAQ",
+ "See map": "See map",
"See on map": "See on map",
"See room details": "See room details",
"See rooms": "See rooms",
@@ -334,7 +344,6 @@
"Show all amenities": "Show all amenities",
"Show less": "Show less",
"Show less rooms": "Show less rooms",
- "Show map": "Show map",
"Show more": "Show more",
"Show more rooms": "Show more rooms",
"Sign up bonus": "Sign up bonus",
@@ -355,8 +364,12 @@
"Terms and conditions": "Terms and conditions",
"Thank you": "Thank you",
"Theatre": "Theatre",
+ "There are no rooms available that match your request": "There are no rooms available that match your request",
+ "There are no rooms available that match your request.": "There are no rooms available that match your request.",
"There are no transactions to display": "There are no transactions to display",
"Things nearby HOTEL_NAME": "Things nearby {hotelName}",
+ "This room is not available": "This room is not available",
+ "To get the member price {amount} {currency}, log in or join when completing the booking.": "To get the member price {amount} {currency}, log in or join when completing the booking.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.",
"Total Points": "Total Points",
"Total cost": "Total cost",
@@ -365,6 +378,7 @@
"Transaction date": "Transaction date",
"Transactions": "Transactions",
"Transportations": "Transportations",
+ "TripAdvisor rating": "TripAdvisor rating",
"Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)",
"Type of bed": "Type of bed",
"Type of room": "Type of room",
@@ -442,7 +456,7 @@
"guest": "guest",
"guest.paid": "{amount} {currency} has been paid",
"guests": "guests",
- "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# persons}}",
+ "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# persons}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
"km to city center": "km to city center",
"lowercase letter": "lowercase letter",
diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json
index c55b5cd2c..114531609 100644
--- a/i18n/dictionaries/fi.json
+++ b/i18n/dictionaries/fi.json
@@ -53,6 +53,7 @@
"Bus terminal": "Bussiasema",
"Business": "Business",
"Cancel": "Peruuttaa",
+ "Change room": "Vaihda huonetta",
"Check in": "Sisäänkirjautuminen",
"Check out": "Uloskirjautuminen",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Varauskoodit / kupongit",
"Coming up": "Tulossa",
"Compare all levels": "Vertaa kaikkia tasoja",
+ "Complete booking": "Täydennä varaus",
"Complete booking & go to payment": "Täydennä varaus & siirry maksamaan",
"Complete the booking": "Täydennä varaus",
"Contact information": "Yhteystiedot",
@@ -96,7 +98,8 @@
"Disabled booking options text": "Koodit, sekit ja palkintoillat eivät ole vielä saatavilla uudella verkkosivustolla.",
"Discard changes": "Hylkää muutokset",
"Discard unsaved changes?": "Hylkäätkö tallentamattomat muutokset?",
- "Distance to city centre": "{number}km Etäisyys kaupunkiin",
+ "Distance in km to city centre": "{number} km Etäisyys kaupunkiin",
+ "Distance to city centre": "Etäisyys kaupungin keskustaan",
"Distance to hotel": "Etäisyys hotelliin",
"Do you want to start the day with Scandics famous breakfast buffé?": "Haluatko aloittaa päiväsi Scandicsin kuuluisalla aamiaisbuffella?",
"Done": "Valmis",
@@ -118,6 +121,7 @@
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
"Fair": "Messukeskus",
"Filter": "Suodatin",
+ "Filter by": "Suodatusperuste",
"Find booking": "Etsi varaus",
"Find hotels": "Etsi hotelleja",
"First name": "Etunimi",
@@ -149,7 +153,7 @@
"How it works": "Kuinka se toimii",
"Hurry up and use them before they expire!": "Ole nopea ja käytä ne ennen kuin ne vanhenevat!",
"I would like to get my booking confirmation via sms": "Haluan saada varauksen vahvistuksen SMS-viestillä",
- "Image gallery": "Kuvagalleria",
+ "Image gallery": "{name} - Kuvagalleria",
"In adults bed": "Aikuisten vuoteessa",
"In crib": "Pinnasängyssä",
"In extra bed": "Oma vuodepaikka",
@@ -157,6 +161,7 @@
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
"Join Scandic Friends": "Liity jäseneksi",
"Join at no cost": "Liity maksutta",
+ "Join or log in while booking for member pricing.": "Liity tai kirjaudu sisään, kun varaat jäsenhinnan.",
"King bed": "King-vuode",
"Language": "Kieli",
"Last name": "Sukunimi",
@@ -201,12 +206,14 @@
"My pages menu": "Omat sivut -valikko",
"My payment cards": "Minun maksukortit",
"My wishes": "Toiveeni",
+ "Name": "Nimi",
"Nearby": "Lähistöllä",
"Nearby companies": "Läheiset yritykset",
"New password": "Uusi salasana",
"Next": "Seuraava",
"Nights needed to level up": "Yöt, joita tarvitaan tasolle",
"No": "Ei",
+ "No availability": "Ei saatavuutta",
"No breakfast": "Ei aamiaista",
"No content published": "Ei julkaistua sisältöä",
"No matching location found": "Vastaavaa sijaintia ei löytynyt",
@@ -224,6 +231,7 @@
"On your journey": "Matkallasi",
"Open": "Avata",
"Open gift(s)": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}",
+ "Open image gallery": "Avaa kuvagalleria",
"Open language menu": "Avaa kielivalikko",
"Open menu": "Avaa valikko",
"Open my pages menu": "Avaa omat sivut -valikko",
@@ -254,6 +262,7 @@
"Practical information": "Käytännön tietoa",
"Previous": "Aikaisempi",
"Previous victories": "Edelliset voitot",
+ "Price": "Hinta",
"Price details": "Hintatiedot",
"Proceed to login": "Jatka kirjautumiseen",
"Proceed to payment method": "Siirry maksutavalle",
@@ -286,8 +295,10 @@
"Search": "Haku",
"See all FAQ": "Katso kaikki UKK",
"See all photos": "Katso kaikki kuvat",
+ "See details": "Katso tiedot",
"See hotel details": "Katso hotellin tiedot",
"See less FAQ": "Katso vähemmän UKK",
+ "See map": "Näytä kartta",
"See on map": "Näytä kartalla",
"See room details": "Katso huoneen tiedot",
"See rooms": "Katso huoneet",
@@ -305,7 +316,6 @@
"Show all amenities": "Näytä kaikki mukavuudet",
"Show less": "Näytä vähemmän",
"Show less rooms": "Näytä vähemmän huoneita",
- "Show map": "Näytä kartta",
"Show more": "Näytä lisää",
"Show more rooms": "Näytä lisää huoneita",
"Sign up bonus": "Liittymisbonus",
@@ -326,8 +336,12 @@
"Terms and conditions": "Käyttöehdot",
"Thank you": "Kiitos",
"Theatre": "Teatteri",
+ "There are no rooms available that match your request": "Pyyntöäsi vastaavia huoneita ei ole saatavilla",
+ "There are no rooms available that match your request.": "Ei huoneita saatavilla, jotka vastaavat pyyntöäsi.",
"There are no transactions to display": "Näytettäviä tapahtumia ei ole",
"Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}",
+ "This room is not available": "Tämä huone ei ole käytettävissä",
+ "To get the member price {amount} {currency}, log in or join when completing the booking.": "Jäsenhintaan saavat sisäänkirjautuneet tai liittyneet jäsenet.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.",
"Total Points": "Kokonaispisteet",
"Total price": "Kokonaishinta",
@@ -336,6 +350,7 @@
"Transaction date": "Tapahtuman päivämäärä",
"Transactions": "Tapahtumat",
"Transportations": "Kuljetukset",
+ "TripAdvisor rating": "TripAdvisor-luokitus",
"Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)",
"Type of bed": "Vuodetyyppi",
"Type of room": "Huonetyyppi",
@@ -403,7 +418,7 @@
"guaranteeing": "varmistetaan",
"guest": "Vieras",
"guests": "Vieraita",
- "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# henkilö} other {# Henkilöä}}",
+ "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# henkilö} other {# Henkilöä}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot",
"km to city center": "km keskustaan",
"lowercase letter": "pien kirjain",
diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json
index 91d28c0b2..a2d8f8bee 100644
--- a/i18n/dictionaries/no.json
+++ b/i18n/dictionaries/no.json
@@ -53,6 +53,7 @@
"Bus terminal": "Bussterminal",
"Business": "Forretnings",
"Cancel": "Avbryt",
+ "Change room": "Endre rom",
"Check in": "Sjekk inn",
"Check out": "Sjekk ut",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Bestillingskoder / kuponger",
"Coming up": "Kommer opp",
"Compare all levels": "Sammenlign alle nivåer",
+ "Complete booking": "Fullfør reservasjonen",
"Complete booking & go to payment": "Fullfør bestilling & gå til betaling",
"Complete the booking": "Fullfør reservasjonen",
"Contact information": "Kontaktinformasjon",
@@ -95,7 +97,8 @@
"Disabled booking options text": "Koder, checks og belønningsnætter er enda ikke tilgjengelige på den nye nettsiden.",
"Discard changes": "Forkaste endringer",
"Discard unsaved changes?": "Forkaste endringer som ikke er lagret?",
- "Distance to city centre": "{number}km til sentrum",
+ "Distance in km to city centre": "{number} km til sentrum",
+ "Distance to city centre": "Avstand til sentrum",
"Distance to hotel": "Avstand til hotell",
"Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte frokostbuffé?",
"Done": "Ferdig",
@@ -117,6 +120,7 @@
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
"Fair": "Messe",
"Filter": "Filter",
+ "Filter by": "Filtrer etter",
"Find booking": "Finn booking",
"Find hotels": "Finn hotell",
"First name": "Fornavn",
@@ -147,7 +151,7 @@
"How do you want to sleep?": "Hvordan vil du sove?",
"How it works": "Hvordan det fungerer",
"Hurry up and use them before they expire!": "Skynd deg og bruk dem før de utløper!",
- "Image gallery": "Bildegalleri",
+ "Image gallery": "{name} - Bildegalleri",
"In adults bed": "i voksnes seng",
"In crib": "i sprinkelseng",
"In extra bed": "i ekstraseng",
@@ -155,6 +159,7 @@
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.",
"Join Scandic Friends": "Bli med i Scandic Friends",
"Join at no cost": "Bli med uten kostnad",
+ "Join or log in while booking for member pricing.": "Bli med eller logg inn under bestilling for medlemspris.",
"King bed": "King-size-seng",
"Language": "Språk",
"Last name": "Etternavn",
@@ -199,12 +204,14 @@
"My pages menu": "Mine sider-menyen",
"My payment cards": "Mine betalingskort",
"My wishes": "Mine ønsker",
+ "Name": "Navn",
"Nearby": "I nærheten",
"Nearby companies": "Nærliggende selskaper",
"New password": "Nytt passord",
"Next": "Neste",
"Nights needed to level up": "Netter som trengs for å komme opp i nivå",
"No": "Nei",
+ "No availability": "Ingen tilgjengelighet",
"No breakfast": "Ingen frokost",
"No content published": "Ingen innhold publisert",
"No matching location found": "Fant ingen samsvarende plassering",
@@ -222,6 +229,7 @@
"On your journey": "På reisen din",
"Open": "Åpen",
"Open gift(s)": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}",
+ "Open image gallery": "Åpne bildegalleri",
"Open language menu": "Åpne språkmenyen",
"Open menu": "Åpne menyen",
"Open my pages menu": "Åpne mine sider menyen",
@@ -252,6 +260,7 @@
"Practical information": "Praktisk informasjon",
"Previous": "Tidligere",
"Previous victories": "Tidligere seire",
+ "Price": "Pris",
"Price details": "Prisdetaljer",
"Proceed to login": "Fortsett til innlogging",
"Proceed to payment method": "Fortsett til betalingsmetode",
@@ -283,8 +292,10 @@
"Search": "Søk",
"See all FAQ": "Se alle FAQ",
"See all photos": "Se alle bilder",
+ "See details": "Se detaljer",
"See hotel details": "Se hotellinformasjon",
"See less FAQ": "Se mindre FAQ",
+ "See map": "Vis kart",
"See on map": "Se på kart",
"See room details": "Se detaljer om rommet",
"See rooms": "Se rom",
@@ -302,7 +313,6 @@
"Show all amenities": "Vis alle fasiliteter",
"Show less": "Vis mindre",
"Show less rooms": "Vise færre rom",
- "Show map": "Vis kart",
"Show more": "Vis mer",
"Show more rooms": "Vise flere rom",
"Sign up bonus": "Velkomstbonus",
@@ -323,8 +333,12 @@
"Terms and conditions": "Vilkår og betingelser",
"Thank you": "Takk",
"Theatre": "Teater",
+ "There are no rooms available that match your request": "Det er ingen tilgjengelige rom som samsvarer med forespørselen din",
+ "There are no rooms available that match your request.": "Det er ingen rom tilgjengelige som matcher din forespørsel.",
"There are no transactions to display": "Det er ingen transaksjoner å vise",
"Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}",
+ "This room is not available": "Dette rommet er ikke tilgjengelig",
+ "To get the member price {amount} {currency}, log in or join when completing the booking.": "For å få medlemsprisen {amount} {currency}, logg inn eller bli med når du fullfører bestillingen.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.",
"Total Points": "Totale poeng",
"Total incl VAT": "Sum inkl mva",
@@ -333,6 +347,7 @@
"Transaction date": "Transaksjonsdato",
"Transactions": "Transaksjoner",
"Transportations": "Transport",
+ "TripAdvisor rating": "TripAdvisor vurdering",
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
"Type of bed": "Sengtype",
"Type of room": "Romtype",
@@ -401,7 +416,7 @@
"guaranteeing": "garantiert",
"guest": "gjest",
"guests": "gjester",
- "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}",
+ "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personer}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet",
"km to city center": "km til sentrum",
"lowercase letter": "liten bokstav",
diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json
index 9834bb6ac..117af5485 100644
--- a/i18n/dictionaries/sv.json
+++ b/i18n/dictionaries/sv.json
@@ -53,6 +53,7 @@
"Bus terminal": "Bussterminal",
"Business": "Business",
"Cancel": "Avbryt",
+ "Change room": "Ändra rum",
"Check in": "Checka in",
"Check out": "Checka ut",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Bokningskoder / kuponger",
"Coming up": "Kommer härnäst",
"Compare all levels": "Jämför alla nivåer",
+ "Complete booking": "Slutför bokning",
"Complete booking & go to payment": "Fullför bokning & gå till betalning",
"Complete the booking": "Slutför bokningen",
"Contact information": "Kontaktinformation",
@@ -95,7 +97,8 @@
"Disabled booking options text": "Koder, bonuscheckar och belöningsnätter är inte tillgängliga på den nya webbplatsen än.",
"Discard changes": "Ignorera ändringar",
"Discard unsaved changes?": "Vill du ignorera ändringar som inte har sparats?",
- "Distance to city centre": "{number}km till centrum",
+ "Distance in km to city centre": "{number} km till centrum",
+ "Distance to city centre": "Avstånd till centrum",
"Distance to hotel": "Avstånd till hotell",
"Do you want to start the day with Scandics famous breakfast buffé?": "Vill du starta dagen med Scandics berömda frukostbuffé?",
"Done": "Klar",
@@ -117,6 +120,7 @@
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
"Fair": "Mässa",
"Filter": "Filter",
+ "Filter by": "Filtrera på",
"Find booking": "Hitta bokning",
"Find hotels": "Hitta hotell",
"First name": "Förnamn",
@@ -147,7 +151,7 @@
"How do you want to sleep?": "Hur vill du sova?",
"How it works": "Hur det fungerar",
"Hurry up and use them before they expire!": "Skynda dig och använd dem innan de går ut!",
- "Image gallery": "Bildgalleri",
+ "Image gallery": "{name} - Bildgalleri",
"In adults bed": "I vuxens säng",
"In crib": "I spjälsäng",
"In extra bed": "Egen sängplats",
@@ -155,6 +159,7 @@
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.",
"Join Scandic Friends": "Gå med i Scandic Friends",
"Join at no cost": "Gå med utan kostnad",
+ "Join or log in while booking for member pricing.": "Bli medlem eller logga in när du bokar för medlemspriser.",
"King bed": "King size-säng",
"Language": "Språk",
"Last name": "Efternamn",
@@ -199,12 +204,14 @@
"My pages menu": "Mina sidor meny",
"My payment cards": "Mina betalningskort",
"My wishes": "Mina önskningar",
+ "Name": "Namn",
"Nearby": "I närheten",
"Nearby companies": "Närliggande företag",
"New password": "Nytt lösenord",
"Next": "Nästa",
"Nights needed to level up": "Nätter som behövs för att gå upp i nivå",
"No": "Nej",
+ "No availability": "Ingen tillgänglighet",
"No breakfast": "Ingen frukost",
"No content published": "Inget innehåll publicerat",
"No matching location found": "Ingen matchande plats hittades",
@@ -222,6 +229,7 @@
"On your journey": "På din resa",
"Open": "Öppna",
"Open gift(s)": "Öppna {amount, plural, one {gåva} other {gåvor}}",
+ "Open image gallery": "Öppna bildgalleri",
"Open language menu": "Öppna språkmenyn",
"Open menu": "Öppna menyn",
"Open my pages menu": "Öppna mina sidor menyn",
@@ -252,6 +260,7 @@
"Practical information": "Praktisk information",
"Previous": "Föregående",
"Previous victories": "Tidigare segrar",
+ "Price": "Pris",
"Price details": "Prisdetaljer",
"Proceed to login": "Fortsätt till inloggning",
"Proceed to payment method": "Gå vidare till betalningsmetod",
@@ -283,8 +292,10 @@
"Search": "Sök",
"See all FAQ": "Se alla FAQ",
"See all photos": "Se alla foton",
+ "See details": "Se detaljer",
"See hotel details": "Se hotellinformation",
"See less FAQ": "See färre FAQ",
+ "See map": "Visa karta",
"See on map": "Se på karta",
"See room details": "Se rumsdetaljer",
"See rooms": "Se rum",
@@ -302,7 +313,6 @@
"Show all amenities": "Visa alla bekvämligheter",
"Show less": "Visa mindre",
"Show less rooms": "Visa färre rum",
- "Show map": "Visa karta",
"Show more": "Visa mer",
"Show more rooms": "Visa fler rum",
"Sign up bonus": "Välkomstbonus",
@@ -323,8 +333,12 @@
"Terms and conditions": "Allmänna villkor",
"Thank you": "Tack",
"Theatre": "Teater",
+ "There are no rooms available that match your request": "Det finns inga tillgängliga rum som matchar din förfrågan",
+ "There are no rooms available that match your request.": "Det finns inga rum tillgängliga som matchar din begäran.",
"There are no transactions to display": "Det finns inga transaktioner att visa",
"Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}",
+ "This room is not available": "Detta rum är inte tillgängligt",
+ "To get the member price {amount} {currency}, log in or join when completing the booking.": "För att få medlemsprisen {amount} {currency}, logga in eller bli medlem när du slutför bokningen.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.",
"Total Points": "Poäng totalt",
"Total incl VAT": "Totalt inkl moms",
@@ -333,6 +347,7 @@
"Transaction date": "Transaktionsdatum",
"Transactions": "Transaktioner",
"Transportations": "Transport",
+ "TripAdvisor rating": "TripAdvisor-betyg",
"Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)",
"Type of bed": "Sängtyp",
"Type of room": "Rumstyp",
@@ -402,7 +417,7 @@
"guaranteeing": "garanterar",
"guest": "gäst",
"guests": "gäster",
- "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}",
+ "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personer}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet",
"km to city center": "km till stadens centrum",
"lowercase letter": "liten bokstav",
diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts
index 100fb7518..781a19f38 100644
--- a/lib/api/endpoints.ts
+++ b/lib/api/endpoints.ts
@@ -151,8 +151,10 @@ export namespace endpoints {
export const invalidateSessions = `${base.path.profile}/${version}/${base.enitity.Profile}/invalidateSessions`
export const membership = `${base.path.profile}/${version}/${base.enitity.Profile}/membership`
export const profile = `${base.path.profile}/${version}/${base.enitity.Profile}`
- export const reward = `${base.path.profile}/${version}/${base.enitity.Profile}/reward`
export const subscriberId = `${base.path.profile}/${version}/${base.enitity.Profile}/SubscriberId`
+
+ // TODO: Remove once new endpoints are out in production.
+ export const reward = `${base.path.profile}/${version}/${base.enitity.Profile}/reward`
export const tierRewards = `${base.path.profile}/${version}/${base.enitity.Profile}/tierRewards`
export function deleteProfile(profileId: string) {
@@ -172,9 +174,10 @@ export namespace endpoints {
}
export namespace Reward {
- export const allTiers = `${base.path.profile}/${version}/${base.enitity.Reward}/AllTiers`
+ export const allTiers = `${base.path.profile}/${version}/${base.enitity.Reward}/allTiers`
export const reward = `${base.path.profile}/${version}/${base.enitity.Reward}`
- export const unwrap = `${base.path.profile}/${version}/${base.enitity.Reward}/Unwrap`
+ export const redeem = `${base.path.profile}/${version}/${base.enitity.Reward}/redeem`
+ export const unwrap = `${base.path.profile}/${version}/${base.enitity.Reward}/unwrap`
export function claim(rewardId: string) {
return `${base.path.profile}/${version}/${base.enitity.Reward}/Claim/${rewardId}`
diff --git a/lib/graphql/Query/Header.graphql b/lib/graphql/Query/Header.graphql
index cd893f135..892a36195 100644
--- a/lib/graphql/Query/Header.graphql
+++ b/lib/graphql/Query/Header.graphql
@@ -1,11 +1,15 @@
#import "../Fragments/System.graphql"
+#import "../Fragments/PageLink/AccountPageLink.graphql"
+#import "../Fragments/PageLink/CollectionPageLink.graphql"
#import "../Fragments/PageLink/ContentPageLink.graphql"
#import "../Fragments/PageLink/HotelPageLink.graphql"
#import "../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../Fragments/Blocks/Card.graphql"
#import "../Fragments/Blocks/Refs/Card.graphql"
+#import "../Fragments/AccountPage/Ref.graphql"
+#import "../Fragments/CollectionPage/Ref.graphql"
#import "../Fragments/ContentPage/Ref.graphql"
#import "../Fragments/HotelPage/Ref.graphql"
#import "../Fragments/LoyaltyPage/Ref.graphql"
@@ -14,14 +18,35 @@ query GetHeader($locale: String!) {
all_header(limit: 1, locale: $locale) {
items {
top_link {
- title
- linkConnection {
- edges {
- node {
- __typename
- ...ContentPageLink
- ...HotelPageLink
- ...LoyaltyPageLink
+ logged_in {
+ icon
+ title
+ linkConnection {
+ edges {
+ node {
+ __typename
+ ...AccountPageLink
+ ...CollectionPageLink
+ ...ContentPageLink
+ ...HotelPageLink
+ ...LoyaltyPageLink
+ }
+ }
+ }
+ }
+ logged_out {
+ icon
+ title
+ linkConnection {
+ edges {
+ node {
+ __typename
+ ...AccountPageLink
+ ...CollectionPageLink
+ ...ContentPageLink
+ ...HotelPageLink
+ ...LoyaltyPageLink
+ }
}
}
}
@@ -84,13 +109,31 @@ query GetHeaderRef($locale: String!) {
all_header(limit: 1, locale: $locale) {
items {
top_link {
- linkConnection {
- edges {
- node {
- __typename
- ...ContentPageRef
- ...HotelPageRef
- ...LoyaltyPageRef
+ logged_in {
+ linkConnection {
+ edges {
+ node {
+ __typename
+ ...AccountPageRef
+ ...CollectionPageRef
+ ...ContentPageRef
+ ...HotelPageRef
+ ...LoyaltyPageRef
+ }
+ }
+ }
+ }
+ logged_out {
+ linkConnection {
+ edges {
+ node {
+ __typename
+ ...AccountPageRef
+ ...CollectionPageRef
+ ...ContentPageRef
+ ...HotelPageRef
+ ...LoyaltyPageRef
+ }
}
}
}
diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts
index a90dc6907..407175703 100644
--- a/lib/trpc/memoizedRequests/index.ts
+++ b/lib/trpc/memoizedRequests/index.ts
@@ -8,7 +8,10 @@ import {
import { serverClient } from "../server"
-import type { BreackfastPackagesInput } from "@/types/requests/packages"
+import type {
+ BreackfastPackagesInput,
+ PackagesInput,
+} from "@/types/requests/packages"
export const getLocations = cache(async function getMemoizedLocations() {
return serverClient().hotel.locations.get()
@@ -144,6 +147,12 @@ export const getBreakfastPackages = cache(async function getMemoizedPackages(
return serverClient().hotel.packages.breakfast(input)
})
+export const getPackages = cache(async function getMemoizedPackages(
+ input: PackagesInput
+) {
+ return serverClient().hotel.packages.get(input)
+})
+
export const getBookingConfirmation = cache(
function getMemoizedBookingConfirmation(confirmationNumber: string) {
return serverClient().booking.confirmation({ confirmationNumber })
diff --git a/middleware.ts b/middleware.ts
index 1f1c36a0b..27b4eca97 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -8,6 +8,7 @@ import * as cmsContent from "@/middlewares/cmsContent"
import * as currentWebLogin from "@/middlewares/currentWebLogin"
import * as currentWebLoginEmail from "@/middlewares/currentWebLoginEmail"
import * as currentWebLogout from "@/middlewares/currentWebLogout"
+import * as dateFormat from "@/middlewares/dateFormat"
import * as handleAuth from "@/middlewares/handleAuth"
import * as myPages from "@/middlewares/myPages"
import { getDefaultRequestHeaders } from "@/middlewares/utils"
@@ -52,6 +53,7 @@ export const middleware: NextMiddleware = async (request, event) => {
webView,
bookingFlow,
cmsContent,
+ dateFormat,
]
try {
diff --git a/middlewares/dateFormat.ts b/middlewares/dateFormat.ts
new file mode 100644
index 000000000..e2e5c0a9e
--- /dev/null
+++ b/middlewares/dateFormat.ts
@@ -0,0 +1,39 @@
+import { NextMiddleware, NextResponse } from "next/server"
+
+import { MiddlewareMatcher } from "@/types/middleware"
+
+/*
+Middleware function to normalize date formats to support
+YYYY-MM-D and YYYY-MM-DD since the current web uses YYYY-MM-D
+in the URL as parameters (toDate and fromDate)
+*/
+export const middleware: NextMiddleware = (request) => {
+ const url = request.nextUrl.clone()
+ const { searchParams } = url
+
+ function normalizeDate(date: string): string {
+ const datePattern = /^\d{4}-\d{1,2}-\d{1,2}$/
+ if (datePattern.test(date)) {
+ const [year, month, day] = date.split("-").map(Number)
+ return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`
+ }
+ return date
+ }
+
+ if (searchParams.has("fromDate")) {
+ const fromDate = searchParams.get("fromDate")!
+ searchParams.set("fromDate", normalizeDate(fromDate))
+ }
+
+ if (searchParams.has("toDate")) {
+ const toDate = searchParams.get("toDate")!
+ searchParams.set("toDate", normalizeDate(toDate))
+ }
+
+ return NextResponse.rewrite(url)
+}
+
+export const matcher: MiddlewareMatcher = (request) => {
+ const { searchParams } = request.nextUrl
+ return searchParams.has("fromDate") || searchParams.has("toDate")
+}
diff --git a/package-lock.json b/package-lock.json
index 766d5cb95..a2d63c0f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,6 +53,7 @@
"server-only": "^0.0.1",
"sonner": "^1.5.0",
"superjson": "^2.2.1",
+ "usehooks-ts": "3.1.0",
"zod": "^3.22.4",
"zustand": "^4.5.2"
},
@@ -14703,7 +14704,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
- "dev": true,
"license": "MIT"
},
"node_modules/lodash.isempty": {
@@ -19454,6 +19454,20 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/usehooks-ts": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz",
+ "integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==",
+ "dependencies": {
+ "lodash.debounce": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=16.15.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index fa08f8495..4afcb9bf1 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"server-only": "^0.0.1",
"sonner": "^1.5.0",
"superjson": "^2.2.1",
+ "usehooks-ts": "3.1.0",
"zod": "^3.22.4",
"zustand": "^4.5.2"
},
diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts
index b5bc65a30..4c7d802ef 100644
--- a/server/routers/booking/input.ts
+++ b/server/routers/booking/input.ts
@@ -14,13 +14,11 @@ const roomsSchema = z.array(
)
.default([]),
rateCode: z.string(),
- roomTypeCode: z.string(),
+ roomTypeCode: z.coerce.string(),
guest: z.object({
- title: z.string(),
firstName: z.string(),
lastName: z.string(),
email: z.string().email(),
- phoneCountryCodePrefix: z.string().nullable(),
phoneNumber: z.string(),
countryCode: z.string(),
membershipNumber: z.string().optional(),
diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts
index 2edbd5bdd..dc3bad0fe 100644
--- a/server/routers/booking/mutation.ts
+++ b/server/routers/booking/mutation.ts
@@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api"
import * as api from "@/lib/api"
import { getVerifiedUser } from "@/server/routers/user/query"
-import { router, serviceProcedure } from "@/server/trpc"
+import { router, safeProtectedServiceProcedure } from "@/server/trpc"
import { getMembership } from "@/utils/user"
@@ -35,95 +35,93 @@ async function getMembershipNumber(
}
export const bookingMutationRouter = router({
- create: serviceProcedure.input(createBookingInput).mutation(async function ({
- ctx,
- input,
- }) {
- const { checkInDate, checkOutDate, hotelId } = input
+ create: safeProtectedServiceProcedure
+ .input(createBookingInput)
+ .mutation(async function ({ ctx, input }) {
+ const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
+ 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,
- }
-
- createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate })
-
- 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.bookings, {
- headers,
- body: input,
- })
-
- if (!apiResponse.ok) {
- const text = await apiResponse.text()
- createBookingFailCounter.add(1, {
- hotelId,
+ const loggingAttributes = {
+ membershipNumber: await getMembershipNumber(ctx.session),
checkInDate,
checkOutDate,
- error_type: "http_error",
- error: JSON.stringify({
- status: apiResponse.status,
- }),
- })
- console.error(
- "api.booking.create error",
+ hotelId,
+ }
+
+ createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate })
+
+ console.info(
+ "api.booking.create start",
JSON.stringify({
query: loggingAttributes,
- error: {
+ })
+ )
+ const headers = {
+ Authorization: `Bearer ${accessToken}`,
+ }
+
+ const apiResponse = await api.post(api.endpoints.v1.Booking.bookings, {
+ 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,
- statusText: apiResponse.statusText,
- error: text,
- },
+ }),
})
- )
- return null
- }
+ console.error(
+ "api.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, {
+ 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.create validation error",
+ JSON.stringify({
+ query: loggingAttributes,
+ error: verifiedData.error,
+ })
+ )
+ return null
+ }
+
+ createBookingSuccessCounter.add(1, {
hotelId,
checkInDate,
checkOutDate,
- error_type: "validation_error",
})
- console.error(
- "api.booking.create validation error",
+ console.info(
+ "api.booking.create success",
JSON.stringify({
query: loggingAttributes,
- error: verifiedData.error,
})
)
- return null
- }
-
- createBookingSuccessCounter.add(1, {
- hotelId,
- checkInDate,
- checkOutDate,
- })
-
- console.info(
- "api.booking.create success",
- JSON.stringify({
- query: loggingAttributes,
- })
- )
- return verifiedData.data
- }),
+ return verifiedData.data
+ }),
})
diff --git a/server/routers/contentstack/base/output.ts b/server/routers/contentstack/base/output.ts
index 6bc0bd9d4..d9f1f1790 100644
--- a/server/routers/contentstack/base/output.ts
+++ b/server/routers/contentstack/base/output.ts
@@ -14,6 +14,7 @@ import { removeMultipleSlashes } from "@/utils/url"
import { systemSchema } from "../schemas/system"
+import { IconName } from "@/types/components/icon"
import { AlertTypeEnum } from "@/types/enums/alert"
import type { Image } from "@/types/image"
@@ -514,6 +515,11 @@ const menuItemsRefsSchema = z.intersection(
})
)
+const topLinkRefsSchema = z.object({
+ logged_in: linkRefsSchema.nullable(),
+ logged_out: linkRefsSchema.nullable(),
+})
+
export const headerRefsSchema = z
.object({
all_header: z.object({
@@ -522,7 +528,7 @@ export const headerRefsSchema = z
z.object({
menu_items: z.array(menuItemsRefsSchema),
system: systemSchema,
- top_link: linkRefsSchema,
+ top_link: topLinkRefsSchema,
})
)
.max(1),
@@ -636,6 +642,32 @@ export const menuItemSchema = z
}
})
+const topLinkItemSchema = z.intersection(
+ linkAndTitleSchema,
+ z.object({
+ icon: z
+ .enum(["loyalty", "info", "offer"])
+ .nullable()
+ .transform((icon) => {
+ switch (icon) {
+ case "loyalty":
+ return IconName.Gift
+ case "info":
+ return IconName.InfoCircle
+ case "offer":
+ return IconName.PriceTag
+ default:
+ return null
+ }
+ }),
+ })
+)
+
+export const topLinkSchema = z.object({
+ logged_in: topLinkItemSchema.nullable(),
+ logged_out: topLinkItemSchema.nullable(),
+})
+
export const headerSchema = z
.object({
all_header: z.object({
@@ -643,7 +675,7 @@ export const headerSchema = z
.array(
z.object({
menu_items: z.array(menuItemSchema),
- top_link: linkAndTitleSchema,
+ top_link: topLinkSchema,
})
)
.max(1),
diff --git a/server/routers/contentstack/base/utils.ts b/server/routers/contentstack/base/utils.ts
index b25fc945c..27ec2304f 100644
--- a/server/routers/contentstack/base/utils.ts
+++ b/server/routers/contentstack/base/utils.ts
@@ -14,8 +14,13 @@ import type { ContactConfig } from "./output"
export function getConnections({ header }: HeaderRefs) {
const connections: System["system"][] = [header.system]
- if (header.top_link?.link) {
- connections.push(header.top_link.link)
+ if (header.top_link) {
+ if (header.top_link.logged_in?.link) {
+ connections.push(header.top_link.logged_in.link)
+ }
+ if (header.top_link.logged_out?.link) {
+ connections.push(header.top_link.logged_out.link)
+ }
}
if (header.menu_items.length) {
diff --git a/server/routers/contentstack/reward/output.ts b/server/routers/contentstack/reward/output.ts
index 5bd2c7d75..8e954e656 100644
--- a/server/routers/contentstack/reward/output.ts
+++ b/server/routers/contentstack/reward/output.ts
@@ -122,3 +122,61 @@ export type SurpriseReward = z.output
export type CmsRewardsResponse = z.input
export type Reward = z.output[0]
+
+// New endpoint related types and schemas.
+
+const BenefitReward = z.object({
+ title: z.string().optional(),
+ id: z.string().optional(),
+ status: z.string().optional(),
+ rewardId: z.string().optional(),
+ rewardType: z.string().optional(),
+ rewardTierLevel: z.string().optional(),
+})
+
+const CouponState = z.enum(["claimed", "redeemed", "viewed"])
+const CouponData = z.object({
+ couponCode: z.string().optional(),
+ unwrapped: z.boolean().default(false),
+ state: CouponState,
+ expiresAt: z.string().datetime({ offset: true }).optional(),
+})
+
+const CouponReward = z.object({
+ title: z.string().optional(),
+ id: z.string().optional(),
+ rewardId: z.string().optional(),
+ rewardType: z.string().optional(),
+ status: z.string().optional(),
+ coupon: z.array(CouponData).optional(),
+})
+
+/**
+ * Schema for the new /profile/v1/Reward endpoint.
+ *
+ * TODO: Once we fully migrate to the new endpoint:
+ * 1. Remove the data transform and use the categorized structure directly.
+ * 2. Simplify surprise filtering in the query.
+ */
+export const validateCategorizedRewardsSchema = z
+ .object({
+ benefits: z.array(BenefitReward),
+ coupons: z.array(CouponReward),
+ })
+ .transform((data) => [
+ ...data.benefits.map((benefit) => ({
+ ...benefit,
+ type: "custom" as const, // Added for legacy compatibility.
+ })),
+ ...data.coupons.map((coupon) => ({
+ ...coupon,
+ type: "coupon" as const, // Added for legacy compatibility.
+ })),
+ ])
+
+export const validateApiAllTiersSchema = z.record(
+ z.nativeEnum(TierKey).transform((data) => {
+ return TierKey[data as unknown as Key]
+ }),
+ z.array(BenefitReward)
+)
diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts
index b4e0e54b2..b1f5ccb8c 100644
--- a/server/routers/contentstack/reward/query.ts
+++ b/server/routers/contentstack/reward/query.ts
@@ -1,10 +1,5 @@
-import { metrics } from "@opentelemetry/api"
-import { unstable_cache } from "next/cache"
-
-import { Lang } from "@/constants/languages"
+import { env } from "@/env/server"
import * as api from "@/lib/api"
-import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql"
-import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import {
contentStackBaseWithProtectedProcedure,
@@ -12,8 +7,6 @@ import {
router,
} from "@/server/trpc"
-import { generateLoyaltyConfigTag } from "@/utils/generateTag"
-
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
import {
rewardsAllInput,
@@ -22,321 +15,40 @@ import {
rewardsUpdateInput,
} from "./input"
import {
- CmsRewardsResponse,
Reward,
SurpriseReward,
validateApiRewardSchema,
- validateApiTierRewardsSchema,
- validateCmsRewardsSchema,
+ validateCategorizedRewardsSchema,
} from "./output"
+import {
+ getAllCachedApiRewards,
+ getAllRewardCounter,
+ getAllRewardFailCounter,
+ getAllRewardSuccessCounter,
+ getByLevelRewardCounter,
+ getByLevelRewardFailCounter,
+ getByLevelRewardSuccessCounter,
+ getCachedAllTierRewards,
+ getCmsRewards,
+ getCurrentRewardCounter,
+ getCurrentRewardFailCounter,
+ getCurrentRewardSuccessCounter,
+ getUniqueRewardIds,
+} from "./utils"
import { Surprise } from "@/types/components/blocks/surprises"
-const meter = metrics.getMeter("trpc.reward")
-// OpenTelemetry metrics: Reward
-
-const getCurrentRewardCounter = meter.createCounter(
- "trpc.contentstack.reward.current"
-)
-const getCurrentRewardSuccessCounter = meter.createCounter(
- "trpc.contentstack.reward.current-success"
-)
-
-const getCurrentRewardFailCounter = meter.createCounter(
- "trpc.contentstack.reward.current-fail"
-)
-
-const getByLevelRewardCounter = meter.createCounter(
- "trpc.contentstack.reward.byLevel"
-)
-const getByLevelRewardSuccessCounter = meter.createCounter(
- "trpc.contentstack.reward.byLevel-success"
-)
-
-const getByLevelRewardFailCounter = meter.createCounter(
- "trpc.contentstack.reward.byLevel-fail"
-)
-
-const getAllRewardCounter = meter.createCounter("trpc.contentstack.reward.all")
-
-const getAllRewardSuccessCounter = meter.createCounter(
- "trpc.contentstack.reward.all-success"
-)
-const getAllRewardFailCounter = meter.createCounter(
- "trpc.contentstack.reward.all-fail"
-)
-
const ONE_HOUR = 60 * 60
-function getUniqueRewardIds(rewardIds: string[]) {
- const uniqueRewardIds = new Set(rewardIds)
- return Array.from(uniqueRewardIds)
-}
-
-const getAllCachedApiRewards = unstable_cache(
- async function (token) {
- const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- })
-
- if (!apiResponse.ok) {
- const text = await apiResponse.text()
- getCurrentRewardFailCounter.add(1, {
- error_type: "http_error",
- error: JSON.stringify({
- status: apiResponse.status,
- statusText: apiResponse.statusText,
- text,
- }),
- })
- console.error(
- "api.rewards.tierRewards error ",
- JSON.stringify({
- error: {
- status: apiResponse.status,
- statusText: apiResponse.statusText,
- text,
- },
- })
- )
-
- throw apiResponse
- }
-
- const data = await apiResponse.json()
- const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data)
-
- if (!validatedApiTierRewards.success) {
- getAllRewardFailCounter.add(1, {
- error_type: "validation_error",
- error: JSON.stringify(validatedApiTierRewards.error),
- })
- console.error(validatedApiTierRewards.error)
- console.error(
- "api.rewards validation error",
- JSON.stringify({
- error: validatedApiTierRewards.error,
- })
- )
- throw validatedApiTierRewards.error
- }
-
- return validatedApiTierRewards.data
- },
- ["getAllApiRewards"],
- { revalidate: ONE_HOUR }
-)
-
-async function getCmsRewards(locale: Lang, rewardIds: string[]) {
- const tags = rewardIds.map((id) =>
- generateLoyaltyConfigTag(locale, "reward", id)
- )
- const cmsRewardsResponse = await request(
- GetRewards,
- {
- locale: locale,
- rewardIds,
- },
- { next: { tags }, cache: "force-cache" }
- )
-
- if (!cmsRewardsResponse.data) {
- getAllRewardFailCounter.add(1, {
- lang: locale,
- error_type: "validation_error",
- error: JSON.stringify(cmsRewardsResponse.data),
- })
- const notFoundError = notFound(cmsRewardsResponse)
- console.error(
- "contentstack.rewards not found error",
- JSON.stringify({
- query: {
- locale,
- rewardIds,
- },
- error: { code: notFoundError.code },
- })
- )
- throw notFoundError
- }
-
- const validatedCmsRewards =
- validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
-
- if (!validatedCmsRewards.success) {
- getAllRewardFailCounter.add(1, {
- locale,
- rewardIds,
- error_type: "validation_error",
- error: JSON.stringify(validatedCmsRewards.error),
- })
- console.error(validatedCmsRewards.error)
- console.error(
- "contentstack.rewards validation error",
- JSON.stringify({
- query: { locale, rewardIds },
- error: validatedCmsRewards.error,
- })
- )
- return null
- }
-
- return validatedCmsRewards.data
-}
-
export const rewardQueryRouter = router({
- current: contentStackBaseWithProtectedProcedure
- .input(rewardsCurrentInput)
- .query(async function ({ input, ctx }) {
- getCurrentRewardCounter.add(1)
-
- const { limit, cursor } = input
-
- const apiResponse = await api.get(api.endpoints.v1.Profile.reward, {
- cache: undefined, // override defaultOptions
- headers: {
- Authorization: `Bearer ${ctx.session.token.access_token}`,
- },
- next: { revalidate: 60 * 60 },
- })
-
- if (!apiResponse.ok) {
- const text = await apiResponse.text()
- getCurrentRewardFailCounter.add(1, {
- error_type: "http_error",
- error: JSON.stringify({
- status: apiResponse.status,
- statusText: apiResponse.statusText,
- text,
- }),
- })
- console.error(
- "api.reward error ",
- JSON.stringify({
- error: {
- status: apiResponse.status,
- statusText: apiResponse.statusText,
- text,
- },
- })
- )
- return null
- }
-
- const data = await apiResponse.json()
-
- const validatedApiRewards = validateApiRewardSchema.safeParse(data)
-
- if (!validatedApiRewards.success) {
- getCurrentRewardFailCounter.add(1, {
- locale: ctx.lang,
- error_type: "validation_error",
- error: JSON.stringify(validatedApiRewards.error),
- })
- console.error(validatedApiRewards.error)
- console.error(
- "contentstack.rewards validation error",
- JSON.stringify({
- query: { locale: ctx.lang },
- error: validatedApiRewards.error,
- })
- )
- return null
- }
-
- const rewardIds = validatedApiRewards.data
- .map((reward) => reward?.rewardId)
- .filter((rewardId): rewardId is string => !!rewardId)
- .sort()
-
- const slicedData = rewardIds.slice(cursor, limit + cursor)
-
- const cmsRewards = await getCmsRewards(ctx.lang, slicedData)
-
- if (!cmsRewards) {
- return null
- }
-
- const nextCursor =
- limit + cursor < rewardIds.length ? limit + cursor : undefined
-
- const surprisesIds = validatedApiRewards.data
- .filter(
- ({ type, rewardType }) =>
- type === "coupon" && rewardType === "Surprise"
- )
- .map(({ rewardId }) => rewardId)
-
- const rewards = cmsRewards.filter(
- (reward) => !surprisesIds.includes(reward.reward_id)
- )
-
- getCurrentRewardSuccessCounter.add(1)
-
- return {
- rewards,
- nextCursor,
- }
- }),
- byLevel: contentStackBaseWithServiceProcedure
- .input(rewardsByLevelInput)
- .query(async function ({ input, ctx }) {
- getByLevelRewardCounter.add(1)
- const { level_id } = input
-
- const allUpcomingApiRewards = await getAllCachedApiRewards(
- ctx.serviceToken
- )
-
- if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) {
- getByLevelRewardFailCounter.add(1)
-
- return null
- }
-
- let apiRewards = allUpcomingApiRewards[level_id]!
-
- if (input.unique) {
- apiRewards = allUpcomingApiRewards[level_id]!.filter(
- (reward) => reward?.rewardTierLevel === level_id
- )
- }
-
- const rewardIds = apiRewards
- .map((reward) => reward?.rewardId)
- .filter((id): id is string => Boolean(id))
-
- const contentStackRewards = await getCmsRewards(ctx.lang, rewardIds)
- if (!contentStackRewards) {
- return null
- }
-
- const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id)
-
- const levelsWithRewards = apiRewards
- .map((reward) => {
- const contentStackReward = contentStackRewards.find((r) => {
- return r.reward_id === reward?.rewardId
- })
-
- if (contentStackReward) {
- return contentStackReward
- } else {
- console.error("No contentStackReward found", reward?.rewardId)
- }
- })
- .filter((reward): reward is Reward => Boolean(reward))
-
- getByLevelRewardSuccessCounter.add(1)
- return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
- }),
all: contentStackBaseWithServiceProcedure
.input(rewardsAllInput)
.query(async function ({ input, ctx }) {
getAllRewardCounter.add(1)
- const allApiRewards = await getAllCachedApiRewards(ctx.serviceToken)
+
+ const allApiRewards = env.USE_NEW_REWARDS_ENDPOINT
+ ? await getCachedAllTierRewards(ctx.serviceToken)
+ : await getAllCachedApiRewards(ctx.serviceToken)
if (!allApiRewards) {
return []
@@ -390,15 +102,171 @@ export const rewardQueryRouter = router({
getAllRewardSuccessCounter.add(1)
return levelsWithRewards
}),
+ byLevel: contentStackBaseWithServiceProcedure
+ .input(rewardsByLevelInput)
+ .query(async function ({ input, ctx }) {
+ getByLevelRewardCounter.add(1)
+ const { level_id } = input
+
+ const allUpcomingApiRewards = env.USE_NEW_REWARDS_ENDPOINT
+ ? await getCachedAllTierRewards(ctx.serviceToken)
+ : await getAllCachedApiRewards(ctx.serviceToken)
+
+ if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) {
+ getByLevelRewardFailCounter.add(1)
+
+ return null
+ }
+
+ let apiRewards = allUpcomingApiRewards[level_id]!
+
+ if (input.unique) {
+ apiRewards = allUpcomingApiRewards[level_id]!.filter(
+ (reward) => reward?.rewardTierLevel === level_id
+ )
+ }
+
+ const rewardIds = apiRewards
+ .map((reward) => reward?.rewardId)
+ .filter((id): id is string => Boolean(id))
+
+ const contentStackRewards = await getCmsRewards(ctx.lang, rewardIds)
+ if (!contentStackRewards) {
+ return null
+ }
+
+ const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id)
+
+ const levelsWithRewards = apiRewards
+ .map((reward) => {
+ const contentStackReward = contentStackRewards.find((r) => {
+ return r.reward_id === reward?.rewardId
+ })
+
+ if (contentStackReward) {
+ return contentStackReward
+ } else {
+ console.info("No contentStackReward found", reward?.rewardId)
+ }
+ })
+ .filter((reward): reward is Reward => Boolean(reward))
+
+ getByLevelRewardSuccessCounter.add(1)
+ return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
+ }),
+ current: contentStackBaseWithProtectedProcedure
+ .input(rewardsCurrentInput)
+ .query(async function ({ input, ctx }) {
+ getCurrentRewardCounter.add(1)
+
+ const { limit, cursor } = input
+
+ const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT
+ const endpoint = isNewEndpoint
+ ? api.endpoints.v1.Profile.Reward.reward
+ : api.endpoints.v1.Profile.reward
+
+ const apiResponse = await api.get(endpoint, {
+ cache: undefined, // override defaultOptions
+ headers: {
+ Authorization: `Bearer ${ctx.session.token.access_token}`,
+ },
+ next: { revalidate: ONE_HOUR },
+ })
+
+ if (!apiResponse.ok) {
+ const text = await apiResponse.text()
+ getCurrentRewardFailCounter.add(1, {
+ error_type: "http_error",
+ error: JSON.stringify({
+ status: apiResponse.status,
+ statusText: apiResponse.statusText,
+ text,
+ }),
+ })
+ console.error(
+ "api.reward error ",
+ JSON.stringify({
+ error: {
+ status: apiResponse.status,
+ statusText: apiResponse.statusText,
+ text,
+ },
+ })
+ )
+ return null
+ }
+
+ const data = await apiResponse.json()
+ const validatedApiRewards = isNewEndpoint
+ ? validateCategorizedRewardsSchema.safeParse(data)
+ : validateApiRewardSchema.safeParse(data)
+
+ if (!validatedApiRewards.success) {
+ getCurrentRewardFailCounter.add(1, {
+ locale: ctx.lang,
+ error_type: "validation_error",
+ error: JSON.stringify(validatedApiRewards.error),
+ })
+ console.error(validatedApiRewards.error)
+ console.error(
+ "contentstack.rewards validation error",
+ JSON.stringify({
+ query: { locale: ctx.lang },
+ error: validatedApiRewards.error,
+ })
+ )
+ return null
+ }
+
+ const rewardIds = validatedApiRewards.data
+ .map((reward) => reward?.rewardId)
+ .filter((rewardId): rewardId is string => !!rewardId)
+ .sort()
+
+ const slicedData = rewardIds.slice(cursor, limit + cursor)
+
+ const cmsRewards = await getCmsRewards(ctx.lang, slicedData)
+
+ if (!cmsRewards) {
+ return null
+ }
+
+ const nextCursor =
+ limit + cursor < rewardIds.length ? limit + cursor : undefined
+
+ const surprisesIds = validatedApiRewards.data
+ .filter(
+ ({ type, rewardType }) =>
+ type === "coupon" && rewardType === "Surprise"
+ )
+ .map(({ rewardId }) => rewardId)
+
+ const rewards = cmsRewards.filter(
+ (reward) => !surprisesIds.includes(reward.reward_id)
+ )
+
+ getCurrentRewardSuccessCounter.add(1)
+
+ return {
+ rewards,
+ nextCursor,
+ }
+ }),
surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => {
getCurrentRewardCounter.add(1)
- const apiResponse = await api.get(api.endpoints.v1.Profile.reward, {
- cache: undefined, // override defaultOptions
+ const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT
+ const endpoint = isNewEndpoint
+ ? api.endpoints.v1.Profile.Reward.reward
+ : api.endpoints.v1.Profile.reward
+
+ const apiResponse = await api.get(endpoint, {
+ cache: undefined,
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
- next: { revalidate: 60 * 60 },
+ next: { revalidate: ONE_HOUR },
})
if (!apiResponse.ok) {
@@ -425,8 +293,9 @@ export const rewardQueryRouter = router({
}
const data = await apiResponse.json()
-
- const validatedApiRewards = validateApiRewardSchema.safeParse(data)
+ const validatedApiRewards = isNewEndpoint
+ ? validateCategorizedRewardsSchema.safeParse(data)
+ : validateApiRewardSchema.safeParse(data)
if (!validatedApiRewards.success) {
getCurrentRewardFailCounter.add(1, {
diff --git a/server/routers/contentstack/reward/utils.ts b/server/routers/contentstack/reward/utils.ts
new file mode 100644
index 000000000..d06f2666b
--- /dev/null
+++ b/server/routers/contentstack/reward/utils.ts
@@ -0,0 +1,232 @@
+import { metrics } from "@opentelemetry/api"
+import { unstable_cache } from "next/cache"
+
+import { Lang } from "@/constants/languages"
+import * as api from "@/lib/api"
+import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql"
+import { request } from "@/lib/graphql/request"
+import { notFound } from "@/server/errors/trpc"
+
+import { generateLoyaltyConfigTag } from "@/utils/generateTag"
+
+import {
+ CmsRewardsResponse,
+ validateApiAllTiersSchema,
+ validateApiTierRewardsSchema,
+ validateCmsRewardsSchema,
+} from "./output"
+
+const meter = metrics.getMeter("trpc.reward")
+export const getAllRewardCounter = meter.createCounter(
+ "trpc.contentstack.reward.all"
+)
+export const getAllRewardFailCounter = meter.createCounter(
+ "trpc.contentstack.reward.all-fail"
+)
+export const getAllRewardSuccessCounter = meter.createCounter(
+ "trpc.contentstack.reward.all-success"
+)
+export const getCurrentRewardCounter = meter.createCounter(
+ "trpc.contentstack.reward.current"
+)
+export const getCurrentRewardFailCounter = meter.createCounter(
+ "trpc.contentstack.reward.current-fail"
+)
+export const getCurrentRewardSuccessCounter = meter.createCounter(
+ "trpc.contentstack.reward.current-success"
+)
+export const getByLevelRewardCounter = meter.createCounter(
+ "trpc.contentstack.reward.byLevel"
+)
+export const getByLevelRewardFailCounter = meter.createCounter(
+ "trpc.contentstack.reward.byLevel-fail"
+)
+export const getByLevelRewardSuccessCounter = meter.createCounter(
+ "trpc.contentstack.reward.byLevel-success"
+)
+
+const ONE_HOUR = 60 * 60
+
+export function getUniqueRewardIds(rewardIds: string[]) {
+ const uniqueRewardIds = new Set(rewardIds)
+ return Array.from(uniqueRewardIds)
+}
+
+/**
+ * Uses the legacy profile/v1/Profile/tierRewards endpoint.
+ * TODO: Delete when the new endpoint is out in production.
+ */
+export const getAllCachedApiRewards = unstable_cache(
+ async function (token) {
+ const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ })
+
+ if (!apiResponse.ok) {
+ const text = await apiResponse.text()
+ getAllRewardFailCounter.add(1, {
+ error_type: "http_error",
+ error: JSON.stringify({
+ status: apiResponse.status,
+ statusText: apiResponse.statusText,
+ text,
+ }),
+ })
+ console.error(
+ "api.rewards.tierRewards error ",
+ JSON.stringify({
+ error: {
+ status: apiResponse.status,
+ statusText: apiResponse.statusText,
+ text,
+ },
+ })
+ )
+
+ throw apiResponse
+ }
+
+ const data = await apiResponse.json()
+ const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data)
+
+ if (!validatedApiTierRewards.success) {
+ getAllRewardFailCounter.add(1, {
+ error_type: "validation_error",
+ error: JSON.stringify(validatedApiTierRewards.error),
+ })
+ console.error(validatedApiTierRewards.error)
+ console.error(
+ "api.rewards validation error",
+ JSON.stringify({
+ error: validatedApiTierRewards.error,
+ })
+ )
+ throw validatedApiTierRewards.error
+ }
+
+ return validatedApiTierRewards.data
+ },
+ ["getAllApiRewards"],
+ { revalidate: ONE_HOUR }
+)
+
+/**
+ * Cached for 1 hour.
+ */
+export const getCachedAllTierRewards = unstable_cache(
+ async function (token) {
+ const apiResponse = await api.get(
+ api.endpoints.v1.Profile.Reward.allTiers,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ )
+
+ if (!apiResponse.ok) {
+ const text = await apiResponse.text()
+ getAllRewardFailCounter.add(1, {
+ error_type: "http_error",
+ error: JSON.stringify({
+ status: apiResponse.status,
+ statusText: apiResponse.statusText,
+ text,
+ }),
+ })
+ console.error(
+ "api.rewards.allTiers error ",
+ JSON.stringify({
+ error: {
+ status: apiResponse.status,
+ statusText: apiResponse.statusText,
+ text,
+ },
+ })
+ )
+
+ throw apiResponse
+ }
+
+ const data = await apiResponse.json()
+ const validatedApiAllTierRewards = validateApiAllTiersSchema.safeParse(data)
+
+ if (!validatedApiAllTierRewards.success) {
+ getAllRewardFailCounter.add(1, {
+ error_type: "validation_error",
+ error: JSON.stringify(validatedApiAllTierRewards.error),
+ })
+ console.error(validatedApiAllTierRewards.error)
+ console.error(
+ "api.rewards validation error",
+ JSON.stringify({
+ error: validatedApiAllTierRewards.error,
+ })
+ )
+ throw validatedApiAllTierRewards.error
+ }
+
+ return validatedApiAllTierRewards.data
+ },
+ ["getApiAllTierRewards"],
+ { revalidate: ONE_HOUR }
+)
+
+export async function getCmsRewards(locale: Lang, rewardIds: string[]) {
+ const tags = rewardIds.map((id) =>
+ generateLoyaltyConfigTag(locale, "reward", id)
+ )
+ const cmsRewardsResponse = await request(
+ GetRewards,
+ {
+ locale: locale,
+ rewardIds,
+ },
+ { next: { tags }, cache: "force-cache" }
+ )
+
+ if (!cmsRewardsResponse.data) {
+ getAllRewardFailCounter.add(1, {
+ lang: locale,
+ error_type: "validation_error",
+ error: JSON.stringify(cmsRewardsResponse.data),
+ })
+ const notFoundError = notFound(cmsRewardsResponse)
+ console.error(
+ "contentstack.rewards not found error",
+ JSON.stringify({
+ query: {
+ locale,
+ rewardIds,
+ },
+ error: { code: notFoundError.code },
+ })
+ )
+ throw notFoundError
+ }
+
+ const validatedCmsRewards =
+ validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
+
+ if (!validatedCmsRewards.success) {
+ getAllRewardFailCounter.add(1, {
+ locale,
+ rewardIds,
+ error_type: "validation_error",
+ error: JSON.stringify(validatedCmsRewards.error),
+ })
+ console.error(validatedCmsRewards.error)
+ console.error(
+ "contentstack.rewards validation error",
+ JSON.stringify({
+ query: { locale, rewardIds },
+ error: validatedCmsRewards.error,
+ })
+ )
+ return null
+ }
+
+ return validatedCmsRewards.data
+}
diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts
index 6b52918f7..04bb16b17 100644
--- a/server/routers/hotels/input.ts
+++ b/server/routers/hotels/input.ts
@@ -1,5 +1,7 @@
import { z } from "zod"
+import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
+
export const getHotelsAvailabilityInputSchema = z.object({
cityId: z.string(),
roomStayStartDate: z.string(),
@@ -34,6 +36,7 @@ export const getSelectedRoomAvailabilityInputSchema = z.object({
attachedProfileId: z.string().optional().default(""),
rateCode: z.string(),
roomTypeCode: z.string(),
+ packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(),
})
export type GetSelectedRoomAvailabilityInput = z.input<
@@ -68,3 +71,12 @@ export const getBreakfastPackageInputSchema = z.object({
.min(1, { message: "toDate is required" })
.pipe(z.coerce.date()),
})
+
+export const getRoomPackagesInputSchema = z.object({
+ hotelId: z.string(),
+ startDate: z.string(),
+ endDate: z.string(),
+ adults: z.number(),
+ children: z.number().optional().default(0),
+ packageCodes: z.array(z.string()).optional().default([]),
+})
diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts
index f0cc7c134..9bc81225e 100644
--- a/server/routers/hotels/output.ts
+++ b/server/routers/hotels/output.ts
@@ -449,6 +449,7 @@ export const getHotelDataSchema = z.object({
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
),
gallery: gallerySchema.optional(),
+ galleryImages: z.array(imageSchema).optional(),
healthAndWellness: facilitySchema.optional(),
healthFacilities: z.array(healthFacilitySchema),
hotelContent: hotelContentSchema,
@@ -540,8 +541,8 @@ export type HotelsAvailabilityPrices =
HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"]
export const priceSchema = z.object({
- pricePerNight: z.string(),
- pricePerStay: z.string(),
+ pricePerNight: z.coerce.number(),
+ pricePerStay: z.coerce.number(),
currency: z.string(),
})
@@ -549,20 +550,19 @@ export const productTypePriceSchema = z.object({
rateCode: z.string(),
rateType: z.string().optional(),
localPrice: priceSchema,
- requestedPrice: priceSchema.optional(),
+ requestedPrice: priceSchema,
})
const productSchema = z.object({
productType: z.object({
- public: productTypePriceSchema.optional(),
+ public: productTypePriceSchema,
member: productTypePriceSchema.optional(),
}),
})
const roomConfigurationSchema = z.object({
status: z.string(),
- // TODO: Remove the optional when the API change has been deployed
- roomTypeCode: z.string().optional(),
+ roomTypeCode: z.string(),
roomType: z.string(),
roomsLeft: z.number(),
features: z.array(
@@ -825,23 +825,17 @@ export const apiLocationsSchema = z.object({
),
})
-const breakfastPackagePriceSchema = z
- .object({
- currency: z.nativeEnum(CurrencyEnum),
- price: z.string(),
- totalPrice: z.string(),
- })
- .default({
- currency: CurrencyEnum.SEK,
- price: "0",
- totalPrice: "0",
- }) // TODO: Remove optional and default when the API change has been deployed
+export const packagePriceSchema = z.object({
+ currency: z.nativeEnum(CurrencyEnum),
+ price: z.string(),
+ totalPrice: z.string(),
+})
export const breakfastPackageSchema = z.object({
code: z.string(),
description: z.string(),
- localPrice: breakfastPackagePriceSchema,
- requestedPrice: breakfastPackagePriceSchema,
+ localPrice: packagePriceSchema,
+ requestedPrice: packagePriceSchema,
packageType: z.literal(PackageTypeEnum.BreakfastAdult),
})
@@ -858,3 +852,40 @@ export const breakfastPackagesSchema = z
.transform(({ data }) =>
data.attributes.packages.filter((pkg) => pkg.code.match(/^(BRF\d+)$/gm))
)
+
+export const packagesSchema = z.object({
+ code: z.nativeEnum(RoomPackageCodeEnum),
+ itemCode: z.string().optional(),
+ description: z.string(),
+ localPrice: packagePriceSchema,
+ requestedPrice: packagePriceSchema,
+ inventories: z.array(
+ z.object({
+ date: z.string(),
+ total: z.number(),
+ available: z.number(),
+ })
+ ),
+})
+
+export const getRoomPackagesSchema = z
+ .object({
+ data: z.object({
+ attributes: z.object({
+ hotelId: z.number(),
+ packages: z.array(packagesSchema).optional().default([]),
+ }),
+ relationships: z
+ .object({
+ links: z.array(
+ z.object({
+ url: z.string(),
+ type: z.string(),
+ })
+ ),
+ })
+ .optional(),
+ type: z.string(),
+ }),
+ })
+ .transform((data) => data.data.attributes.packages)
diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts
index 882eefb6b..11f235ab6 100644
--- a/server/routers/hotels/query.ts
+++ b/server/routers/hotels/query.ts
@@ -28,15 +28,12 @@ import {
validateHotelPageRefs,
} from "../contentstack/hotelPage/utils"
import { getVerifiedUser, parsedUser } from "../user/query"
-import {
- getRoomPackagesInputSchema,
- getRoomPackagesSchema,
-} from "./schemas/packages"
import {
getBreakfastPackageInputSchema,
getHotelDataInputSchema,
getHotelsAvailabilityInputSchema,
getRatesInputSchema,
+ getRoomPackagesInputSchema,
getRoomsAvailabilityInputSchema,
getSelectedRoomAvailabilityInputSchema,
type HotelDataInput,
@@ -46,6 +43,7 @@ import {
getHotelDataSchema,
getHotelsAvailabilitySchema,
getRatesSchema,
+ getRoomPackagesSchema,
getRoomsAvailabilitySchema,
} from "./output"
import tempRatesData from "./tempRatesData.json"
@@ -60,6 +58,7 @@ import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
+import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type { Facility } from "@/types/hotel"
import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage"
@@ -257,13 +256,22 @@ export const getHotelData = cache(
query: { hotelId, params: params },
})
)
+ const hotelData = validateHotelData.data
if (isCardOnlyPayment) {
- validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions =
+ hotelData.data.attributes.merchantInformationData.alternatePaymentOptions =
[]
}
+ if (hotelData.data.attributes.gallery) {
+ const smallerImages = hotelData.data.attributes.gallery.smallerImages
+ const hotelGalleryImages =
+ hotelData.data.attributes.hotelType === HotelTypeEnum.Signature
+ ? smallerImages.slice(0, 10)
+ : smallerImages.slice(0, 6)
+ hotelData.data.attributes.galleryImages = hotelGalleryImages
+ }
- return validateHotelData.data
+ return hotelData
}
)
@@ -616,6 +624,7 @@ export const hotelQueryRouter = router({
attachedProfileId,
rateCode,
roomTypeCode,
+ packageCodes,
} = input
const params: Record = {
@@ -715,17 +724,27 @@ export const hotelQueryRouter = router({
ctx.serviceToken
)
- const selectedRoom = validateAvailabilityData.data.roomConfigurations
- .filter((room) => room.status === "Available")
- .find((room) => room.roomTypeCode === roomTypeCode)
+ const availableRooms =
+ validateAvailabilityData.data.roomConfigurations.filter((room) => {
+ if (packageCodes) {
+ return (
+ room.status === "Available" &&
+ room.features.some(
+ (feature) =>
+ packageCodes.includes(feature.code) && feature.inventory > 0
+ )
+ )
+ }
+ return room.status === "Available"
+ })
- const availableRoomsInCategory =
- validateAvailabilityData.data.roomConfigurations.filter(
- (room) =>
- room.status === "Available" &&
- room.roomType === selectedRoom?.roomType
- )
+ const selectedRoom = availableRooms.find(
+ (room) => room.roomTypeCode === roomTypeCode
+ )
+ const availableRoomsInCategory = availableRooms.filter(
+ (room) => room.roomType === selectedRoom?.roomType
+ )
if (!selectedRoom) {
console.error("No matching room found")
return null
@@ -733,9 +752,15 @@ export const hotelQueryRouter = router({
const rateTypes = selectedRoom.products.find(
(rate) =>
- rate.productType.public?.rateCode === rateCode ||
+ rate.productType.public.rateCode === rateCode ||
rate.productType.member?.rateCode === rateCode
- )?.productType
+ )
+
+ if (!rateTypes) {
+ console.error("No matching rate found")
+ return null
+ }
+ const rates = rateTypes.productType
const mustBeGuaranteed =
validateAvailabilityData.data.rateDefinitions.filter(
@@ -785,8 +810,8 @@ export const hotelQueryRouter = router({
selectedRoom,
mustBeGuaranteed,
cancellationText,
- memberRate: rateTypes?.member,
- publicRate: rateTypes?.public,
+ memberRate: rates?.member,
+ publicRate: rates.public,
bedTypes,
}
}),
@@ -976,8 +1001,8 @@ export const hotelQueryRouter = router({
const apiLang = toApiLang(lang)
const params = {
Adults: input.adults,
- EndDate: dt(input.toDate).format("YYYY-MM-D"),
- StartDate: dt(input.fromDate).format("YYYY-MM-D"),
+ EndDate: dt(input.toDate).format("YYYY-MM-DD"),
+ StartDate: dt(input.fromDate).format("YYYY-MM-DD"),
language: apiLang,
}
diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts
deleted file mode 100644
index 738da80ce..000000000
--- a/server/routers/hotels/schemas/packages.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { z } from "zod"
-
-import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
-import { CurrencyEnum } from "@/types/enums/currency"
-
-export const getRoomPackagesInputSchema = z.object({
- hotelId: z.string(),
- startDate: z.string(),
- endDate: z.string(),
- adults: z.number(),
- children: z.number().optional().default(0),
- packageCodes: z.array(z.string()).optional().default([]),
-})
-
-export const packagePriceSchema = z
- .object({
- currency: z.nativeEnum(CurrencyEnum),
- price: z.string(),
- totalPrice: z.string(),
- })
- .optional()
- .default({
- currency: CurrencyEnum.SEK,
- price: "0",
- totalPrice: "0",
- }) // TODO: Remove optional and default when the API change has been deployed
-
-export const packagesSchema = z.object({
- code: z.nativeEnum(RoomPackageCodeEnum),
- itemCode: z.string(),
- description: z.string(),
- localPrice: packagePriceSchema,
- requestedPrice: packagePriceSchema,
- inventories: z.array(
- z.object({
- date: z.string(),
- total: z.number(),
- available: z.number(),
- })
- ),
-})
-
-export const getRoomPackagesSchema = z
- .object({
- data: z.object({
- attributes: z.object({
- hotelId: z.number(),
- packages: z.array(packagesSchema),
- }),
- relationships: z
- .object({
- links: z.array(
- z.object({
- url: z.string(),
- type: z.string(),
- })
- ),
- })
- .optional(),
- type: z.string(),
- }),
- })
- .transform((data) => data.data.attributes.packages)
diff --git a/stores/enter-details.ts b/stores/enter-details.ts
index 88290619e..374f1dc89 100644
--- a/stores/enter-details.ts
+++ b/stores/enter-details.ts
@@ -23,7 +23,12 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast"
const SESSION_STORAGE_KEY = "enterDetails"
-interface EnterDetailsState {
+type TotalPrice = {
+ local: { price: number; currency: string }
+ euro: { price: number; currency: string }
+}
+
+export interface EnterDetailsState {
userData: {
bedType: BedTypeSchema | undefined
breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined
@@ -32,6 +37,9 @@ interface EnterDetailsState {
steps: StepEnum[]
selectRateUrl: string
currentStep: StepEnum
+ totalPrice: TotalPrice
+ isSubmittingDisabled: boolean
+ isSummaryOpen: boolean
isValid: Record
completeStep: (updatedData: Partial) => void
navigate: (
@@ -42,6 +50,9 @@ interface EnterDetailsState {
>
) => void
setCurrentStep: (step: StepEnum) => void
+ toggleSummaryOpen: () => void
+ setTotalPrice: (totalPrice: TotalPrice) => void
+ setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void
}
export function initEditDetailsState(
@@ -129,6 +140,12 @@ export function initEditDetailsState(
roomData,
selectRateUrl,
steps: Object.values(StepEnum),
+ totalPrice: {
+ local: { price: 0, currency: "" },
+ euro: { price: 0, currency: "" },
+ },
+ isSummaryOpen: false,
+ isSubmittingDisabled: false,
setCurrentStep: (step) => set({ currentStep: step }),
navigate: (step, updatedData) =>
set(
@@ -166,6 +183,10 @@ export function initEditDetailsState(
get().navigate(nextStep, updatedData)
})
),
+ toggleSummaryOpen: () => set({ isSummaryOpen: !get().isSummaryOpen }),
+ setTotalPrice: (totalPrice) => set({ totalPrice: totalPrice }),
+ setIsSubmittingDisabled: (isSubmittingDisabled) =>
+ set({ isSubmittingDisabled }),
}))
}
diff --git a/stores/guests-rooms.ts b/stores/guests-rooms.ts
deleted file mode 100644
index 04cfbc3ec..000000000
--- a/stores/guests-rooms.ts
+++ /dev/null
@@ -1,227 +0,0 @@
-"use client"
-
-import { produce } from "immer"
-import { createContext, useContext } from "react"
-import { create, useStore } from "zustand"
-
-import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
-import {
- Child,
- GuestsRoom,
-} from "@/types/components/bookingWidget/guestsRoomsPicker"
-
-const SESSION_STORAGE_KEY = "guests_rooms"
-
-interface extendedGuestsRoom extends GuestsRoom {
- childrenInAdultsBed: number
-}
-interface GuestsRoomsState {
- rooms: extendedGuestsRoom[]
- adultCount: number
- childCount: number
- isValidated: boolean
-}
-
-interface GuestsRoomsStoreState extends GuestsRoomsState {
- increaseAdults: (roomIndex: number) => void
- decreaseAdults: (roomIndex: number) => void
- increaseChildren: (roomIndex: number) => void
- decreaseChildren: (roomIndex: number) => Child[]
- updateChildAge: (age: number, roomIndex: number, childIndex: number) => void
- updateChildBed: (bed: number, roomIndex: number, childIndex: number) => void
- increaseChildInAdultsBed: (roomIndex: number) => void
- decreaseChildInAdultsBed: (roomIndex: number) => void
- increaseRoom: () => void
- decreaseRoom: (roomIndex: number) => void
- setIsValidated: (isValidated: boolean) => void
-}
-
-export function validateBedTypes(data: extendedGuestsRoom[]) {
- data.forEach((room) => {
- room.child.forEach((child) => {
- const allowedBedTypes: number[] = []
- if (child.age <= 5 && room.adults >= room.childrenInAdultsBed) {
- allowedBedTypes.push(ChildBedMapEnum.IN_ADULTS_BED)
- } else if (child.age <= 5) {
- room.childrenInAdultsBed = room.childrenInAdultsBed - 1
- }
- if (child.age < 3) {
- allowedBedTypes.push(ChildBedMapEnum.IN_CRIB)
- }
- if (child.age > 2) {
- allowedBedTypes.push(ChildBedMapEnum.IN_EXTRA_BED)
- }
- if (!allowedBedTypes.includes(child.bed)) {
- child.bed = allowedBedTypes[0]
- }
- })
- })
-}
-
-export function initGuestsRoomsState(initData?: GuestsRoom[]) {
- const isBrowser = typeof window !== "undefined"
- const sessionData = isBrowser
- ? sessionStorage.getItem(SESSION_STORAGE_KEY)
- : null
-
- const defaultGuestsData: extendedGuestsRoom = {
- adults: 1,
- child: [],
- childrenInAdultsBed: 0,
- }
- const defaultData: GuestsRoomsState = {
- rooms: [defaultGuestsData],
- adultCount: 1,
- childCount: 0,
- isValidated: false,
- }
-
- let inputData: GuestsRoomsState = defaultData
- if (sessionData) {
- inputData = JSON.parse(sessionData)
- }
- if (initData) {
- inputData.rooms = initData.map((room) => {
- const childrenInAdultsBed = room.child
- ? room.child.reduce((acc, child) => {
- acc = acc + (child.bed == ChildBedMapEnum.IN_ADULTS_BED ? 1 : 0)
- return acc
- }, 0)
- : 0
- return { ...defaultGuestsData, ...room, childrenInAdultsBed }
- }) as extendedGuestsRoom[]
-
- inputData.adultCount = initData.reduce((acc, room) => {
- acc = acc + room.adults
- return acc
- }, 0)
- inputData.childCount = initData.reduce((acc, room) => {
- acc = acc + room.child?.length
- return acc
- }, 0)
- validateBedTypes(inputData.rooms)
- }
-
- return create()((set, get) => ({
- ...inputData,
- increaseAdults: (roomIndex) =>
- set(
- produce((state: GuestsRoomsState) => {
- state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1
- state.adultCount = state.adultCount + 1
- })
- ),
- decreaseAdults: (roomIndex) =>
- set(
- produce((state: GuestsRoomsState) => {
- state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1
- state.adultCount = state.adultCount - 1
- if (
- state.rooms[roomIndex].childrenInAdultsBed >
- state.rooms[roomIndex].adults
- ) {
- const toUpdateIndex = state.rooms[roomIndex].child.findIndex(
- (child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED
- )
- if (toUpdateIndex != -1) {
- state.rooms[roomIndex].child[toUpdateIndex].bed =
- state.rooms[roomIndex].child[toUpdateIndex].age < 3
- ? ChildBedMapEnum.IN_CRIB
- : ChildBedMapEnum.IN_EXTRA_BED
- state.rooms[roomIndex].childrenInAdultsBed =
- state.rooms[roomIndex].adults
- }
- }
- })
- ),
- increaseChildren: (roomIndex) =>
- set(
- produce((state: GuestsRoomsState) => {
- state.rooms[roomIndex].child.push({
- age: -1,
- bed: -1,
- })
- state.childCount = state.childCount + 1
- })
- ),
- decreaseChildren: (roomIndex) => {
- set(
- produce((state: GuestsRoomsState) => {
- const roomChildren = state.rooms[roomIndex].child
- if (
- roomChildren.length &&
- roomChildren[roomChildren.length - 1].bed ==
- ChildBedMapEnum.IN_ADULTS_BED
- ) {
- state.rooms[roomIndex].childrenInAdultsBed =
- state.rooms[roomIndex].childrenInAdultsBed - 1
- }
- state.rooms[roomIndex].child.pop()
- state.childCount = state.childCount - 1
- })
- )
- return get().rooms[roomIndex].child
- },
- updateChildAge: (age, roomIndex, childIndex) =>
- set(
- produce((state: GuestsRoomsState) => {
- state.rooms[roomIndex].child[childIndex].age = age
- })
- ),
- updateChildBed: (bed, roomIndex, childIndex) =>
- set(
- produce((state: GuestsRoomsState) => {
- state.rooms[roomIndex].child[childIndex].bed = bed
- })
- ),
- increaseChildInAdultsBed: (roomIndex) =>
- set(
- produce((state: GuestsRoomsState) => {
- state.rooms[roomIndex].childrenInAdultsBed =
- state.rooms[roomIndex].childrenInAdultsBed + 1
- })
- ),
- decreaseChildInAdultsBed: (roomIndex) =>
- set(
- produce((state: GuestsRoomsState) => {
- state.rooms[roomIndex].childrenInAdultsBed =
- state.rooms[roomIndex].childrenInAdultsBed - 1
- })
- ),
- increaseRoom: () =>
- set(
- produce((state: GuestsRoomsState) => {
- state.rooms.push({
- adults: 1,
- child: [],
- childrenInAdultsBed: 0,
- })
- })
- ),
- decreaseRoom: (roomIndex) =>
- set(
- produce((state: GuestsRoomsState) => {
- state.rooms.splice(roomIndex, 1)
- })
- ),
- setIsValidated: (isValidated) => set(() => ({ isValidated })),
- }))
-}
-
-export type GuestsRoomsStore = ReturnType
-
-export const GuestsRoomsContext = createContext(null)
-
-export const useGuestsRoomsStore = (
- selector: (store: GuestsRoomsStoreState) => T
-): T => {
- const guestsRoomsContextStore = useContext(GuestsRoomsContext)
-
- if (!guestsRoomsContextStore) {
- throw new Error(
- `guestsRoomsContextStore must be used within GuestsRoomsContextProvider`
- )
- }
-
- return useStore(guestsRoomsContextStore, selector)
-}
diff --git a/stores/roomAvailability.ts b/stores/roomAvailability.ts
new file mode 100644
index 000000000..ad01453e4
--- /dev/null
+++ b/stores/roomAvailability.ts
@@ -0,0 +1,17 @@
+"use client"
+
+import { create } from "zustand"
+
+interface RoomAvailabilityState {
+ noRoomsAvailable: boolean
+ setNoRoomsAvailable: () => void
+ setRoomsAvailable: () => void
+}
+
+const useRoomAvailableStore = create((set) => ({
+ noRoomsAvailable: false,
+ setNoRoomsAvailable: () => set(() => ({ noRoomsAvailable: true })),
+ setRoomsAvailable: () => set(() => ({ noRoomsAvailable: false })),
+}))
+
+export default useRoomAvailableStore
diff --git a/stores/sidepeek.ts b/stores/sidepeek.ts
index 30d6a00e2..9912617ac 100644
--- a/stores/sidepeek.ts
+++ b/stores/sidepeek.ts
@@ -6,14 +6,17 @@ interface SidePeekState {
activeSidePeek: SidePeekEnum | null
hotelId: string | null
roomTypeCode: string | null
+ showCTA: boolean
openSidePeek: ({
key,
hotelId,
roomTypeCode,
+ showCTA,
}: {
key: SidePeekEnum | null
hotelId: string
roomTypeCode?: string
+ showCTA?: boolean
}) => void
closeSidePeek: () => void
}
@@ -22,8 +25,9 @@ const useSidePeekStore = create((set) => ({
activeSidePeek: null,
hotelId: null,
roomTypeCode: null,
- openSidePeek: ({ key, hotelId, roomTypeCode }) =>
- set({ activeSidePeek: key, hotelId, roomTypeCode }),
+ showCTA: true,
+ openSidePeek: ({ key, hotelId, roomTypeCode, showCTA }) =>
+ set({ activeSidePeek: key, hotelId, roomTypeCode, showCTA }),
closeSidePeek: () =>
set({ activeSidePeek: null, hotelId: null, roomTypeCode: null }),
}))
diff --git a/stores/sticky-position.ts b/stores/sticky-position.ts
index 99272ab62..93328b9e9 100644
--- a/stores/sticky-position.ts
+++ b/stores/sticky-position.ts
@@ -3,7 +3,6 @@ import { create } from "zustand"
export enum StickyElementNameEnum {
SITEWIDE_ALERT = "SITEWIDE_ALERT",
BOOKING_WIDGET = "BOOKING_WIDGET",
- BOOKING_WIDGET_MOBILE = "BOOKING_WIDGET_MOBILE",
HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION",
HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP",
}
@@ -32,7 +31,6 @@ interface StickyStore {
const priorityMap: Record = {
[StickyElementNameEnum.SITEWIDE_ALERT]: 1,
[StickyElementNameEnum.BOOKING_WIDGET]: 2,
- [StickyElementNameEnum.BOOKING_WIDGET_MOBILE]: 2,
[StickyElementNameEnum.HOTEL_TAB_NAVIGATION]: 3,
[StickyElementNameEnum.HOTEL_STATIC_MAP]: 3,
diff --git a/types/components/bookingWidget/guestsRoomsPicker.ts b/types/components/bookingWidget/guestsRoomsPicker.ts
index 61e8f7d7a..a075c9f8d 100644
--- a/types/components/bookingWidget/guestsRoomsPicker.ts
+++ b/types/components/bookingWidget/guestsRoomsPicker.ts
@@ -13,26 +13,23 @@ export type GuestsRoom = {
child: Child[]
}
-export interface GuestsRoomsPickerProps {
- closePicker: () => void
-}
-
export type GuestsRoomPickerProps = {
index: number
}
-export type AdultSelectorProps = {
- roomIndex: number
-}
-
-export type ChildSelectorProps = {
+export type SelectorProps = {
roomIndex: number
+ currentAdults: number
+ currentChildren: Child[]
+ childrenInAdultsBed: number
}
export type ChildInfoSelectorProps = {
child: Child
+ adults: number
index: number
roomIndex: number
+ childrenInAdultsBed: number
}
export interface CounterProps {
diff --git a/types/components/form/bookingwidget.ts b/types/components/form/bookingwidget.ts
index 14a548952..887c4eae2 100644
--- a/types/components/form/bookingwidget.ts
+++ b/types/components/form/bookingwidget.ts
@@ -1,14 +1,10 @@
-import { FormState, UseFormReturn } from "react-hook-form"
-
-import type {
- BookingWidgetSchema,
- BookingWidgetType,
-} from "@/types/components/bookingWidget"
+import type { BookingWidgetType } from "@/types/components/bookingWidget"
import type { Location, Locations } from "@/types/trpc/routers/hotel/locations"
export interface BookingWidgetFormProps {
locations: Locations
type?: BookingWidgetType
+ setIsOpen: (isOpen: boolean) => void
}
export interface BookingWidgetFormContentProps {
diff --git a/types/components/form/filterChip.ts b/types/components/form/filterChip.ts
index 3ff40673d..062d398f4 100644
--- a/types/components/form/filterChip.ts
+++ b/types/components/form/filterChip.ts
@@ -11,6 +11,7 @@ export interface FilterChipProps {
value?: string
selected?: boolean
disabled?: boolean
+ hasTooltip?: boolean
}
export type FilterChipCheckboxProps = Omit
diff --git a/types/components/header/headerLink.ts b/types/components/header/headerLink.ts
index deb1c71ab..3ee168dea 100644
--- a/types/components/header/headerLink.ts
+++ b/types/components/header/headerLink.ts
@@ -1,3 +1,9 @@
-import type { LinkProps } from "@/components/TempDesignSystem/Link/link"
+import type { LinkProps } from "next/link"
-export interface HeaderLinkProps extends React.PropsWithChildren {}
+import type { IconName } from "../icon"
+
+export interface HeaderLinkProps extends React.PropsWithChildren {
+ href: LinkProps["href"]
+ iconName: IconName | null
+ iconSize?: number
+}
diff --git a/types/components/header/mobileMenu.ts b/types/components/header/mobileMenu.ts
index d773b397b..1d0b73513 100644
--- a/types/components/header/mobileMenu.ts
+++ b/types/components/header/mobileMenu.ts
@@ -4,4 +4,5 @@ import type { Header } from "@/types/trpc/routers/contentstack/header"
export interface MobileMenuProps {
languageUrls: LanguageSwitcherData
topLink: Header["header"]["topLink"]
+ isLoggedIn: boolean
}
diff --git a/types/components/header/topLink.ts b/types/components/header/topLink.ts
new file mode 100644
index 000000000..5f9e855da
--- /dev/null
+++ b/types/components/header/topLink.ts
@@ -0,0 +1,7 @@
+import type { Header } from "@/types/trpc/routers/contentstack/header"
+
+export interface TopLinkProps {
+ isLoggedIn: boolean
+ topLink: Header["header"]["topLink"]
+ iconSize?: number
+}
diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts
index 6aad97085..74ed4bda4 100644
--- a/types/components/hotelReservation/enterDetails/bookingData.ts
+++ b/types/components/hotelReservation/enterDetails/bookingData.ts
@@ -1,6 +1,8 @@
import { RoomPackageCodeEnum } from "../selectRate/roomFilter"
import { Child } from "../selectRate/selectRate"
+import { Packages } from "@/types/requests/packages"
+
interface Room {
adults: number
roomTypeCode: string
@@ -16,8 +18,8 @@ export interface BookingData {
}
type Price = {
- price?: string
- currency?: string
+ price: number
+ currency: string
}
export type RoomsData = {
@@ -27,4 +29,5 @@ export type RoomsData = {
adults: number
children?: Child[]
cancellationText: string
+ packages: Packages | null
}
diff --git a/types/components/hotelReservation/hotelSidePeek.ts b/types/components/hotelReservation/hotelSidePeek.ts
index 3f5b2ce7c..d188215b5 100644
--- a/types/components/hotelReservation/hotelSidePeek.ts
+++ b/types/components/hotelReservation/hotelSidePeek.ts
@@ -5,4 +5,5 @@ export type HotelSidePeekProps = {
hotel: Hotel
activeSidePeek: SidePeekEnum
close: () => void
+ showCTA: boolean
}
diff --git a/types/components/hotelReservation/selectHotel/hotePriceListProps.ts b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts
new file mode 100644
index 000000000..2464fad43
--- /dev/null
+++ b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts
@@ -0,0 +1,5 @@
+import type { HotelsAvailabilityPrices } from "@/server/routers/hotels/output"
+
+export type HotelPriceListProps = {
+ price: HotelsAvailabilityPrices
+}
diff --git a/types/components/hotelReservation/selectHotel/hotelLogoProps.ts b/types/components/hotelReservation/selectHotel/hotelLogoProps.ts
new file mode 100644
index 000000000..8f19490a6
--- /dev/null
+++ b/types/components/hotelReservation/selectHotel/hotelLogoProps.ts
@@ -0,0 +1,6 @@
+import { Hotel } from "@/types/hotel"
+
+export type HotelLogoProps = {
+ hotelId: Hotel["operaId"]
+ hotelType: Hotel["hotelType"]
+}
diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts
index 28965807a..233fc2105 100644
--- a/types/components/hotelReservation/selectHotel/map.ts
+++ b/types/components/hotelReservation/selectHotel/map.ts
@@ -13,15 +13,13 @@ import type { Coordinates } from "@/types/components/maps/coordinates"
export interface HotelListingProps {
hotels: HotelData[]
activeHotelPin?: string | null
- onHotelCardHover?: (hotelName: string | null) => void
+ setActiveHotelPin: (hotelName: string | null) => void
}
export interface SelectHotelMapProps {
apiKey: string
- coordinates: Coordinates
hotelPins: HotelPin[]
mapId: string
- isModal: boolean
hotels: HotelData[]
}
@@ -40,6 +38,7 @@ export type HotelPin = {
}[]
amenities: Filter[]
ratings: number | null
+ operaId: string
}
export interface HotelListingMapContentProps {
@@ -50,6 +49,12 @@ export interface HotelListingMapContentProps {
export interface HotelCardDialogProps {
isOpen: boolean
- pin: HotelPin
+ data: HotelPin
handleClose: (event: { stopPropagation: () => void }) => void
}
+
+export interface HotelCardDialogListingProps {
+ hotels: HotelData[]
+ activeCard: string | null | undefined
+ onActiveCardChange: (hotelName: string | null) => void
+}
diff --git a/types/components/hotelReservation/selectHotel/priceCardProps.ts b/types/components/hotelReservation/selectHotel/priceCardProps.ts
new file mode 100644
index 000000000..d339b4a06
--- /dev/null
+++ b/types/components/hotelReservation/selectHotel/priceCardProps.ts
@@ -0,0 +1,5 @@
+export type PriceCardProps = {
+ currency: string
+ memberAmount?: string | undefined
+ regularAmount?: string | undefined
+}
diff --git a/types/components/hotelReservation/selectHotel/selectHotel.ts b/types/components/hotelReservation/selectHotel/selectHotel.ts
index b50521040..233cf0076 100644
--- a/types/components/hotelReservation/selectHotel/selectHotel.ts
+++ b/types/components/hotelReservation/selectHotel/selectHotel.ts
@@ -9,6 +9,7 @@ export interface ReadMoreProps {
label: string
hotelId: string
hotel: Hotel
+ showCTA: boolean
}
export interface ContactProps {
diff --git a/types/components/hotelReservation/selectRate/hotelInfoCardProps.ts b/types/components/hotelReservation/selectRate/hotelInfoCardProps.ts
index 0a3526167..088c54e51 100644
--- a/types/components/hotelReservation/selectRate/hotelInfoCardProps.ts
+++ b/types/components/hotelReservation/selectRate/hotelInfoCardProps.ts
@@ -2,4 +2,5 @@ import type { HotelData } from "@/types/hotel"
export type HotelInfoCardProps = {
hotelData: HotelData | null
+ noAvailability: boolean
}
diff --git a/types/components/hotelReservation/selectRate/imageGallery.ts b/types/components/hotelReservation/selectRate/imageGallery.ts
index 5d75189fa..0c16c82e0 100644
--- a/types/components/hotelReservation/selectRate/imageGallery.ts
+++ b/types/components/hotelReservation/selectRate/imageGallery.ts
@@ -1,3 +1,3 @@
import type { GalleryImage } from "@/types/hotel"
-export type ImageGalleryProps = { images: GalleryImage[]; title: string }
+export type ImageGalleryProps = { images?: GalleryImage[]; title: string }
diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts
index f6c0f03b6..40c595508 100644
--- a/types/components/hotelReservation/selectRate/rateSummary.ts
+++ b/types/components/hotelReservation/selectRate/rateSummary.ts
@@ -5,6 +5,6 @@ import type { Rate } from "./selectRate"
export interface RateSummaryProps {
rateSummary: Rate
isUserLoggedIn: boolean
- packages: RoomPackageData
+ packages: RoomPackageData | undefined
roomsAvailability: RoomsAvailability
}
diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts
index caf025524..aa0d647be 100644
--- a/types/components/hotelReservation/selectRate/roomCard.ts
+++ b/types/components/hotelReservation/selectRate/roomCard.ts
@@ -1,10 +1,10 @@
import { z } from "zod"
import {
+ packagePriceSchema,
RateDefinition,
RoomConfiguration,
} from "@/server/routers/hotels/output"
-import { packagePriceSchema } from "@/server/routers/hotels/schemas/packages"
import { RoomPriceSchema } from "./flexibilityOption"
import { Rate } from "./selectRate"
@@ -18,7 +18,7 @@ export type RoomCardProps = {
rateDefinitions: RateDefinition[]
roomCategories: RoomData[]
selectedPackages: RoomPackageCodes[]
- packages: RoomPackageData
+ packages: RoomPackageData | undefined
handleSelectRate: (rate: Rate) => void
}
diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts
index 8250e7f32..f895ed73a 100644
--- a/types/components/hotelReservation/selectRate/roomFilter.ts
+++ b/types/components/hotelReservation/selectRate/roomFilter.ts
@@ -3,7 +3,7 @@ import { z } from "zod"
import {
getRoomPackagesSchema,
packagesSchema,
-} from "@/server/routers/hotels/schemas/packages"
+} from "@/server/routers/hotels/output"
export enum RoomPackageCodeEnum {
PET_ROOM = "PETR",
@@ -16,9 +16,7 @@ export interface RoomFilterProps {
filterOptions: RoomPackageData
}
-export interface RoomPackageData
- extends z.output {}
-
-export type RoomPackageCodes = RoomPackageData[number]["code"]
-
export type RoomPackage = z.output
+export interface RoomPackageData extends Array {}
+
+export type RoomPackageCodes = RoomPackage["code"]
diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts
index 3e3a6117e..163bfd6fa 100644
--- a/types/components/hotelReservation/selectRate/roomSelection.ts
+++ b/types/components/hotelReservation/selectRate/roomSelection.ts
@@ -2,11 +2,21 @@ import type { RoomData } from "@/types/hotel"
import type { SafeUser } from "@/types/user"
import type { RoomsAvailability } from "@/server/routers/hotels/output"
import type { RoomPackageCodes, RoomPackageData } from "./roomFilter"
+import type { Rate } from "./selectRate"
export interface RoomSelectionProps {
roomsAvailability: RoomsAvailability
roomCategories: RoomData[]
user: SafeUser
- packages: RoomPackageData
+ packages: RoomPackageData | undefined
selectedPackages: RoomPackageCodes[]
+ setRateSummary: (rateSummary: Rate) => void
+ rateSummary: Rate | null
+}
+
+export interface SelectRateProps {
+ roomsAvailability: RoomsAvailability
+ roomCategories: RoomData[]
+ user: SafeUser
+ packages: RoomPackageData
}
diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts
index df9ec7a71..578819fb1 100644
--- a/types/components/hotelReservation/selectRate/section.ts
+++ b/types/components/hotelReservation/selectRate/section.ts
@@ -28,7 +28,7 @@ export interface BreakfastSelectionProps extends SectionProps {
export interface DetailsProps extends SectionProps {}
export interface PaymentProps {
- roomPrice: string
+ roomPrice: number
otherPaymentOptions: string[]
savedCreditCards: CreditCard[] | null
mustBeGuaranteed: boolean
diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts
index ba8f3f45c..a1da31b84 100644
--- a/types/components/hotelReservation/selectRate/selectRate.ts
+++ b/types/components/hotelReservation/selectRate/selectRate.ts
@@ -29,6 +29,6 @@ export interface Rate {
roomTypeCode: RoomConfiguration["roomTypeCode"]
priceName: string
public: Product["productType"]["public"]
- member: Product["productType"]["member"]
+ member?: Product["productType"]["member"]
features: RoomConfiguration["features"]
}
diff --git a/types/components/hotelReservation/tripAdvisorProps.ts b/types/components/hotelReservation/tripAdvisorProps.ts
new file mode 100644
index 000000000..62636cdcc
--- /dev/null
+++ b/types/components/hotelReservation/tripAdvisorProps.ts
@@ -0,0 +1,3 @@
+export type TripAdvisorProps = {
+ rating: number
+}
diff --git a/types/components/icon.ts b/types/components/icon.ts
index 17156dee4..3d47bbe9a 100644
--- a/types/components/icon.ts
+++ b/types/components/icon.ts
@@ -82,6 +82,7 @@ export enum IconName {
Phone = "Phone",
Plus = "Plus",
PlusCircle = "PlusCircle",
+ PriceTag = "PriceTag",
Restaurant = "Restaurant",
RoomService = "RoomService",
Sauna = "Sauna",
diff --git a/types/components/imageGallery.ts b/types/components/imageGallery.ts
new file mode 100644
index 000000000..019fd8032
--- /dev/null
+++ b/types/components/imageGallery.ts
@@ -0,0 +1,9 @@
+import type { GalleryImage } from "@/types/hotel"
+
+export type ImageGalleryProps = {
+ images?: GalleryImage[]
+ title: string
+ fill?: boolean
+ width?: number
+ height?: number
+}
diff --git a/types/components/lightbox/lightbox.ts b/types/components/lightbox/lightbox.ts
index af592bca6..3c0b7db16 100644
--- a/types/components/lightbox/lightbox.ts
+++ b/types/components/lightbox/lightbox.ts
@@ -3,12 +3,12 @@ import type { GalleryImage } from "@/types/hotel"
export interface LightboxProps {
images: GalleryImage[]
dialogTitle: string /* Accessible title for dialog screen readers */
- children: React.ReactNode
+ onClose: () => void
+ isOpen: boolean
}
export interface GalleryProps {
images: GalleryImage[]
- dialogTitle: string
onClose: () => void
onSelectImage: (image: GalleryImage) => void
onImageClick: () => void
diff --git a/types/components/myPages/header.ts b/types/components/myPages/header.ts
index 82a662a94..1af4abf21 100644
--- a/types/components/myPages/header.ts
+++ b/types/components/myPages/header.ts
@@ -7,6 +7,6 @@ export type HeaderProps = {
}
preamble?: string | null
textTransform?: HeadingProps["textTransform"]
- title: string | null
+ title?: string | null
topTitle?: boolean
}
diff --git a/types/enums/hotelType.ts b/types/enums/hotelType.ts
new file mode 100644
index 000000000..00826f4a0
--- /dev/null
+++ b/types/enums/hotelType.ts
@@ -0,0 +1,5 @@
+export enum HotelTypeEnum {
+ Signature = "signature",
+ ScandicGo = "scandicgo",
+ Regular = "regular",
+}
diff --git a/types/enums/signatureHotel.ts b/types/enums/signatureHotel.ts
new file mode 100644
index 000000000..1f5b68d6a
--- /dev/null
+++ b/types/enums/signatureHotel.ts
@@ -0,0 +1,7 @@
+export enum SignatureHotelEnum {
+ DowntownCamper = "879",
+ GrandHotelOslo = "340",
+ Haymarket = "890",
+ HotelNorge = "785",
+ Marski = "605",
+}
diff --git a/types/requests/packages.ts b/types/requests/packages.ts
index 3d794e0f3..fb242917f 100644
--- a/types/requests/packages.ts
+++ b/types/requests/packages.ts
@@ -1,6 +1,15 @@
import { z } from "zod"
-import { getBreakfastPackageInputSchema } from "@/server/routers/hotels/input"
+import {
+ getBreakfastPackageInputSchema,
+ getRoomPackagesInputSchema,
+} from "@/server/routers/hotels/input"
+import { getRoomPackagesSchema } from "@/server/routers/hotels/output"
export interface BreackfastPackagesInput
extends z.input {}
+
+export interface PackagesInput
+ extends z.input {}
+
+export interface Packages extends z.output {}