diff --git a/app/globals.css b/app/globals.css index 07a78808b..5fff0324b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -115,6 +115,7 @@ --header-z-index: 10; --menu-overlay-z-index: 10; --dialog-z-index: 9; + --sidepeek-z-index: 11; } * { diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index 134eeaa41..45a500d20 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -6,12 +6,15 @@ import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard" import TeaserCard from "@/components/TempDesignSystem/TeaserCard" import type { CardsGridProps } from "@/types/components/blocks/cardsGrid" -import { CardsGridEnum } from "@/types/enums/cardsGrid" +import { CardsGridEnum, CardsGridLayoutEnum } from "@/types/enums/cardsGrid" export default function CardsGrid({ cards_grid, firstItem = false, }: CardsGridProps) { + const columns = + cards_grid.layout === CardsGridLayoutEnum.THREE_COLUMNS ? 3 : 2 + return ( - + {cards_grid.cards.map((card) => { switch (card.__typename) { case CardsGridEnum.cards.Card: @@ -43,6 +46,7 @@ export default function CardsGrid({ primaryButton={card.primaryButton} secondaryButton={card.secondaryButton} sidePeekButton={card.sidePeekButton} + sidePeekContent={card.sidePeekContent} image={card.image} /> ) diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 48f08f323..eded233f0 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -7,7 +7,7 @@ import { dt } from "@/lib/dt" import Form from "@/components/Forms/BookingWidget" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" -import { CloseLarge } from "@/components/Icons" +import { CloseLargeIcon } from "@/components/Icons" import { debounce } from "@/utils/debounce" import MobileToggleButton from "./MobileToggleButton" @@ -98,7 +98,7 @@ export default function BookingWidgetClient({ onClick={closeMobileSearch} type="button" > - +
diff --git a/components/DatePicker/Screen/Mobile.tsx b/components/DatePicker/Screen/Mobile.tsx index c56a79cd6..3d367d995 100644 --- a/components/DatePicker/Screen/Mobile.tsx +++ b/components/DatePicker/Screen/Mobile.tsx @@ -6,7 +6,7 @@ import { useIntl } from "react-intl" import { Lang } from "@/constants/languages" import { dt } from "@/lib/dt" -import { CloseLarge } from "@/components/Icons" +import { CloseLargeIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -127,7 +127,7 @@ export default function DatePickerMobile({ ))} {children} diff --git a/components/Forms/Register/index.tsx b/components/Forms/Register/index.tsx index e154817ff..f8b79f61e 100644 --- a/components/Forms/Register/index.tsx +++ b/components/Forms/Register/index.tsx @@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import { signupTerms } from "@/constants/routes/signup" +import { privacyPolicy } from "@/constants/currentWebHrefs" import { registerUser } from "@/actions/registerUser" import Button from "@/components/TempDesignSystem/Button" @@ -163,7 +163,7 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) { variant="underscored" color="peach80" target="_blank" - href={signupTerms[lang]} + href={privacyPolicy[lang]} > {intl.formatMessage({ id: "Scandic's Privacy Policy." })} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 9bfc4d9c7..2f89aa6db 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -21,7 +21,7 @@ import { ChevronRightIcon, ChevronRightSmallIcon, CloseIcon, - CloseLarge, + CloseLargeIcon, CoffeeIcon, ConciergeIcon, CrossCircle, @@ -103,7 +103,7 @@ export function getIconByIconName(icon?: IconName): FC | null { case IconName.Close: return CloseIcon case IconName.CloseLarge: - return CloseLarge + return CloseLargeIcon case IconName.Coffee: return CoffeeIcon case IconName.Concierge: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 31b77b1e5..ba86f0784 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -16,7 +16,7 @@ export { default as ChevronLeftIcon } from "./ChevronLeft" export { default as ChevronRightIcon } from "./ChevronRight" export { default as ChevronRightSmallIcon } from "./ChevronRightSmall" export { default as CloseIcon } from "./Close" -export { default as CloseLarge } from "./CloseLarge" +export { default as CloseLargeIcon } from "./CloseLarge" export { default as CoffeeIcon } from "./Coffee" export { default as ConciergeIcon } from "./Concierge" export { default as CreditCard } from "./CreditCard" diff --git a/components/TempDesignSystem/SidePeek/index.tsx b/components/TempDesignSystem/SidePeek/index.tsx index 6df535b54..ee59c0e28 100644 --- a/components/TempDesignSystem/SidePeek/index.tsx +++ b/components/TempDesignSystem/SidePeek/index.tsx @@ -10,7 +10,7 @@ import { } from "react-aria-components" import { useIntl } from "react-intl" -import { CloseIcon } from "@/components/Icons" +import { CloseLargeIcon } from "@/components/Icons" import { SidePeekContext } from "@/components/SidePeekProvider" import Button from "../Button" @@ -52,13 +52,16 @@ function SidePeek({ ) } + return (
@@ -82,7 +85,7 @@ function SidePeek({ intent="text" onPress={onClose} > - +
{children}
diff --git a/components/TempDesignSystem/SidePeek/sidePeek.module.css b/components/TempDesignSystem/SidePeek/sidePeek.module.css index 65ad886b1..8282166c1 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.module.css +++ b/components/TempDesignSystem/SidePeek/sidePeek.module.css @@ -22,12 +22,13 @@ } .overlay { - position: absolute; + position: fixed; top: 0; bottom: 0; left: 0; right: 0; - z-index: 99; + z-index: var(--sidepeek-z-index); + background-color: var(--UI-Opacity-Almost-Black-30); } .modal { diff --git a/components/TempDesignSystem/SidePeek/sidePeek.ts b/components/TempDesignSystem/SidePeek/sidePeek.ts index e1781f137..6caa9f0f4 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.ts +++ b/components/TempDesignSystem/SidePeek/sidePeek.ts @@ -1,5 +1,5 @@ export interface SidePeekProps { - contentKey: string + contentKey?: string title: string isOpen?: boolean handleClose?: (isOpen: boolean) => void diff --git a/components/TempDesignSystem/TeaserCard/Sidepeek/index.tsx b/components/TempDesignSystem/TeaserCard/Sidepeek/index.tsx new file mode 100644 index 000000000..ee3c6be79 --- /dev/null +++ b/components/TempDesignSystem/TeaserCard/Sidepeek/index.tsx @@ -0,0 +1,82 @@ +"use client" + +import { useState } from "react" + +import { ChevronRightIcon } from "@/components/Icons" +import JsonToHtml from "@/components/JsonToHtml" +import Button from "@/components/TempDesignSystem/Button" + +import Link from "../../Link" +import SidePeek from "../../SidePeek" + +import styles from "./sidepeek.module.css" + +import type { TeaserCardSidepeekProps } from "@/types/components/teaserCard" + +export default function TeaserCardSidepeek({ + button, + sidePeekContent, +}: TeaserCardSidepeekProps) { + const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false) + const { heading, content, primary_button, secondary_button } = sidePeekContent + + return ( + <> + + setSidePeekIsOpen(false)} + > + +
+ {primary_button && ( + + )} + {secondary_button && ( + + )} +
+
+ + ) +} diff --git a/components/TempDesignSystem/TeaserCard/Sidepeek/sidepeek.module.css b/components/TempDesignSystem/TeaserCard/Sidepeek/sidepeek.module.css new file mode 100644 index 000000000..e40062540 --- /dev/null +++ b/components/TempDesignSystem/TeaserCard/Sidepeek/sidepeek.module.css @@ -0,0 +1,4 @@ +.ctaContainer { + display: grid; + gap: var(--Spacing-x2); +} diff --git a/components/TempDesignSystem/TeaserCard/index.tsx b/components/TempDesignSystem/TeaserCard/index.tsx index a441a3322..a5a578df1 100644 --- a/components/TempDesignSystem/TeaserCard/index.tsx +++ b/components/TempDesignSystem/TeaserCard/index.tsx @@ -1,10 +1,10 @@ -import { ChevronRightIcon } from "@/components/Icons" import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "../Text/Subtitle" +import TeaserCardSidepeek from "./Sidepeek" import { teaserCardVariants } from "./variants" import styles from "./teaserCard.module.css" @@ -17,6 +17,7 @@ export default function TeaserCard({ primaryButton, secondaryButton, sidePeekButton, + sidePeekContent, image, style = "default", alwaysStack = false, @@ -41,21 +42,14 @@ export default function TeaserCard({ {title} - {description} - {!!sidePeekButton ? ( - + + {description} + + {sidePeekButton && sidePeekContent ? ( + ) : (
{primaryButton && ( diff --git a/components/TempDesignSystem/TeaserCard/teaserCard.module.css b/components/TempDesignSystem/TeaserCard/teaserCard.module.css index 9a90d97c7..e16d06c22 100644 --- a/components/TempDesignSystem/TeaserCard/teaserCard.module.css +++ b/components/TempDesignSystem/TeaserCard/teaserCard.module.css @@ -54,6 +54,17 @@ width: 100%; } +.body { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + line-clamp: 3; + overflow: hidden; + text-overflow: ellipsis; + /* line-height variables are in %, so using the value in rem instead */ + max-height: calc(3 * 1.5rem); +} + @media (min-width: 1367px) { .card:not(.alwaysStack) .ctaContainer { grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); @@ -63,9 +74,3 @@ grid-template-columns: 1fr; } } - -.sidePeekCTA { - /* TODO: Create ticket to remove padding on "link" buttons, - align w. design on this. */ - padding: 0 !important; -} diff --git a/components/TempDesignSystem/Toasts/index.tsx b/components/TempDesignSystem/Toasts/index.tsx index 486bc4f46..df08c6803 100644 --- a/components/TempDesignSystem/Toasts/index.tsx +++ b/components/TempDesignSystem/Toasts/index.tsx @@ -2,7 +2,7 @@ import { ExternalToast, toast as sonnerToast, Toaster } from "sonner" import { CheckCircleIcon, - CloseLarge, + CloseLargeIcon, CrossCircle, InfoCircleIcon, WarningTriangle, @@ -42,7 +42,7 @@ export function Toast({ message, onClose, variant }: ToastsProps) {
{message}
) diff --git a/constants/currentWebHrefs.ts b/constants/currentWebHrefs.ts index d0ebfe954..7438ee13a 100644 --- a/constants/currentWebHrefs.ts +++ b/constants/currentWebHrefs.ts @@ -1,17 +1,28 @@ -export const bookingTermsAndConditions = { - da: "https://www.scandichotels.dk/kundeservice/priser-og-bookingvilkar/vilkar-og-betingelser3", - de: "https://www.scandichotels.de/kundenbetreuung/preise-und-richtlinien/reservierungsbedingungen", - en: "https://www.scandichotels.com/customer-service/rates-and-policies/terms-conditions", - fi: "https://www.scandichotels.fi/asiakaspalvelu/hinnat-ja-varausehdot/varausehdot", - no: "https://www.scandichotels.no/kundeservice/priser-og-bestillingsvilkar/reservasjonsbetingelser", - sv: "https://www.scandichotels.se/kundservice/priser-och-bokningsregler/bokningsregler", +import { LangRoute } from "@/types/routes" + +const baseUrl: LangRoute = { + da: "https://www.scandichotels.dk", + de: "https://www.scandichotels.de", + en: "https://www.scandichotels.com", + fi: "https://www.scandichotels.fi", + no: "https://www.scandichotels.no", + sv: "https://www.scandichotels.se", } -export const privacyPolicy = { - da: "https://www.scandichotels.dk/kundeservice/priser-og-bookingvilkar/persondatapolitik", - de: "https://www.scandichotels.de/kundenbetreuung/preise-und-richtlinien/reservierungsbedingungen", - en: "https://www.scandichotels.com/customer-service/rates-and-policies/privacy-policy", - fi: "https://www.scandichotels.fi/asiakaspalvelu/hinnat-ja-varausehdot/tietosuojaseloste", - no: "https://www.scandichotels.no/kundeservice/priser-og-bestillingsvilkar/personvernpolicy", - sv: "https://www.scandichotels.se/kundservice/priser-och-bokningsregler/integritetspolicy", +export const bookingTermsAndConditions: LangRoute = { + da: `${baseUrl.da}/kundeservice/priser-og-bookingvilkar/vilkar-og-betingelser3`, + de: `${baseUrl.de}/kundenbetreuung/preise-und-richtlinien/reservierungsbedingungen`, + en: `${baseUrl.en}/customer-service/rates-and-policies/terms-conditions`, + fi: `${baseUrl.fi}/asiakaspalvelu/hinnat-ja-varausehdot/varausehdot`, + no: `${baseUrl.no}/kundeservice/priser-og-bestillingsvilkar/reservasjonsbetingelser`, + sv: `${baseUrl.sv}/kundservice/priser-och-bokningsregler/bokningsregler`, +} + +export const privacyPolicy: LangRoute = { + da: `${baseUrl.da}/kundeservice/priser-og-bookingvilkar/persondatapolitik`, + de: `${baseUrl.de}/kundenbetreuung/preise-und-richtlinien/datenschutzrichtlinie`, + en: `${baseUrl.en}/customer-service/rates-and-policies/privacy-policy`, + fi: `${baseUrl.fi}/asiakaspalvelu/hinnat-ja-varausehdot/tietosuojaseloste`, + no: `${baseUrl.no}/kundeservice/priser-og-bestillingsvilkar/personvernpolicy`, + sv: `${baseUrl.sv}/kundservice/priser-och-bokningsregler/integritetspolicy`, } diff --git a/constants/routes/signup.ts b/constants/routes/signup.ts index 16d4f171d..82a39ee41 100644 --- a/constants/routes/signup.ts +++ b/constants/routes/signup.ts @@ -17,17 +17,3 @@ export const signupVerify: LangRoute = { da: `${signup.da}/bekraeft`, de: `${signup.de}/verifizieren`, } - -/* No work has been scoped out to implement terms of privacy pages for new web yet, - * which is why these currently link to routes in current web. - * TODO: Align w. stakeholders, make tickets (and reference them here) to update these - * links when the time comes. - */ -export const signupTerms: LangRoute = { - en: "https://www.scandichotels.com/customer-service/rates-and-policies/privacy-policy", - sv: "https://www.scandichotels.se/kundservice/priser-och-bokningsregler/integritetspolicy", - no: "https://www.scandichotels.no/kundeservice/priser-og-bestillingsvilkar/personvernpolicy", - fi: "https://www.scandichotels.fi/asiakaspalvelu/hinnat-ja-varausehdot/tietosuojaseloste", - da: "https://www.scandichotels.dk/kundeservice/priser-og-bookingvilkar/persondatapolitik", - de: "https://www.scandichotels.de/kundenbetreuung/preise-und-richtlinien/datenschutzrichtlinie", -} diff --git a/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql b/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql index c922f8adc..cba0b5231 100644 --- a/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql +++ b/lib/graphql/Fragments/Blocks/Refs/TeaserCard.graphql @@ -30,6 +30,45 @@ fragment TeaserCardBlockRef on TeaserCard { } } } + sidepeek_content { + content { + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + ...HotelPageRef + } + } + } + } + primary_button { + linkConnection { + edges { + node { + __typename + ...ContentPageRef + ...LoyaltyPageRef + ...HotelPageRef + } + } + } + } + secondary_button { + linkConnection { + edges { + node { + __typename + ...ContentPageRef + ...LoyaltyPageRef + ...HotelPageRef + } + } + } + } + } system { ...System } diff --git a/lib/graphql/Fragments/Blocks/TeaserCard.graphql b/lib/graphql/Fragments/Blocks/TeaserCard.graphql index 5b333797c..07a16b366 100644 --- a/lib/graphql/Fragments/Blocks/TeaserCard.graphql +++ b/lib/graphql/Fragments/Blocks/TeaserCard.graphql @@ -28,6 +28,7 @@ fragment TeaserCardBlock on TeaserCard { ...LoyaltyPageLink ...ContentPageLink ...AccountPageLink + ...HotelPageLink } } } @@ -55,6 +56,63 @@ fragment TeaserCardBlock on TeaserCard { sidepeek_button { call_to_action_text } + sidepeek_content { + heading + content { + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageLink + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + json + } + has_primary_button + primary_button { + is_contentstack_link + cta_text + open_in_new_tab + external_link { + title + href + } + linkConnection { + edges { + node { + __typename + ...LoyaltyPageLink + ...ContentPageLink + ...HotelPageLink + } + } + } + } + has_secondary_button + secondary_button { + is_contentstack_link + cta_text + open_in_new_tab + external_link { + title + href + } + linkConnection { + edges { + node { + __typename + ...LoyaltyPageLink + ...ContentPageLink + ...HotelPageLink + } + } + } + } + } system { ...System } diff --git a/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index 2bed1d02d..686ed9329 100644 --- a/lib/graphql/Query/ContentPage/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage/ContentPage.graphql @@ -23,16 +23,6 @@ query GetContentPage($locale: String!, $uid: String!) { preamble ...NavigationLinks } - blocks { - __typename - ...CardsGrid_ContentPage - ...Content_ContentPage - ...DynamicContent_ContentPage - ...Shortcuts_ContentPage - ...Table_ContentPage - ...TextCols_ContentPage - ...UspGrid_ContentPage - } sidebar { __typename ...ContentSidebar_ContentPage @@ -47,6 +37,21 @@ query GetContentPage($locale: String!, $uid: String!) { } } +query GetContentPageBlocks($locale: String!, $uid: String!) { + content_page(uid: $uid, locale: $locale) { + blocks { + __typename + ...CardsGrid_ContentPage + ...Content_ContentPage + ...DynamicContent_ContentPage + ...Shortcuts_ContentPage + ...Table_ContentPage + ...TextCols_ContentPage + ...UspGrid_ContentPage + } + } +} + query GetContentPageRefs($locale: String!, $uid: String!) { content_page(locale: $locale, uid: $uid) { header { diff --git a/lib/trpc/server.ts b/lib/trpc/server.ts index 383da4966..46c0f8840 100644 --- a/lib/trpc/server.ts +++ b/lib/trpc/server.ts @@ -50,8 +50,6 @@ export function serverClient() { redirect(redirectUrl) } } - - throw internalServerError() }, }) } diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index 79ef83a4e..1e6536ae2 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -1,5 +1,8 @@ import { Lang } from "@/constants/languages" -import { GetContentPage } from "@/lib/graphql/Query/ContentPage/ContentPage.graphql" +import { + GetContentPage, + GetContentPageBlocks, +} from "@/lib/graphql/Query/ContentPage/ContentPage.graphql" import { request } from "@/lib/graphql/request" import { contentstackExtendedProcedureUID, router } from "@/server/trpc" @@ -40,18 +43,38 @@ export const contentPageQueryRouter = router({ }) ) - const response = await request( - GetContentPage, - { locale: lang, uid }, - { - cache: "force-cache", - next: { - tags, - }, - } - ) + const [mainResponse, blocksResponse] = await Promise.all([ + request( + GetContentPage, + { locale: lang, uid }, + { + cache: "force-cache", + next: { + tags, + }, + } + ), + request( + GetContentPageBlocks, + { locale: lang, uid }, + { + cache: "force-cache", + next: { + tags, + }, + } + ), + ]) - const contentPage = contentPageSchema.safeParse(response.data) + const responseData = { + ...mainResponse.data, + content_page: { + ...mainResponse.data.content_page, + blocks: blocksResponse.data.content_page.blocks, + }, + } + + const contentPage = contentPageSchema.safeParse(responseData) if (!contentPage.success) { console.error( diff --git a/server/routers/contentstack/schemas/blocks/cardsGrid.ts b/server/routers/contentstack/schemas/blocks/cardsGrid.ts index 5f1b9af5c..a9de83854 100644 --- a/server/routers/contentstack/schemas/blocks/cardsGrid.ts +++ b/server/routers/contentstack/schemas/blocks/cardsGrid.ts @@ -1,12 +1,16 @@ import { z } from "zod" +import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" + import { tempImageVaultAssetSchema } from "../imageVault" import { systemSchema } from "../system" import { buttonSchema } from "./utils/buttonLinkSchema" import { linkConnectionRefsSchema } from "./utils/linkConnection" +import { imageSchema } from "./image" +import { imageContainerSchema } from "./imageContainer" import { BlocksEnums } from "@/types/enums/blocks" -import { CardsGridEnum } from "@/types/enums/cardsGrid" +import { CardsGridEnum, CardsGridLayoutEnum } from "@/types/enums/cardsGrid" export const cardBlockSchema = z.object({ __typename: z.literal(CardsGridEnum.cards.Card), @@ -49,9 +53,43 @@ export const teaserCardBlockSchema = z.object({ has_primary_button: z.boolean().default(false), has_secondary_button: z.boolean().default(false), has_sidepeek_button: z.boolean().default(false), - side_peek_button: z + sidepeek_button: z .object({ - title: z.string().optional().default(""), + call_to_action_text: z.string().optional().default(""), + }) + .optional(), + sidepeek_content: z + .object({ + heading: z.string(), + content: z.object({ + json: z.any(), + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z + .discriminatedUnion("__typename", [ + imageContainerSchema, + imageSchema, + pageLinks.accountPageSchema, + pageLinks.contentPageSchema, + pageLinks.hotelPageSchema, + pageLinks.loyaltyPageSchema, + ]) + .transform((data) => { + const link = pageLinks.transform(data) + if (link) { + return link + } + return data + }), + }) + ), + }), + }), + has_primary_button: z.boolean().default(false), + primary_button: buttonSchema, + has_secondary_button: z.boolean().default(false), + secondary_button: buttonSchema, }) .optional(), system: systemSchema, @@ -68,8 +106,9 @@ export function transformTeaserCardBlock( secondaryButton: card.has_secondary_button ? card.secondary_button : undefined, - sidePeekButton: card.has_sidepeek_button - ? card.side_peek_button + sidePeekButton: card.has_sidepeek_button ? card.sidepeek_button : undefined, + sidePeekContent: card.has_sidepeek_button + ? card.sidepeek_content : undefined, image: card.image, system: card.system, @@ -105,7 +144,7 @@ export const cardsGridSchema = z.object({ }) ), }), - layout: z.enum(["twoColumnGrid", "threeColumnGrid", "twoPlusOne"]), + layout: z.nativeEnum(CardsGridLayoutEnum), preamble: z.string().optional().default(""), theme: z.enum(["one", "two", "three"]).nullable(), title: z.string().optional().default(""), diff --git a/server/routers/contentstack/schemas/blocks/utils/buttonLinkSchema.ts b/server/routers/contentstack/schemas/blocks/utils/buttonLinkSchema.ts index 9735c276f..62baefdab 100644 --- a/server/routers/contentstack/schemas/blocks/utils/buttonLinkSchema.ts +++ b/server/routers/contentstack/schemas/blocks/utils/buttonLinkSchema.ts @@ -20,6 +20,7 @@ export const buttonSchema = z pageLinks.accountPageSchema, pageLinks.contentPageSchema, pageLinks.loyaltyPageSchema, + pageLinks.hotelPageSchema, ]) .transform((data) => { const link = pageLinks.transform(data) diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 2e6d82b04..cdd6078dc 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -21,6 +21,7 @@ const fetchServiceTokenFailCounter = meter.createCounter( async function fetchServiceToken(scopes: string[]) { fetchServiceTokenCounter.add(1) + const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, { method: "POST", headers: { @@ -36,52 +37,67 @@ async function fetchServiceToken(scopes: string[]) { }) if (!response.ok) { - fetchServiceTokenFailCounter.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 getServiceToken(): Promise { - try { - const scopes = ["profile", "hotel", "booking"] - const tag = generateServiceTokenTag(scopes) - const getCachedJwt = unstable_cache( - 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(scopes) - if (cachedJwt.expiresAt < Date.now()) { - console.log( - "trpc.context.serviceToken: Service token expired, revalidating tag" - ) - revalidateTag(tag) - - console.log( - "trpc.context.serviceToken: Fetching new temporary service token." - ) - fetchTempServiceTokenCounter.add(1) - const newToken = await fetchServiceToken(scopes) - return newToken + const text = await response.text() + const error = { + status: response.status, + statusText: response.statusText, + text, } - return cachedJwt.jwt - } catch (error) { - console.error("Error fetching service token:", error) - throw error + fetchServiceTokenFailCounter.add(1, { + error_type: "http_error", + error: JSON.stringify(error), + }) + + console.error( + "fetchServiceToken error", + JSON.stringify({ + query: { + grant_type: "client_credentials", + client_id: env.CURITY_CLIENT_ID_SERVICE, + scope: scopes.join(" "), + }, + error, + }) + ) + + throw new Error( + `[fetchServiceToken] Failed to obtain service token: ${JSON.stringify(error)}` + ) } + + return response.json() as Promise +} + +export async function getServiceToken() { + const scopes = ["profile", "hotel", "booking"] + const tag = generateServiceTokenTag(scopes) + const getCachedJwt = unstable_cache( + 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(scopes) + if (cachedJwt.expiresAt < Date.now()) { + console.log( + "trpc.context.serviceToken: Service token expired, revalidating tag" + ) + + revalidateTag(tag) + + console.log( + "trpc.context.serviceToken: Fetching new temporary service token." + ) + fetchTempServiceTokenCounter.add(1) + const newToken = await fetchServiceToken(scopes) + return newToken + } + + return cachedJwt.jwt } diff --git a/server/trpc.ts b/server/trpc.ts index 4eea99c53..0079a1ff6 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -124,7 +124,7 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) { export const serviceProcedure = t.procedure.use(async (opts) => { const { access_token } = await getServiceToken() if (!access_token) { - throw internalServerError(`Failed to obtain service token`) + throw internalServerError(`[serviceProcedure] No service token`) } return opts.next({ ctx: { @@ -144,7 +144,9 @@ export const serviceServerActionProcedure = serverActionProcedure.use( async (opts) => { const { access_token } = await getServiceToken() if (!access_token) { - throw internalServerError("Failed to obtain service token") + throw internalServerError( + "[serviceServerActionProcedure]: No service token" + ) } return opts.next({ ctx: { diff --git a/types/components/teaserCard.ts b/types/components/teaserCard.ts index 4ca7f56f1..5cb7dd01a 100644 --- a/types/components/teaserCard.ts +++ b/types/components/teaserCard.ts @@ -2,11 +2,12 @@ import { VariantProps } from "class-variance-authority" import { teaserCardVariants } from "@/components/TempDesignSystem/TeaserCard/variants" -import { ImageVaultAsset } from "@/types/components/imageVault" +import type { ImageVaultAsset } from "@/types/components/imageVault" import type { CardProps } from "@/components/TempDesignSystem/Card/card" +import type { TeaserCard } from "../trpc/routers/contentstack/blocks" interface SidePeekButton { - title: string + call_to_action_text: string } export interface TeaserCardProps @@ -16,6 +17,12 @@ export interface TeaserCardProps primaryButton?: CardProps["primaryButton"] secondaryButton?: CardProps["secondaryButton"] sidePeekButton?: SidePeekButton + sidePeekContent?: TeaserCard["sidepeek_content"] image?: ImageVaultAsset className?: string } + +export interface TeaserCardSidepeekProps { + button: SidePeekButton + sidePeekContent: NonNullable +} diff --git a/types/enums/cardsGrid.ts b/types/enums/cardsGrid.ts index 3bb96c684..9fa1d560a 100644 --- a/types/enums/cardsGrid.ts +++ b/types/enums/cardsGrid.ts @@ -5,3 +5,9 @@ export namespace CardsGridEnum { TeaserCard = "TeaserCard", } } + +export enum CardsGridLayoutEnum { + TWO_COLUMNS = "twoColumnGrid", + THREE_COLUMNS = "threeColumnGrid", + TWO_PLUS_ONE = "twoPlusOne", // Not sure if this is used? +} diff --git a/types/trpc/routers/contentstack/blocks.ts b/types/trpc/routers/contentstack/blocks.ts index 1c46dda93..149f9b35c 100644 --- a/types/trpc/routers/contentstack/blocks.ts +++ b/types/trpc/routers/contentstack/blocks.ts @@ -1,6 +1,9 @@ import { z } from "zod" -import { cardsGridSchema } from "@/server/routers/contentstack/schemas/blocks/cardsGrid" +import { + cardsGridSchema, + teaserCardBlockSchema, +} from "@/server/routers/contentstack/schemas/blocks/cardsGrid" import { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content" import { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent" import { shortcutsSchema } from "@/server/routers/contentstack/schemas/blocks/shortcuts" @@ -8,6 +11,7 @@ import { tableSchema } from "@/server/routers/contentstack/schemas/blocks/table" import { textColsSchema } from "@/server/routers/contentstack/schemas/blocks/textCols" import { uspGridSchema } from "@/server/routers/contentstack/schemas/blocks/uspGrid" +export interface TeaserCard extends z.output {} export interface CardsGrid extends z.output {} export interface Content extends z.output {} export interface DynamicContent extends z.output {}