From ed55f966c7e6085658345aacc64646e9f669931c Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Mon, 7 Oct 2024 09:17:04 +0200 Subject: [PATCH 01/10] feat/sw-561: some design tweaks for container widths --- app/[lang]/(live)/(protected)/my-pages/layout.module.css | 2 ++ .../(live)/(public)/[contentType]/[uid]/layout.module.css | 2 ++ app/[lang]/(live)/(public)/hotelreservation/layout.module.css | 2 ++ components/HotelReservation/HotelCard/hotelCard.module.css | 4 ++-- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.module.css b/app/[lang]/(live)/(protected)/my-pages/layout.module.css index bee1488c6..a2ff660c4 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.module.css +++ b/app/[lang]/(live)/(protected)/my-pages/layout.module.css @@ -5,6 +5,8 @@ gap: var(--Spacing-x3); grid-template-rows: auto 1fr; min-height: 100dvh; + max-width: var(--max-width); + margin: 0 auto; } .content { diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.module.css b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.module.css index edd0b9f96..af1b93963 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.module.css +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.module.css @@ -5,4 +5,6 @@ gap: var(--Spacing-x3); grid-template-rows: auto 1fr; position: relative; + max-width: var(--max-width); + margin: 0 auto; } diff --git a/app/[lang]/(live)/(public)/hotelreservation/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/layout.module.css index 4478bdb18..e12cfcba6 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/layout.module.css @@ -1,3 +1,5 @@ .layout { min-height: 100dvh; + max-width: var(--max-width); + margin: 0 auto; } diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index a6c4a892d..9bb66cfd3 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -9,7 +9,7 @@ background-color: var(--Base-Surface-Primary-light-Normal); border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Medium); - width: 307px; + width: 100%; } .imageContainer { @@ -72,7 +72,7 @@ "image hotel" "image prices"; overflow: hidden; - width: 1050px; + width: 100%; padding: 0; } From 158472524f31f2b727e4a29492d9001e344a811e Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Mon, 7 Oct 2024 14:10:25 +0200 Subject: [PATCH 02/10] More design fixes --- components/ContentType/HotelPage/hotelPage.module.css | 1 + components/HotelReservation/HotelCard/hotelCard.module.css | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index eda544de8..9d38ca1da 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -62,6 +62,7 @@ ); /* Full height without the header + booking widget */ max-height: 935px; /* Fixed max according to figma */ overflow: hidden; + width: 100%; } .pageContainer > nav { diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 9bb66cfd3..0ffa5b150 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -72,7 +72,6 @@ "image hotel" "image prices"; overflow: hidden; - width: 100%; padding: 0; } From 801da501206c6803e7a23cc4c31bd51003c3d8a8 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Mon, 7 Oct 2024 14:22:43 +0200 Subject: [PATCH 03/10] Remove unnecessary css --- components/ContentType/HotelPage/hotelPage.module.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index 9d38ca1da..c5dd0a038 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -1,7 +1,5 @@ .pageContainer { display: grid; - max-width: var(--max-width); - margin: 0 auto; grid-template-areas: "hotelImages" "tabNavigation" From 915657bf156295b84388121d321488ec4184c5b4 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Mon, 7 Oct 2024 14:33:08 +0200 Subject: [PATCH 04/10] Fix for current navigation --- components/Current/Header/MainMenu/mainMenu.module.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/Current/Header/MainMenu/mainMenu.module.css b/components/Current/Header/MainMenu/mainMenu.module.css index 122abdc4f..a7677e895 100644 --- a/components/Current/Header/MainMenu/mainMenu.module.css +++ b/components/Current/Header/MainMenu/mainMenu.module.css @@ -9,6 +9,8 @@ width: 100%; z-index: 99999; height: var(--current-mobile-site-header-height); + max-width: var(--max-width-navigation); + margin: 0 auto; } .container { From cc60b4d264ca1c2c46d42b70d6a3383cf758e2b6 Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Tue, 8 Oct 2024 16:37:58 +0200 Subject: [PATCH 05/10] feat: add selected room card --- .../hotelreservation/[step]/layout.module.css | 20 +++++ .../hotelreservation/[step]/layout.tsx | 38 ++++++++ .../hotelreservation/[step]/page.module.css | 30 ------- .../(public)/hotelreservation/[step]/page.tsx | 90 ++++++++----------- .../EnterDetails/SelectedRoom/index.tsx | 77 ++++++++++++++++ .../SelectedRoom/selectedRoom.module.css | 51 +++++++++++ .../hotelSelectionHeader.module.css | 17 +++- .../HotelSelectionHeader/index.tsx | 58 ++++++------ .../SelectRate/Payment/index.tsx | 4 +- .../sectionAccordion.module.css | 2 +- components/Icons/icon.module.css | 5 ++ components/Icons/variants.ts | 5 +- components/Lightbox/FullView.tsx | 6 +- .../TempDesignSystem/Button/button.module.css | 13 +-- .../Text/Subtitle/subtitle.module.css | 8 +- .../Text/Subtitle/variants.ts | 1 + i18n/dictionaries/da.json | 13 ++- i18n/dictionaries/de.json | 3 +- i18n/dictionaries/en.json | 8 +- i18n/dictionaries/fi.json | 10 +-- i18n/dictionaries/no.json | 13 ++- i18n/dictionaries/sv.json | 13 ++- 22 files changed, 324 insertions(+), 161 deletions(-) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css create mode 100644 app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/[step]/page.module.css create mode 100644 components/HotelReservation/EnterDetails/SelectedRoom/index.tsx create mode 100644 components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css new file mode 100644 index 000000000..f40fca31d --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css @@ -0,0 +1,20 @@ +.layout { + min-height: 100dvh; + background-color: var(--Scandic-Brand-Warm-White); +} + +.content { + display: grid; + gap: var(--Spacing-x3) var(--Spacing-x9); + grid-template-columns: 1fr 340px; + grid-template-rows: auto 1fr; + margin: var(--Spacing-x5) auto 0; + max-width: var(--max-width-navigation); + padding: var(--Spacing-x6) var(--Spacing-x2) 0; +} + +.summary { + align-self: flex-start; + grid-column: 2 / 3; + grid-row: 1/-1; +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx new file mode 100644 index 000000000..c234123a4 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx @@ -0,0 +1,38 @@ +import { redirect } from "next/navigation" + +import { serverClient } from "@/lib/trpc/server" + +import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" +import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" +import Summary from "@/components/HotelReservation/SelectRate/Summary" + +import styles from "./layout.module.css" + +import type { LangParams, LayoutArgs } from "@/types/params" + +export default async function StepLayout({ + children, + params, +}: React.PropsWithChildren>) { + const hotel = await serverClient().hotel.hotelData.get({ + hotelId: "811", + language: params.lang, + }) + + if (!hotel?.data) { + redirect(`/${params.lang}`) + } + + return ( +
+ +
+ + {children} + +
+
+ ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/[step]/page.module.css deleted file mode 100644 index 8962fd5ee..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.module.css +++ /dev/null @@ -1,30 +0,0 @@ -.page { - min-height: 100dvh; - padding-top: var(--Spacing-x6); - padding-left: var(--Spacing-x2); - padding-right: var(--Spacing-x2); - background-color: var(--Scandic-Brand-Warm-White); -} - -.content { - max-width: 1134px; - margin-top: var(--Spacing-x5); - margin-left: auto; - margin-right: auto; - display: flex; - justify-content: space-between; - gap: var(--Spacing-x7); -} - -.section { - flex-grow: 1; -} - -.summary { - max-width: 340px; -} - -.form { - display: grid; - gap: var(--Spacing-x2); -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx index 050aa0280..b853fc9bc 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx @@ -9,15 +9,11 @@ import { trpc } from "@/lib/trpc/client" import BedType from "@/components/HotelReservation/EnterDetails/BedType" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Details from "@/components/HotelReservation/EnterDetails/Details" -import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" import Payment from "@/components/HotelReservation/SelectRate/Payment" import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion" -import Summary from "@/components/HotelReservation/SelectRate/Summary" import LoadingSpinner from "@/components/LoadingSpinner" -import styles from "./page.module.css" - -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" enum StepEnum { selectBed = "select-bed", @@ -75,51 +71,43 @@ export default function StepPage({ } return ( -
- -
-
- - - - - - - -
- - - - -
- -
-
+
+ + + + + + + +
+ + + + +
) } diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx new file mode 100644 index 000000000..7812c6be6 --- /dev/null +++ b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx @@ -0,0 +1,77 @@ +"use client" + +import { useIntl } from "react-intl" + +import { EditIcon, ImageIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "./selectedRoom.module.css" + +export default function SelectedRoom() { + const intl = useIntl() + return ( +
+
+ +
+
+
+ + {intl.formatMessage({ id: "Your room" })} + +
+ {/** + * [TEMP] + * No translation on Subtitles as they will be derived + * from Room selection. + */} + + Cozy cabin + + + Free rebooking + + + Pay now + +
+
+ +
+
+ ) +} diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css new file mode 100644 index 000000000..5e64327cf --- /dev/null +++ b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css @@ -0,0 +1,51 @@ +.container { + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Large); + display: grid; + grid-template-columns: 144px 1fr; + gap: var(--Spacing-x3); + padding: var(--Spacing-x2) var(--Spacing-x4) var(--Spacing-x2) + var(--Spacing-x2); +} + +.tempImage { + align-items: center; + background-color: lightgray; + border-radius: var(--Corner-radius-Medium); + display: flex; + height: auto; + justify-content: center; + min-height: 80px; +} + +.content { + align-items: center; + display: grid; + gap: var(--Spacing-x3); + grid-template-columns: 1fr auto; +} + +.textContainer { + display: grid; +} + +.label { + grid-column: 1 / -1; +} + +.text { + display: flex; + flex-wrap: wrap; + gap: var(--Spacing-x1); +} + +p.invertFontWeight { + font-weight: 400; +} + +.invertFontWeight:not(:last-of-type)::after, +.room::after { + color: var(--UI-Text-Medium-contrast); + content: "∙"; + padding-left: var(--Spacing-x1); +} diff --git a/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css b/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css index 9646ec746..268bfe4fe 100644 --- a/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css +++ b/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css @@ -1,10 +1,13 @@ .hotelSelectionHeader { - display: flex; - flex-direction: column; background-color: var(--Base-Surface-Subtle-Normal); padding: var(--Spacing-x3) var(--Spacing-x2); - justify-content: center; +} + +.hotelSelectionHeaderWrapper { + display: flex; + flex-direction: column; gap: var(--Spacing-x3); + justify-content: center; } .titleContainer { @@ -33,9 +36,15 @@ @media (min-width: 768px) { .hotelSelectionHeader { - flex-direction: row; padding: var(--Spacing-x4) var(--Spacing-x5); + } + + .hotelSelectionHeaderWrapper { + flex-direction: row; gap: var(--Spacing-x6); + max-width: var(--max-width-navigation); + margin: 0 auto; + width: 100%; } .titleContainer > h1 { diff --git a/components/HotelReservation/HotelSelectionHeader/index.tsx b/components/HotelReservation/HotelSelectionHeader/index.tsx index e34f05f37..222dceda1 100644 --- a/components/HotelReservation/HotelSelectionHeader/index.tsx +++ b/components/HotelReservation/HotelSelectionHeader/index.tsx @@ -19,35 +19,35 @@ export default function HotelSelectionHeader({ return (
-
- - {hotel.name} - -
- - {hotel.address.streetAddress}, {hotel.address.city} - -
- -
- - {intl.formatMessage( - { - id: "Distance to city centre", - }, - { number: hotel.location.distanceToCentre } - )} - -
-
-
- -
-
- - {hotel.hotelContent.texts.descriptions.short} - - +
+
+ + {hotel.name} + +
+ + {hotel.address.streetAddress}, {hotel.address.city} + +
+ +
+ + {intl.formatMessage( + { id: "Distance to city centre" }, + { number: hotel.location.distanceToCentre } + )} + +
+
+
+ +
+
+ + {hotel.hotelContent.texts.descriptions.short} + + +
) diff --git a/components/HotelReservation/SelectRate/Payment/index.tsx b/components/HotelReservation/SelectRate/Payment/index.tsx index 3ddf73c01..8216e8631 100644 --- a/components/HotelReservation/SelectRate/Payment/index.tsx +++ b/components/HotelReservation/SelectRate/Payment/index.tsx @@ -129,7 +129,7 @@ export default function Payment({ hotel }: PaymentProps) { name="payment-method" id="card" value="card" - checked={selectedPaymentMethod === "card"} + defaultChecked={selectedPaymentMethod === "card"} /> @@ -145,7 +145,7 @@ export default function Payment({ hotel }: PaymentProps) { name="payment-method" id={paymentOption} value={paymentOption} - checked={selectedPaymentMethod === paymentOption} + defaultChecked={selectedPaymentMethod === paymentOption} /> diff --git a/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css index 8c1a05ba4..653c98fad 100644 --- a/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css @@ -40,7 +40,7 @@ .iconWrapper { position: relative; top: var(--Spacing-x1); - z-index: 10; + z-index: 2; } .circle { diff --git a/components/Icons/icon.module.css b/components/Icons/icon.module.css index 3fa235d32..bb86becdb 100644 --- a/components/Icons/icon.module.css +++ b/components/Icons/icon.module.css @@ -66,3 +66,8 @@ .blue * { fill: var(--UI-Input-Controls-Fill-Selected); } + +.baseButtonTertiaryOnFillNormal, +.baseButtonTertiaryOnFillNormal * { + fill: var(--Base-Button-Tertiary-On-Fill-Normal); +} diff --git a/components/Icons/variants.ts b/components/Icons/variants.ts index 12b9cb574..911d183da 100644 --- a/components/Icons/variants.ts +++ b/components/Icons/variants.ts @@ -5,19 +5,20 @@ import styles from "./icon.module.css" const config = { variants: { color: { + baseButtonTertiaryOnFillNormal: styles.baseButtonTertiaryOnFillNormal, baseIconLowContrast: styles.baseIconLowContrast, black: styles.black, + blue: styles.blue, burgundy: styles.burgundy, + green: styles.green, grey80: styles.grey80, pale: styles.pale, peach80: styles.peach80, primaryLightOnSurfaceAccent: styles.plosa, red: styles.red, - green: styles.green, white: styles.white, uiTextHighContrast: styles.uiTextHighContrast, uiTextMediumContrast: styles.uiTextMediumContrast, - blue: styles.blue, }, }, defaultVariants: { diff --git a/components/Lightbox/FullView.tsx b/components/Lightbox/FullView.tsx index 73f6dde98..5541d365b 100644 --- a/components/Lightbox/FullView.tsx +++ b/components/Lightbox/FullView.tsx @@ -49,10 +49,10 @@ export default function FullView({ className={styles.fullViewImage} > {image.alt}
diff --git a/components/TempDesignSystem/Button/button.module.css b/components/TempDesignSystem/Button/button.module.css index f6499c5c1..0afa102a1 100644 --- a/components/TempDesignSystem/Button/button.module.css +++ b/components/TempDesignSystem/Button/button.module.css @@ -64,10 +64,10 @@ a.default { justify-content: center; } -.icon { +.btn.icon:is(.small, .medium, .large) { align-items: center; display: flex; - gap: var(--Spacing-x-half); + gap: var(--Spacing-x1); justify-content: center; } @@ -76,7 +76,8 @@ a.default { font-size: var(--typography-Caption-Bold-fontSize); line-height: var(--typography-Caption-Bold-lineHeight); gap: var(--Spacing-x-quarter); - padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2); /* Special case padding to adjust the missing border */ + padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2); + /* Special case padding to adjust the missing border */ } .btn.small.secondary { @@ -85,7 +86,8 @@ a.default { .btn.medium { gap: var(--Spacing-x-half); - padding: calc(var(--Spacing-x-one-and-half) + 2px) var(--Spacing-x2); /* Special case padding to adjust the missing border */ + padding: calc(var(--Spacing-x-one-and-half) + 2px) var(--Spacing-x2); + /* Special case padding to adjust the missing border */ } .medium.secondary { @@ -94,7 +96,8 @@ a.default { .btn.large { gap: var(--Spacing-x-half); - padding: calc(var(--Spacing-x2) + 2px) var(--Spacing-x3); /* Special case padding to adjust the missing border */ + padding: calc(var(--Spacing-x2) + 2px) var(--Spacing-x3); + /* Special case padding to adjust the missing border */ } .large.secondary { diff --git a/components/TempDesignSystem/Text/Subtitle/subtitle.module.css b/components/TempDesignSystem/Text/Subtitle/subtitle.module.css index 8fdac30c4..22d09e16e 100644 --- a/components/TempDesignSystem/Text/Subtitle/subtitle.module.css +++ b/components/TempDesignSystem/Text/Subtitle/subtitle.module.css @@ -10,7 +10,7 @@ 0.3vw + 15px, var(--typography-Subtitle-1-Desktop-fontSize) ); - font-weight: 600; + font-weight: 500; letter-spacing: var(--typography-Subtitle-1-letterSpacing); line-height: var(--typography-Subtitle-1-lineHeight); } @@ -22,7 +22,7 @@ 0.3vw + 15px, var(--typography-Subtitle-2-Desktop-fontSize) ); - font-weight: 600; + font-weight: 500; letter-spacing: var(--typography-Subtitle-2-letterSpacing); line-height: var(--typography-Subtitle-2-lineHeight); } @@ -62,3 +62,7 @@ .uiTextHighContrast { color: var(--UI-Text-High-contrast); } + +.uiTextMediumContrast { + color: var(--UI-Text-Medium-contrast); +} diff --git a/components/TempDesignSystem/Text/Subtitle/variants.ts b/components/TempDesignSystem/Text/Subtitle/variants.ts index 7a8faa54c..3e36d39e0 100644 --- a/components/TempDesignSystem/Text/Subtitle/variants.ts +++ b/components/TempDesignSystem/Text/Subtitle/variants.ts @@ -9,6 +9,7 @@ const config = { burgundy: styles.burgundy, pale: styles.pale, uiTextHighContrast: styles.uiTextHighContrast, + uiTextMediumContrast: styles.uiTextMediumContrast, }, textAlign: { center: styles.center, diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index a260dd068..76237d5a5 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -13,13 +13,12 @@ "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", "Amusement park": "Forlystelsespark", - "An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.", "An error occurred trying to manage your preferences, please try again later.": "Der opstod en fejl under forsøget på at administrere dine præferencer. Prøv venligst igen senere.", + "An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.", "An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.", "Any changes you've made will be lost.": "Alle ændringer, du har foretaget, går tabt.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?", "Arrival date": "Ankomstdato", - "as of today": "pr. dags dato", "As our": "Som vores {level}", "As our Close Friend": "Som vores nære ven", "At latest": "Senest", @@ -35,9 +34,9 @@ "Breakfast buffet": "Morgenbuffet", "Breakfast excluded": "Morgenmad ikke inkluderet", "Breakfast included": "Morgenmad inkluderet", + "Breakfast restaurant": "Breakfast restaurant", "Bus terminal": "Busstation", "Business": "Forretning", - "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Afbestille", "Check in": "Check ind", "Check out": "Check ud", @@ -83,9 +82,9 @@ "Edit profile": "Rediger profil", "Email": "E-mail", "Email address": "E-mailadresse", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Indtast destination eller hotel", "Enter your details": "Indtast dine oplysninger", - "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Udforsk alle niveauer og fordele", "Explore nearby": "Udforsk i nærheden", @@ -121,7 +120,6 @@ "Join Scandic Friends": "Tilmeld dig Scandic Friends", "Join at no cost": "Tilmeld dig uden omkostninger", "King bed": "Kingsize-seng", - "km to city center": "km til byens centrum", "Language": "Sprog", "Lastname": "Efternavn", "Latest searches": "Seneste søgninger", @@ -209,11 +207,11 @@ "Read more about the hotel": "Læs mere om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", + "Request bedtype": "Anmod om sengetype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Gentag den nye adgangskode", - "Request bedtype": "Anmod om sengetype", "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", "Rooms": "Værelser", @@ -299,11 +297,12 @@ "Your details": "Dine oplysninger", "Your level": "Dit niveau", "Your points to spend": "Dine brugbare point", + "Your room": "Dit værelse", "Zip code": "Postnummer", "Zoo": "Zoo", "Zoom in": "Zoom ind", "Zoom out": "Zoom ud", - "as of today": "fra idag", + "as of today": "pr. dags dato", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index ad755efdc..742890d7c 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -207,11 +207,11 @@ "Read more about the hotel": "Lesen Sie mehr über das Hotel", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen", + "Request bedtype": "Bettentyp anfragen", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Neues Passwort erneut eingeben", - "Request bedtype": "Bettentyp anfragen", "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", "Rooms": "Räume", @@ -297,6 +297,7 @@ "Your details": "Ihre Angaben", "Your level": "Dein level", "Your points to spend": "Meine Punkte", + "Your room": "Ihr Zimmer", "Zip code": "PLZ", "Zoo": "Zoo", "Zoom in": "Vergrößern", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 43903c4c2..fa28c7399 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -120,7 +120,6 @@ "Join Scandic Friends": "Join Scandic Friends", "Join at no cost": "Join at no cost", "King bed": "King bed", - "km to city center": "km to city center", "Language": "Language", "Lastname": "Lastname", "Latest searches": "Latest searches", @@ -208,6 +207,7 @@ "Read more about the hotel": "Read more about the hotel", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Remove card from member profile", + "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", @@ -226,10 +226,12 @@ "See room details": "See room details", "See rooms": "See rooms", "Select a country": "Select a country", + "Select breakfast options": "Select breakfast options", "Select country of residence": "Select country of residence", "Select date of birth": "Select date of birth", "Select dates": "Select dates", "Select language": "Select language", + "Select payment method": "Select payment method", "Select your language": "Select your language", "Shopping": "Shopping", "Shopping & Dining": "Shopping & Dining", @@ -295,6 +297,7 @@ "Your details": "Your details", "Your level": "Your level", "Your points to spend": "Your points to spend", + "Your room": "Your room", "Zip code": "Zip code", "Zoo": "Zoo", "Zoom in": "Zoom in", @@ -315,9 +318,6 @@ "number": "number", "or": "or", "points": "Points", - "Request bedtype": "Request bedtype", - "Select breakfast options": "Select breakfast options", - "Select payment method": "Select payment method", "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 558c99dcb..ec0b1ee0c 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -13,8 +13,8 @@ "Already a friend?": "Oletko jo ystävä?", "Amenities": "Mukavuudet", "Amusement park": "Huvipuisto", - "An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.", "An error occurred trying to manage your preferences, please try again later.": "Asetusten hallinnassa tapahtui virhe. Yritä myöhemmin uudelleen.", + "An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.", "An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.", "Any changes you've made will be lost.": "Kaikki tekemäsi muutokset menetetään.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?", @@ -34,9 +34,9 @@ "Breakfast buffet": "Aamiaisbuffet", "Breakfast excluded": "Aamiainen ei sisälly", "Breakfast included": "Aamiainen sisältyy", + "Breakfast restaurant": "Breakfast restaurant", "Bus terminal": "Bussiasema", "Business": "Business", - "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Peruuttaa", "Check in": "Sisäänkirjautuminen", "Check out": "Uloskirjautuminen", @@ -82,9 +82,9 @@ "Edit profile": "Muokkaa profiilia", "Email": "Sähköposti", "Email address": "Sähköpostiosoite", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Anna kohde tai hotelli", "Enter your details": "Anna tietosi", - "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "Explore nearby": "Tutustu lähialueeseen", @@ -120,7 +120,6 @@ "Join Scandic Friends": "Liity jäseneksi", "Join at no cost": "Liity maksutta", "King bed": "King-vuode", - "km to city center": "km keskustaan", "Language": "Kieli", "Lastname": "Sukunimi", "Latest searches": "Viimeisimmät haut", @@ -208,6 +207,7 @@ "Read more about the hotel": "Lue lisää hotellista", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Poista kortti jäsenprofiilista", + "Request bedtype": "Pyydä sänkytyyppiä", "Restaurant": "{count, plural, one {#Ravintola} other {#Restaurants}}", "Restaurant & Bar": "Ravintola & Baari", "Restaurants & Bars": "Restaurants & Bars", @@ -217,7 +217,6 @@ "Rooms": "Huoneet", "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", - "Request bedtype": "Pyydä sänkytyyppiä", "Sauna and gym": "Sauna and gym", "Save": "Tallenna", "Scandic Friends Mastercard": "Scandic Friends Mastercard", @@ -299,6 +298,7 @@ "Your details": "Tietosi", "Your level": "Tasosi", "Your points to spend": "Käytettävissä olevat pisteesi", + "Your room": "Sinun huoneesi", "Zip code": "Postinumero", "Zoo": "Eläintarha", "Zoom in": "Lähennä", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 5db6252f3..4a7ab2908 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -13,13 +13,12 @@ "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", "Amusement park": "Tivoli", - "An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.", "An error occurred trying to manage your preferences, please try again later.": "Det oppstod en feil under forsøket på å administrere innstillingene dine. Prøv igjen senere.", + "An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.", "An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.", "Any changes you've made will be lost.": "Eventuelle endringer du har gjort, går tapt.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?", "Arrival date": "Ankomstdato", - "as of today": "per i dag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nære venn", "At latest": "Senest", @@ -82,16 +81,16 @@ "Edit profile": "Rediger profil", "Email": "E-post", "Email address": "E-postadresse", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Skriv inn destinasjon eller hotell", "Enter your details": "Skriv inn detaljene dine", - "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Utforsk alle nivåer og fordeler", "Explore nearby": "Utforsk i nærheten", "Extras to your booking": "Tilvalg til bestillingen din", + "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Fair": "Messe", - "FAQ": "FAQ", "Find booking": "Finn booking", "Find hotels": "Finn hotell", "Firstname": "Fornavn", @@ -120,7 +119,6 @@ "Join Scandic Friends": "Bli med i Scandic Friends", "Join at no cost": "Bli med uten kostnad", "King bed": "King-size-seng", - "km to city center": "km til sentrum", "Language": "Språk", "Lastname": "Etternavn", "Latest searches": "Siste søk", @@ -208,11 +206,11 @@ "Read more about the hotel": "Les mer om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", + "Request bedtype": "Be om sengetype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Skriv inn nytt passord på nytt", - "Request bedtype": "Be om sengetype", "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", "Rooms": "Rom", @@ -298,11 +296,12 @@ "Your details": "Dine detaljer", "Your level": "Ditt nivå", "Your points to spend": "Dine brukbare poeng", + "Your room": "Rommet ditt", "Zip code": "Post kode", "Zoo": "Dyrehage", "Zoom in": "Zoom inn", "Zoom out": "Zoom ut", - "as of today": "per idag", + "as of today": "per i dag", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index e7b55e6f6..28c7a9889 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -13,13 +13,12 @@ "Already a friend?": "Är du redan en vän?", "Amenities": "Bekvämligheter", "Amusement park": "Nöjespark", - "An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.", "An error occurred trying to manage your preferences, please try again later.": "Ett fel uppstod när du försökte hantera dina inställningar, försök igen senare.", + "An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.", "An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.", "Any changes you've made will be lost.": "Alla ändringar du har gjort kommer att gå förlorade.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?", "Arrival date": "Ankomstdatum", - "as of today": "per idag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nära vän", "At latest": "Senast", @@ -35,9 +34,9 @@ "Breakfast buffet": "Frukostbuffé", "Breakfast excluded": "Frukost ingår ej", "Breakfast included": "Frukost ingår", + "Breakfast restaurant": "Breakfast restaurant", "Bus terminal": "Bussterminal", "Business": "Business", - "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Avbryt", "Check in": "Checka in", "Check out": "Checka ut", @@ -83,9 +82,9 @@ "Edit profile": "Redigera profil", "Email": "E-post", "Email address": "E-postadress", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Ange destination eller hotell", "Enter your details": "Ange dina uppgifter", - "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Utforska alla nivåer och fördelar", "Explore nearby": "Utforska i närheten", @@ -118,11 +117,9 @@ "How it works": "Hur det fungerar", "Image gallery": "Bildgalleri", "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", "King bed": "King size-säng", - "km to city center": "km till stadens centrum", "Language": "Språk", "Lastname": "Efternamn", "Latest searches": "Senaste sökningarna", @@ -210,11 +207,11 @@ "Read more about the hotel": "Läs mer om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Ta bort kortet från medlemsprofilen", + "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurang} other {#Restauranger}}", "Restaurant & Bar": "Restaurang & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Upprepa nytt lösenord", - "Request bedtype": "Request bedtype", "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", "Rooms": "Rum", @@ -266,7 +263,6 @@ "Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)", "Type of bed": "Sängtyp", "Type of room": "Rumstyp", - "uppercase letter": "stor bokstav", "Use bonus cheque": "Använd bonuscheck", "Use code/voucher": "Använd kod/voucher", "User information": "Användarinformation", @@ -301,6 +297,7 @@ "Your details": "Dina uppgifter", "Your level": "Din nivå", "Your points to spend": "Dina spenderbara poäng", + "Your room": "Ditt rum", "Zip code": "Postnummer", "Zoo": "Djurpark", "Zoom in": "Zooma in", From 71d08be154c0d2fc2ea678d95207021e1fdd17b3 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 9 Oct 2024 11:35:10 +0200 Subject: [PATCH 06/10] style(SW-337): hotel page ui updates --- components/ContentType/HotelPage/AmenitiesList/index.tsx | 2 +- components/ContentType/HotelPage/IntroSection/index.tsx | 3 ++- .../ContentType/HotelPage/Rooms/RoomCard/index.tsx | 1 + .../HotelPage/Rooms/RoomCard/roomCard.module.css | 9 +++++++++ components/Lightbox/Lightbox.module.css | 1 + 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/components/ContentType/HotelPage/AmenitiesList/index.tsx b/components/ContentType/HotelPage/AmenitiesList/index.tsx index 27448827d..8b6a1977e 100644 --- a/components/ContentType/HotelPage/AmenitiesList/index.tsx +++ b/components/ContentType/HotelPage/AmenitiesList/index.tsx @@ -44,7 +44,7 @@ export default async function AmenitiesList({ className={styles.showAllAmenities} > {intl.formatMessage({ id: "Show all amenities" })} - + ) diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index cb62ca01c..f6118a2fc 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -1,5 +1,6 @@ import { about } from "@/constants/routes/hotelPageParams" +import { ChevronRightIcon } from "@/components/Icons" import ArrowRight from "@/components/Icons/ArrowRight" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" import Link from "@/components/TempDesignSystem/Link" @@ -76,7 +77,7 @@ export default async function IntroSection({ scroll={false} > {intl.formatMessage({ id: "Read more about the hotel" })} - +
diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index 967580157..35f671fe5 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -62,6 +62,7 @@ export function RoomCard({ textAlign="center" type="one" color="black" + className={styles.title} > {title} diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css index 5fd955df6..fcec738e4 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css +++ b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css @@ -45,6 +45,15 @@ gap: var(--Spacing-x1); } +.title { + display: flex; + align-items: center; +} + +.title:first-child { + height: 2em; +} + .imageWrapper { position: relative; background-color: transparent; diff --git a/components/Lightbox/Lightbox.module.css b/components/Lightbox/Lightbox.module.css index f4b6611ba..f5446a2aa 100644 --- a/components/Lightbox/Lightbox.module.css +++ b/components/Lightbox/Lightbox.module.css @@ -197,6 +197,7 @@ .desktopGallery { display: grid; grid-template-rows: 1.71875rem 1fr 7.8125rem; + row-gap: var(--Spacing-x-one-and-half); background-color: var(--Base-Background-Primary-Normal); height: 100%; position: relative; From 2ea0adbf9860b6a4911514d4495028ddfb7c743e Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 1 Oct 2024 14:48:28 +0200 Subject: [PATCH 07/10] fix: handle cache tags for service tokens --- server/tokenManager.ts | 65 +++++++++++++++++++++++++------------ server/trpc.ts | 18 +++++++--- types/enums/serviceToken.ts | 7 ++++ utils/generateTag.ts | 10 ++++++ 4 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 types/enums/serviceToken.ts diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 4007896d6..4681721e3 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -1,35 +1,58 @@ +import { revalidateTag, unstable_cache } from "next/cache" + import { env } from "@/env/server" +import { generateServiceTokenTag } from "@/utils/generateTag" + +import { ServiceTokenScope } from "@/types/enums/serviceToken" import { ServiceTokenResponse } from "@/types/tokens" -const SERVICE_TOKEN_REVALIDATE_SECONDS = 3599 // 59 minutes and 59 seconds. +async function getServiceToken(scopes: ServiceTokenScope[]) { + const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "client_credentials", + client_id: env.CURITY_CLIENT_ID_SERVICE, + client_secret: env.CURITY_CLIENT_SECRET_SERVICE, + scope: scopes.join(" "), + }), + }) + + if (!response.ok) { + throw new Error("Failed to obtain service token") + } + + return response.json() +} export async function fetchServiceToken( - scopes: string[] + scopes: ServiceTokenScope[] ): Promise { try { - const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "client_credentials", - client_id: env.CURITY_CLIENT_ID_SERVICE, - client_secret: env.CURITY_CLIENT_SECRET_SERVICE, - scope: scopes.join(" "), - }), - next: { - revalidate: SERVICE_TOKEN_REVALIDATE_SECONDS, - }, - }) + const tag = generateServiceTokenTag(scopes) + const getCachedJwt = unstable_cache( + async (scopes) => { + const jwt = await getServiceToken(scopes) - if (!response.ok) { - throw new Error("Failed to obtain service token") + const expiresAt = Date.now() + jwt.expires_in * 1000 + return { expiresAt, jwt } + }, + scopes, + { tags: [tag] } + ) + + const cachedJwt = await getCachedJwt(scopes) + if (cachedJwt.expiresAt < Date.now()) { + revalidateTag(tag) + const newToken = await getServiceToken(scopes) + return newToken } - return response.json() + return cachedJwt.jwt } catch (error) { console.error("Error fetching service token:", error) throw error diff --git a/server/trpc.ts b/server/trpc.ts index e8f9c26f1..2ea6a871e 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -17,6 +17,10 @@ import { langInput } from "./utils" import type { Session } from "next-auth" +import { + ServiceTokenScope, + ServiceTokenScopeEnum, +} from "@/types/enums/serviceToken" import type { Meta } from "@/types/trpc/meta" const t = initTRPC @@ -121,7 +125,7 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) { }) }) -function createServiceProcedure(serviceName: string) { +function createServiceProcedure(serviceName: ServiceTokenScope) { return t.procedure.use(async (opts) => { const { access_token } = await fetchServiceToken([serviceName]) if (!access_token) { @@ -135,9 +139,15 @@ function createServiceProcedure(serviceName: string) { }) } -export const bookingServiceProcedure = createServiceProcedure("booking") -export const hotelServiceProcedure = createServiceProcedure("hotel") -export const profileServiceProcedure = createServiceProcedure("profile") +export const bookingServiceProcedure = createServiceProcedure( + ServiceTokenScopeEnum.booking +) +export const hotelServiceProcedure = createServiceProcedure( + ServiceTokenScopeEnum.hotel +) +export const profileServiceProcedure = createServiceProcedure( + ServiceTokenScopeEnum.profile +) export const serverActionProcedure = t.procedure.experimental_caller( experimental_nextAppDirCaller({ diff --git a/types/enums/serviceToken.ts b/types/enums/serviceToken.ts new file mode 100644 index 000000000..4efbe1218 --- /dev/null +++ b/types/enums/serviceToken.ts @@ -0,0 +1,7 @@ +export enum ServiceTokenScopeEnum { + profile = "profile", + hotel = "hotel", + booking = "booking", +} + +export type ServiceTokenScope = keyof typeof ServiceTokenScopeEnum diff --git a/utils/generateTag.ts b/utils/generateTag.ts index 08f2b404a..41251245c 100644 --- a/utils/generateTag.ts +++ b/utils/generateTag.ts @@ -99,3 +99,13 @@ export function generateLoyaltyConfigTag( ) { return `${lang}:loyalty_config:${contentTypeUid}:${id}` } + +/** + * Function to generate tags for service tokens + * + * @param serviceTokenScope scope of service token + * @returns string + */ +export function generateServiceTokenTag(serviceTokenScopes: string[]) { + return `service_token:${serviceTokenScopes.join("-")}` +} From 71b03143ce8d9244b0e49eb19270d32b646e8d27 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Mon, 7 Oct 2024 16:48:23 +0200 Subject: [PATCH 08/10] fix: refactor scopes for service token --- server/routers/booking/mutation.ts | 4 +- server/routers/booking/query.ts | 117 ++++++++++---------- server/routers/contentstack/reward/query.ts | 6 +- server/routers/hotels/query.ts | 14 +-- server/routers/user/query.ts | 1 - server/tokenManager.ts | 50 +++++++-- server/trpc.ts | 40 +++---- utils/generateTag.ts | 6 +- 8 files changed, 128 insertions(+), 110 deletions(-) diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 53595b2d0..1ff3422af 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 { bookingServiceProcedure, router } from "@/server/trpc" +import { router, serviceProcedure } from "@/server/trpc" import { getMembership } from "@/utils/user" @@ -36,7 +36,7 @@ async function getMembershipNumber( export const bookingMutationRouter = router({ booking: router({ - create: bookingServiceProcedure + create: serviceProcedure .input(createBookingInput) .mutation(async function ({ ctx, input }) { const { checkInDate, checkOutDate, hotelId } = input diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index f7f439b90..d053782bd 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api" import * as api from "@/lib/api" import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" -import { bookingServiceProcedure, router } from "@/server/trpc" +import { router, serviceProcedure } from "@/server/trpc" import { getBookingStatusInput } from "./input" import { createBookingSchema } from "./output" @@ -17,69 +17,70 @@ const getBookingStatusFailCounter = meter.createCounter( ) export const bookingQueryRouter = router({ - status: bookingServiceProcedure - .input(getBookingStatusInput) - .query(async function ({ ctx, input }) { - const { confirmationNumber } = input - getBookingStatusCounter.add(1, { confirmationNumber }) + status: serviceProcedure.input(getBookingStatusInput).query(async function ({ + ctx, + input, + }) { + const { confirmationNumber } = input + getBookingStatusCounter.add(1, { confirmationNumber }) - const apiResponse = await api.get( - `${api.endpoints.v1.booking}/${confirmationNumber}/status`, - { - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - } - ) - - if (!apiResponse.ok) { - const responseMessage = await apiResponse.text() - getBookingStatusFailCounter.add(1, { - confirmationNumber, - error_type: "http_error", - error: responseMessage, - }) - console.error( - "api.booking.status error", - JSON.stringify({ - query: { confirmationNumber }, - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - text: responseMessage, - }, - }) - ) - - throw serverErrorByStatus(apiResponse.status, apiResponse) + const apiResponse = await api.get( + `${api.endpoints.v1.booking}/${confirmationNumber}/status`, + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, } + ) - const apiJson = await apiResponse.json() - const verifiedData = createBookingSchema.safeParse(apiJson) - if (!verifiedData.success) { - getBookingStatusFailCounter.add(1, { - confirmationNumber, - error_type: "validation_error", - error: JSON.stringify(verifiedData.error), - }) - console.error( - "api.booking.status validation error", - JSON.stringify({ - query: { confirmationNumber }, - error: verifiedData.error, - }) - ) - throw badRequestError() - } - - getBookingStatusSuccessCounter.add(1, { confirmationNumber }) - console.info( - "api.booking.status success", + if (!apiResponse.ok) { + const responseMessage = await apiResponse.text() + getBookingStatusFailCounter.add(1, { + confirmationNumber, + error_type: "http_error", + error: responseMessage, + }) + console.error( + "api.booking.status error", JSON.stringify({ query: { confirmationNumber }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text: responseMessage, + }, }) ) - return verifiedData.data - }), + throw serverErrorByStatus(apiResponse.status, apiResponse) + } + + const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + getBookingStatusFailCounter.add(1, { + confirmationNumber, + error_type: "validation_error", + error: JSON.stringify(verifiedData.error), + }) + console.error( + "api.booking.status validation error", + JSON.stringify({ + query: { confirmationNumber }, + error: verifiedData.error, + }) + ) + throw badRequestError() + } + + getBookingStatusSuccessCounter.add(1, { confirmationNumber }) + console.info( + "api.booking.status success", + JSON.stringify({ + query: { confirmationNumber }, + }) + ) + + return verifiedData.data + }), }) diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 543cd46cc..ea6023fee 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -7,8 +7,8 @@ import { request } from "@/lib/graphql/request" import { Context } from "@/server/context" import { notFound } from "@/server/errors/trpc" import { - contentStackBaseWithProfileServiceProcedure, contentStackBaseWithProtectedProcedure, + contentStackBaseWithServiceProcedure, router, } from "@/server/trpc" @@ -260,7 +260,7 @@ export const rewardQueryRouter = router({ nextCursor, } }), - byLevel: contentStackBaseWithProfileServiceProcedure + byLevel: contentStackBaseWithServiceProcedure .input(rewardsByLevelInput) .query(async function ({ input, ctx }) { getByLevelRewardCounter.add(1) @@ -310,7 +310,7 @@ export const rewardQueryRouter = router({ getByLevelRewardSuccessCounter.add(1) return { level: loyaltyLevelsConfig, rewards: levelsWithRewards } }), - all: contentStackBaseWithProfileServiceProcedure + all: contentStackBaseWithServiceProcedure .input(rewardsAllInput) .query(async function ({ input, ctx }) { getAllRewardCounter.add(1) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 859e5662c..8ce752577 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -11,10 +11,10 @@ import { } from "@/server/errors/trpc" import { extractHotelImages } from "@/server/routers/utils/hotels" import { - contentStackUidWithHotelServiceProcedure, - hotelServiceProcedure, + contentStackUidWithServiceProcedure, publicProcedure, router, + serviceProcedure, } from "@/server/trpc" import { toApiLang } from "@/server/utils" @@ -99,7 +99,7 @@ async function getContentstackData( } export const hotelQueryRouter = router({ - get: contentStackUidWithHotelServiceProcedure + get: contentStackUidWithServiceProcedure .input(getHotelInputSchema) .query(async ({ ctx, input }) => { const { lang, uid } = ctx @@ -264,7 +264,7 @@ export const hotelQueryRouter = router({ } }), availability: router({ - hotels: hotelServiceProcedure + hotels: serviceProcedure .input(getHotelsAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { @@ -388,7 +388,7 @@ export const hotelQueryRouter = router({ .flatMap((hotels) => hotels.attributes), } }), - rooms: hotelServiceProcedure + rooms: serviceProcedure .input(getRoomsAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { @@ -543,7 +543,7 @@ export const hotelQueryRouter = router({ }), }), hotelData: router({ - get: hotelServiceProcedure + get: serviceProcedure .input(getlHotelDataInputSchema) .query(async ({ ctx, input }) => { const { hotelId, language, include } = input @@ -641,7 +641,7 @@ export const hotelQueryRouter = router({ }), }), locations: router({ - get: hotelServiceProcedure.query(async function ({ ctx }) { + get: serviceProcedure.query(async function ({ ctx }) { const searchParams = new URLSearchParams() searchParams.set("language", toApiLang(ctx.lang)) diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 28dee9cd2..df900953c 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -1,5 +1,4 @@ import { metrics } from "@opentelemetry/api" -import { SafeParseSuccess } from "zod" import * as api from "@/lib/api" import { diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 4681721e3..6df7def66 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -1,13 +1,28 @@ +import { metrics } from "@opentelemetry/api" import { revalidateTag, unstable_cache } from "next/cache" import { env } from "@/env/server" import { generateServiceTokenTag } from "@/utils/generateTag" -import { ServiceTokenScope } from "@/types/enums/serviceToken" +import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken" import { ServiceTokenResponse } from "@/types/tokens" -async function getServiceToken(scopes: ServiceTokenScope[]) { +// OpenTelemetry metrics: Service token +const meter = metrics.getMeter("trpc.context.serviceToken") +const getServiceTokenCounter = meter.createCounter( + "trpc.context.serviceToken.get-new-token" +) +const getTempServiceTokenCounter = meter.createCounter( + "trpc.context.serviceToken.get-temporary" +) +const getServiceTokenFailCounter = meter.createCounter( + "trpc.context.serviceToken.get-fail" +) + +async function getServiceToken() { + getServiceTokenCounter.add(1) + const scopes = Object.keys(ServiceTokenScopeEnum) const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, { method: "POST", headers: { @@ -23,32 +38,45 @@ async function getServiceToken(scopes: ServiceTokenScope[]) { }) if (!response.ok) { + getServiceTokenFailCounter.add(1, { + error_type: "http_error", + error: JSON.stringify({ + status: response.status, + statusText: response.statusText, + }), + }) throw new Error("Failed to obtain service token") } return response.json() } -export async function fetchServiceToken( - scopes: ServiceTokenScope[] -): Promise { +export async function fetchServiceToken(): Promise { try { - const tag = generateServiceTokenTag(scopes) + const tag = generateServiceTokenTag() const getCachedJwt = unstable_cache( - async (scopes) => { - const jwt = await getServiceToken(scopes) + async () => { + const jwt = await getServiceToken() const expiresAt = Date.now() + jwt.expires_in * 1000 return { expiresAt, jwt } }, - scopes, + [], { tags: [tag] } ) - const cachedJwt = await getCachedJwt(scopes) + const cachedJwt = await getCachedJwt() if (cachedJwt.expiresAt < Date.now()) { + console.log( + "trpc.context.serviceToken: Service token expired, revalidating tag" + ) revalidateTag(tag) - const newToken = await getServiceToken(scopes) + + console.log( + "trpc.context.serviceToken: Fetching new temporary service token." + ) + getTempServiceTokenCounter.add(1) + const newToken = await getServiceToken() return newToken } diff --git a/server/trpc.ts b/server/trpc.ts index 2ea6a871e..94d4e47b5 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -125,29 +125,17 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) { }) }) -function createServiceProcedure(serviceName: ServiceTokenScope) { - return t.procedure.use(async (opts) => { - const { access_token } = await fetchServiceToken([serviceName]) - if (!access_token) { - throw internalServerError(`Failed to obtain ${serviceName} service token`) - } - return opts.next({ - ctx: { - serviceToken: access_token, - }, - }) +export const serviceProcedure = t.procedure.use(async (opts) => { + const { access_token } = await fetchServiceToken() + if (!access_token) { + throw internalServerError(`Failed to obtain service token`) + } + return opts.next({ + ctx: { + serviceToken: access_token, + }, }) -} - -export const bookingServiceProcedure = createServiceProcedure( - ServiceTokenScopeEnum.booking -) -export const hotelServiceProcedure = createServiceProcedure( - ServiceTokenScopeEnum.hotel -) -export const profileServiceProcedure = createServiceProcedure( - ServiceTokenScopeEnum.profile -) +}) export const serverActionProcedure = t.procedure.experimental_caller( experimental_nextAppDirCaller({ @@ -178,11 +166,11 @@ export const protectedServerActionProcedure = serverActionProcedure.use( // NOTE: This is actually save to use, just the implementation could change // in minor version bumps. Please read: https://trpc.io/docs/faq#unstable -export const contentStackUidWithHotelServiceProcedure = - contentstackExtendedProcedureUID.unstable_concat(hotelServiceProcedure) +export const contentStackUidWithServiceProcedure = + contentstackExtendedProcedureUID.unstable_concat(serviceProcedure) -export const contentStackBaseWithProfileServiceProcedure = - contentstackBaseProcedure.unstable_concat(profileServiceProcedure) +export const contentStackBaseWithServiceProcedure = + contentstackBaseProcedure.unstable_concat(serviceProcedure) export const contentStackBaseWithProtectedProcedure = contentstackBaseProcedure.unstable_concat(protectedProcedure) diff --git a/utils/generateTag.ts b/utils/generateTag.ts index 41251245c..0dbf95d33 100644 --- a/utils/generateTag.ts +++ b/utils/generateTag.ts @@ -1,3 +1,4 @@ +import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken" import { System } from "@/types/requests/system" import type { Edges } from "@/types/requests/utils/edges" import type { NodeRefs } from "@/types/requests/utils/refs" @@ -106,6 +107,7 @@ export function generateLoyaltyConfigTag( * @param serviceTokenScope scope of service token * @returns string */ -export function generateServiceTokenTag(serviceTokenScopes: string[]) { - return `service_token:${serviceTokenScopes.join("-")}` +export function generateServiceTokenTag() { + const scopes = Object.keys(ServiceTokenScopeEnum).join("-") + return `service_token:${scopes}` } From 95ff011bdb0cffad8da6a530e91529e1e544239c Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 9 Oct 2024 11:27:16 +0200 Subject: [PATCH 09/10] fix: add args as keyParts for unstable_cache --- server/routers/hotels/query.ts | 29 +-- server/routers/hotels/utils.ts | 300 +++++++++++++++----------- server/tokenManager.ts | 37 ++-- server/trpc.ts | 8 +- types/enums/serviceToken.ts | 7 - types/trpc/routers/hotel/locations.ts | 3 + utils/generateTag.ts | 6 +- 7 files changed, 204 insertions(+), 186 deletions(-) delete mode 100644 types/enums/serviceToken.ts diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 8ce752577..f0c5e1ef8 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -1,5 +1,4 @@ import { metrics } from "@opentelemetry/api" -import { unstable_cache } from "next/cache" import * as api from "@/lib/api" import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql" @@ -38,7 +37,6 @@ import { getCitiesByCountry, getCountries, getLocations, - locationsAffix, TWENTYFOUR_HOURS, } from "./utils" @@ -657,36 +655,19 @@ export const hotelQueryRouter = router({ }, } - const getCachedCountries = unstable_cache( - getCountries, - [`${ctx.lang}:${locationsAffix}:countries`], - { revalidate: TWENTYFOUR_HOURS } - ) - - const countries = await getCachedCountries(options, searchParams) - - const getCachedCitiesByCountry = unstable_cache( - getCitiesByCountry, - [`${ctx.lang}:${locationsAffix}:cities-by-country`], - { revalidate: TWENTYFOUR_HOURS } - ) + const countries = await getCountries(options, searchParams, ctx.lang) let citiesByCountry = null if (countries) { - citiesByCountry = await getCachedCitiesByCountry( + citiesByCountry = await getCitiesByCountry( countries, options, - searchParams + searchParams, + ctx.lang ) } - const getCachedLocations = unstable_cache( - getLocations, - [`${ctx.lang}:${locationsAffix}`], - { revalidate: TWENTYFOUR_HOURS } - ) - - const locations = await getCachedLocations( + const locations = await getLocations( ctx.lang, options, searchParams, diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts index d19d1a492..84020bfaa 100644 --- a/server/routers/hotels/utils.ts +++ b/server/routers/hotels/utils.ts @@ -17,6 +17,7 @@ import { PointOfInterestCategoryNameEnum, PointOfInterestGroupEnum, } from "@/types/hotel" +import { HotelLocation } from "@/types/trpc/routers/hotel/locations" import type { Lang } from "@/constants/languages" import type { Endpoint } from "@/lib/api/endpoints" @@ -54,89 +55,119 @@ export const locationsAffix = "locations" export const TWENTYFOUR_HOURS = 60 * 60 * 24 export async function getCity( cityUrl: string, - options: RequestOptionsWithOutBody + options: RequestOptionsWithOutBody, + lang: Lang, + relationshipCity: HotelLocation["relationships"]["city"] ) { - const url = new URL(cityUrl) - const cityResponse = await api.get( - url.pathname as Endpoint, - options, - url.searchParams - ) + return unstable_cache( + async function (locationCityUrl: string) { + const url = new URL(locationCityUrl) + const cityResponse = await api.get( + url.pathname as Endpoint, + options, + url.searchParams + ) - if (!cityResponse.ok) { - return null - } + if (!cityResponse.ok) { + return null + } - const cityJson = await cityResponse.json() - const city = apiCitySchema.safeParse(cityJson) - if (!city.success) { - console.info(`Validation of city failed`) - console.info(`cityUrl: ${cityUrl}`) - console.error(city.error) - return null - } + const cityJson = await cityResponse.json() + const city = apiCitySchema.safeParse(cityJson) + if (!city.success) { + console.info(`Validation of city failed`) + console.info(`cityUrl: ${locationCityUrl}`) + console.error(city.error) + return null + } - return city.data + return city.data + }, + [cityUrl, `${lang}:${relationshipCity}`], + { revalidate: TWENTYFOUR_HOURS } + )(cityUrl) } export async function getCountries( options: RequestOptionsWithOutBody, - params: URLSearchParams + params: URLSearchParams, + lang: Lang ) { - const countryResponse = await api.get( - api.endpoints.v1.countries, - options, - params - ) - - if (!countryResponse.ok) { - return null - } - - const countriesJson = await countryResponse.json() - const countries = apiCountriesSchema.safeParse(countriesJson) - if (!countries.success) { - console.info(`Validation for countries failed`) - console.error(countries.error) - return null - } - - return countries.data -} - -export async function getCitiesByCountry( - countries: Countries, - options: RequestOptionsWithOutBody, - params: URLSearchParams -) { - const citiesGroupedByCountry: CitiesGroupedByCountry = {} - - await Promise.all( - countries.data.map(async (country) => { + return unstable_cache( + async function (searchParams) { const countryResponse = await api.get( - `${api.endpoints.v1.citiesCountry}/${country.name}`, + api.endpoints.v1.countries, options, - params + searchParams ) if (!countryResponse.ok) { return null } - const countryJson = await countryResponse.json() - const citiesByCountry = apiCitiesByCountrySchema.safeParse(countryJson) - if (!citiesByCountry.success) { - console.info(`Failed to validate Cities by Country payload`) - console.error(citiesByCountry.error) + const countriesJson = await countryResponse.json() + const countries = apiCountriesSchema.safeParse(countriesJson) + if (!countries.success) { + console.info(`Validation for countries failed`) + console.error(countries.error) return null } - citiesGroupedByCountry[country.name] = citiesByCountry.data.data - return true - }) - ) + return countries.data + }, + [`${lang}:${locationsAffix}:countries`, params.toString()], + { revalidate: TWENTYFOUR_HOURS } + )(params) +} - return citiesGroupedByCountry +export async function getCitiesByCountry( + countries: Countries, + options: RequestOptionsWithOutBody, + params: URLSearchParams, + lang: Lang +) { + return unstable_cache( + async function ( + searchParams: URLSearchParams, + searchedCountries: Countries + ) { + const citiesGroupedByCountry: CitiesGroupedByCountry = {} + + await Promise.all( + searchedCountries.data.map(async (country) => { + const countryResponse = await api.get( + `${api.endpoints.v1.citiesCountry}/${country.name}`, + options, + searchParams + ) + + if (!countryResponse.ok) { + return null + } + + const countryJson = await countryResponse.json() + const citiesByCountry = + apiCitiesByCountrySchema.safeParse(countryJson) + if (!citiesByCountry.success) { + console.info(`Failed to validate Cities by Country payload`) + console.error(citiesByCountry.error) + return null + } + + citiesGroupedByCountry[country.name] = citiesByCountry.data.data + return true + }) + ) + + return citiesGroupedByCountry + }, + [ + `${lang}:${locationsAffix}:cities-by-country`, + params.toString(), + JSON.stringify(countries), + ], + { revalidate: TWENTYFOUR_HOURS } + )(params, countries) } export async function getLocations( @@ -145,72 +176,89 @@ export async function getLocations( params: URLSearchParams, citiesByCountry: CitiesGroupedByCountry | null ) { - const apiResponse = await api.get(api.endpoints.v1.locations, options, params) + return unstable_cache( + async function ( + searchParams: URLSearchParams, + groupedCitiesByCountry: CitiesGroupedByCountry | null + ) { + const apiResponse = await api.get( + api.endpoints.v1.locations, + options, + searchParams + ) - if (!apiResponse.ok) { - if (apiResponse.status === 401) { - return { error: true, cause: "unauthorized" } as const - } else if (apiResponse.status === 403) { - return { error: true, cause: "forbidden" } as const - } - return null - } - - const apiJson = await apiResponse.json() - const verifiedLocations = apiLocationsSchema.safeParse(apiJson) - if (!verifiedLocations.success) { - console.info(`Locations Verification Failed`) - console.error(verifiedLocations.error) - return null - } - - return await Promise.all( - verifiedLocations.data.data.map(async (location) => { - if (location.type === "cities") { - if (citiesByCountry) { - const country = Object.keys(citiesByCountry).find((country) => { - if ( - citiesByCountry[country].find((loc) => loc.name === location.name) - ) { - return true - } - return false - }) - if (country) { - return { - ...location, - country, - } - } else { - console.info( - `Location cannot be found in any of the countries cities` - ) - console.info(location) - } - } - } else if (location.type === "hotels") { - if (location.relationships.city?.url) { - const getCachedCity = unstable_cache( - getCity, - [`${lang}:${location.relationships.city}`], - { revalidate: TWENTYFOUR_HOURS } - ) - - const city = await getCachedCity( - location.relationships.city.url, - options - ) - if (city) { - return deepmerge(location, { - relationships: { - city, - }, - }) - } + if (!apiResponse.ok) { + if (apiResponse.status === 401) { + return { error: true, cause: "unauthorized" } as const + } else if (apiResponse.status === 403) { + return { error: true, cause: "forbidden" } as const } + return null } - return location - }) - ) + const apiJson = await apiResponse.json() + const verifiedLocations = apiLocationsSchema.safeParse(apiJson) + if (!verifiedLocations.success) { + console.info(`Locations Verification Failed`) + console.error(verifiedLocations.error) + return null + } + + return await Promise.all( + verifiedLocations.data.data.map(async (location) => { + if (location.type === "cities") { + if (groupedCitiesByCountry) { + const country = Object.keys(groupedCitiesByCountry).find( + (country) => { + if ( + groupedCitiesByCountry[country].find( + (loc) => loc.name === location.name + ) + ) { + return true + } + return false + } + ) + if (country) { + return { + ...location, + country, + } + } else { + console.info( + `Location cannot be found in any of the countries cities` + ) + console.info(location) + } + } + } else if (location.type === "hotels") { + if (location.relationships.city?.url) { + const city = await getCity( + location.relationships.city.url, + options, + lang, + location.relationships.city + ) + if (city) { + return deepmerge(location, { + relationships: { + city, + }, + }) + } + } + } + + return location + }) + ) + }, + [ + `${lang}:${locationsAffix}`, + params.toString(), + JSON.stringify(citiesByCountry), + ], + { revalidate: TWENTYFOUR_HOURS } + )(params, citiesByCountry) } diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 6df7def66..2e6d82b04 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -5,24 +5,22 @@ import { env } from "@/env/server" import { generateServiceTokenTag } from "@/utils/generateTag" -import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken" import { ServiceTokenResponse } from "@/types/tokens" // OpenTelemetry metrics: Service token const meter = metrics.getMeter("trpc.context.serviceToken") -const getServiceTokenCounter = meter.createCounter( - "trpc.context.serviceToken.get-new-token" +const fetchServiceTokenCounter = meter.createCounter( + "trpc.context.serviceToken.fetch-new-token" ) -const getTempServiceTokenCounter = meter.createCounter( - "trpc.context.serviceToken.get-temporary" +const fetchTempServiceTokenCounter = meter.createCounter( + "trpc.context.serviceToken.fetch-temporary" ) -const getServiceTokenFailCounter = meter.createCounter( - "trpc.context.serviceToken.get-fail" +const fetchServiceTokenFailCounter = meter.createCounter( + "trpc.context.serviceToken.fetch-fail" ) -async function getServiceToken() { - getServiceTokenCounter.add(1) - const scopes = Object.keys(ServiceTokenScopeEnum) +async function fetchServiceToken(scopes: string[]) { + fetchServiceTokenCounter.add(1) const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, { method: "POST", headers: { @@ -38,7 +36,7 @@ async function getServiceToken() { }) if (!response.ok) { - getServiceTokenFailCounter.add(1, { + fetchServiceTokenFailCounter.add(1, { error_type: "http_error", error: JSON.stringify({ status: response.status, @@ -51,21 +49,22 @@ async function getServiceToken() { return response.json() } -export async function fetchServiceToken(): Promise { +export async function getServiceToken(): Promise { try { - const tag = generateServiceTokenTag() + const scopes = ["profile", "hotel", "booking"] + const tag = generateServiceTokenTag(scopes) const getCachedJwt = unstable_cache( - async () => { - const jwt = await getServiceToken() + async (scopes) => { + const jwt = await fetchServiceToken(scopes) const expiresAt = Date.now() + jwt.expires_in * 1000 return { expiresAt, jwt } }, - [], + [tag], { tags: [tag] } ) - const cachedJwt = await getCachedJwt() + const cachedJwt = await getCachedJwt(scopes) if (cachedJwt.expiresAt < Date.now()) { console.log( "trpc.context.serviceToken: Service token expired, revalidating tag" @@ -75,8 +74,8 @@ export async function fetchServiceToken(): Promise { console.log( "trpc.context.serviceToken: Fetching new temporary service token." ) - getTempServiceTokenCounter.add(1) - const newToken = await getServiceToken() + fetchTempServiceTokenCounter.add(1) + const newToken = await fetchServiceToken(scopes) return newToken } diff --git a/server/trpc.ts b/server/trpc.ts index 94d4e47b5..dafd26690 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -11,16 +11,12 @@ import { unauthorizedError, } from "./errors/trpc" import { type Context, createContext } from "./context" -import { fetchServiceToken } from "./tokenManager" +import { getServiceToken } from "./tokenManager" import { transformer } from "./transformer" import { langInput } from "./utils" import type { Session } from "next-auth" -import { - ServiceTokenScope, - ServiceTokenScopeEnum, -} from "@/types/enums/serviceToken" import type { Meta } from "@/types/trpc/meta" const t = initTRPC @@ -126,7 +122,7 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) { }) export const serviceProcedure = t.procedure.use(async (opts) => { - const { access_token } = await fetchServiceToken() + const { access_token } = await getServiceToken() if (!access_token) { throw internalServerError(`Failed to obtain service token`) } diff --git a/types/enums/serviceToken.ts b/types/enums/serviceToken.ts deleted file mode 100644 index 4efbe1218..000000000 --- a/types/enums/serviceToken.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ServiceTokenScopeEnum { - profile = "profile", - hotel = "hotel", - booking = "booking", -} - -export type ServiceTokenScope = keyof typeof ServiceTokenScopeEnum diff --git a/types/trpc/routers/hotel/locations.ts b/types/trpc/routers/hotel/locations.ts index 859aa8a03..8f2deed1e 100644 --- a/types/trpc/routers/hotel/locations.ts +++ b/types/trpc/routers/hotel/locations.ts @@ -6,3 +6,6 @@ export interface LocationSchema extends z.output {} export type Locations = LocationSchema["data"] export type Location = Locations[number] + +export type CityLocation = Location & { type: "cities" } +export type HotelLocation = Location & { type: "hotels" } diff --git a/utils/generateTag.ts b/utils/generateTag.ts index 0dbf95d33..b067f536e 100644 --- a/utils/generateTag.ts +++ b/utils/generateTag.ts @@ -1,4 +1,3 @@ -import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken" import { System } from "@/types/requests/system" import type { Edges } from "@/types/requests/utils/edges" import type { NodeRefs } from "@/types/requests/utils/refs" @@ -107,7 +106,6 @@ export function generateLoyaltyConfigTag( * @param serviceTokenScope scope of service token * @returns string */ -export function generateServiceTokenTag() { - const scopes = Object.keys(ServiceTokenScopeEnum).join("-") - return `service_token:${scopes}` +export function generateServiceTokenTag(scopes: string[]) { + return `service_token:${scopes.join("-")}` } From 491253d8662dc8de0bad9cc10ae8e5af4c38f139 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 9 Oct 2024 15:20:00 +0200 Subject: [PATCH 10/10] fix: wrap tier rewards endpoint in unstable_cache --- server/routers/contentstack/reward/query.ts | 98 +++++++++++---------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index ea6023fee..f6af86315 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -1,10 +1,10 @@ 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 { Context } from "@/server/context" import { notFound } from "@/server/errors/trpc" import { contentStackBaseWithProtectedProcedure, @@ -62,69 +62,71 @@ 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) } -async function getAllApiRewards(ctx: Context & { serviceToken: string }) { - const apiResponse = await api.get(api.endpoints.v1.tierRewards, { - cache: undefined, // override defaultOptions - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - // One hour. Since the service token is refreshed every hour, this is the longest cache we can have. - 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, - }), +const getAllCachedApiRewards = unstable_cache( + async function (token) { + const apiResponse = await api.get(api.endpoints.v1.tierRewards, { + headers: { + Authorization: `Bearer ${token}`, + }, }) - console.error( - "api.rewards.tierRewards error ", - JSON.stringify({ - error: { + + 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, + }, + }) + ) + } - const data = await apiResponse.json() - const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data) + 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, + if (!validatedApiTierRewards.success) { + getAllRewardFailCounter.add(1, { + error_type: "validation_error", + error: JSON.stringify(validatedApiTierRewards.error), }) - ) - return null - } + console.error(validatedApiTierRewards.error) + console.error( + "api.rewards validation error", + JSON.stringify({ + error: validatedApiTierRewards.error, + }) + ) + return null + } - return validatedApiTierRewards.data -} + 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, { @@ -266,7 +268,9 @@ export const rewardQueryRouter = router({ getByLevelRewardCounter.add(1) const { level_id } = input - const allUpcomingApiRewards = await getAllApiRewards(ctx) + const allUpcomingApiRewards = await getAllCachedApiRewards( + ctx.serviceToken + ) if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) { getByLevelRewardFailCounter.add(1) @@ -314,7 +318,7 @@ export const rewardQueryRouter = router({ .input(rewardsAllInput) .query(async function ({ input, ctx }) { getAllRewardCounter.add(1) - const allApiRewards = await getAllApiRewards(ctx) + const allApiRewards = await getAllCachedApiRewards(ctx.serviceToken) if (!allApiRewards) { return []