Merged in feat/LOY-400-create-spend-points-modal (pull request #3131)

Feat/LOY-400 create spend points modal

* feat(LOY-400): Added custom button to my pages overview and skeleton file to custom modal for my points.

* feat(LOY-400): Added custom button to my pages overview and components for custom modal for my points.

* feat(LOY-400): Changed some style and infogridcardover

* feat(LOY-400):Removed custom card components and changed in infoCard: Added imagePosition top, added optional height prop. In Card: Changed Text-wrap styling, added min-width styling to buttons, added optional Icon prop, added optional height prop

* feat(LOY-400):Added linkList, LinkListItem component and messageBanner component. Added granola illustration.

* feat(LOY-400): Removed background in several illustrations. Added component for illustration. Fixed LinkedList and styling for UsePointsButton.

* feat(LOY-400): Added modal to PointsToSpendCard and fixed UsePointsButton.

* fix(LOY-400):added some styling

* feat(LOY-400): Linked Modal to contentstack and fetch the data in cards with UsePointsModal for now

* feat(LOY-400): changed link to aria-component, cleaned up a bit

* feat(LOY-400): Changed height for larger modals in mobile, fixed zod schema for no illustration input, cleaned up

* fix(LOY-400): fixed graphql after rebase

* fix(LOY-400): mini fix

* fix(LOY-400): fixed pr-comments

* fix(LOY-400): fixed some PR-comments

* fix(LOY-400): fixed a PR-comment

* feat(LOY-400): added size prop to ilustration in LinkListItem to be able to use illustrations in IllustrationByIconName

* fix(LOY-400): fixed pr-comments

* Merged in feat/LOY-402-pre-ticked-book-reward-night-in-booking-flow (pull request #3210)

Feat/LOY-402 pre ticked book reward night in booking flow

* feat(LOY-402): Changed UsePointsModal structure to handle button actions in card.

* feat(LOY-402): added functionality for book now button

* feat(LOY-400): pr comment fix

* feat(LOY-402): transformed the contentstack data

* fix(LOY-402): fixed pr comments

Approved-by: Chuma Mcphoy (We Ahead)
Approved-by: Anton Gunnarsson
Approved-by: Matilda Landström

* Merged in feat/LOY-404-add-tracking-for-spend-points-modal (pull request #3229)

Feat/LOY-404 add tracking for spend points modal

* feat(LOY-402): Changed UsePointsModal structure to handle button actions in card.

* feat(LOY-402): added functionality for book now button

* feat(LOY-400): pr comment fix

* feat(LOY-402): transformed the contentstack data

* feat(LOY-404): added tracking

* fix(LOY-404): fix for session storage removal of bookNowFromPointsModal

* feat(LOY-404): added consts

* fix(LOY-404): moved foxusWidget const

* fix(LOY-404): moved BOOKING_WIDGET_STATE const

* fix(LOY-404):fix


Approved-by: Matilda Landström

* fix(LOY-400): some fixes

* feat(LOY-400): created linkList storybook


Approved-by: Chuma Mcphoy (We Ahead)
Approved-by: Matilda Landström
This commit is contained in:
Emma Zettervall
2025-11-28 15:08:06 +00:00
parent 69f194f7bf
commit f443bae46e
54 changed files with 3631 additions and 70 deletions

View File

@@ -0,0 +1,117 @@
import { gql } from "graphql-tag"
import { AccountPageRef } from "../Fragments/AccountPage/Ref.graphql"
import { CampaignOverviewPageRef } from "../Fragments/CampaignOverviewPage/Ref.graphql"
import { CampaignPageRef } from "../Fragments/CampaignPage/Ref.graphql"
import { CollectionPageRef } from "../Fragments/CollectionPage/Ref.graphql"
import { ContentPageRef } from "../Fragments/ContentPage/Ref.graphql"
import { DestinationCityPageRef } from "../Fragments/DestinationCityPage/Ref.graphql"
import { DestinationCountryPageRef } from "../Fragments/DestinationCountryPage/Ref.graphql"
import { DestinationOverviewPageRef } from "../Fragments/DestinationOverviewPage/Ref.graphql"
import { HotelPageRef } from "../Fragments/HotelPage/Ref.graphql"
import { LoyaltyPageRef } from "../Fragments/LoyaltyPage/Ref.graphql"
import { AccountPageLink } from "../Fragments/PageLink/AccountPageLink.graphql"
import { CampaignOverviewPageLink } from "../Fragments/PageLink/CampaignOverviewPageLink.graphql"
import { CampaignPageLink } from "../Fragments/PageLink/CampaignPageLink.graphql"
import { CollectionPageLink } from "../Fragments/PageLink/CollectionPageLink.graphql"
import { ContentPageLink } from "../Fragments/PageLink/ContentPageLink.graphql"
import { DestinationCityPageLink } from "../Fragments/PageLink/DestinationCityPageLink.graphql"
import { DestinationCountryPageLink } from "../Fragments/PageLink/DestinationCountryPageLink.graphql"
import { DestinationOverviewPageLink } from "../Fragments/PageLink/DestinationOverviewPageLink.graphql"
import { HotelPageLink } from "../Fragments/PageLink/HotelPageLink.graphql"
import { LoyaltyPageLink } from "../Fragments/PageLink/LoyaltyPageLink.graphql"
import { PromoCampaignPageLink } from "../Fragments/PageLink/PromoCampaignPageLink.graphql"
import { StartPageLink } from "../Fragments/PageLink/StartPageLink.graphql"
import { PromoCampaignPageRef } from "../Fragments/PromoCampaignPage/Ref.graphql"
import { StartPageRef } from "../Fragments/StartPage/Ref.graphql"
export const GetUsePointsModal = gql`
query GetUsePointsModal($locale: String!) {
all_usepointsmodal(locale: $locale) {
items {
image
link_group {
link_text
illustration
illustration_size
is_contentstack_link
external_link {
href
}
linkConnection {
edges {
node {
__typename
...AccountPageLink
...CampaignOverviewPageLink
...CampaignPageLink
...CollectionPageLink
...ContentPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
...HotelPageLink
...LoyaltyPageLink
...StartPageLink
...PromoCampaignPageLink
}
}
}
}
}
}
}
${AccountPageLink}
${CampaignOverviewPageLink}
${CampaignPageLink}
${CollectionPageLink}
${ContentPageLink}
${DestinationCityPageLink}
${DestinationCountryPageLink}
${DestinationOverviewPageLink}
${HotelPageLink}
${LoyaltyPageLink}
${StartPageLink}
${PromoCampaignPageLink}
`
export const GetUsePointsModalRefs = gql`
query GetUsePointsModalRefs($locale: String!) {
all_usepointsmodal(locale: $locale) {
items {
link_group {
linkConnection {
edges {
node {
__typename
...AccountPageRef
...CampaignOverviewPageRef
...CampaignPageRef
...CollectionPageRef
...ContentPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
...HotelPageRef
...LoyaltyPageRef
...StartPageRef
...PromoCampaignPageRef
}
}
}
}
}
}
}
${AccountPageRef}
${CampaignOverviewPageRef}
${CampaignPageRef}
${CollectionPageRef}
${ContentPageRef}
${DestinationCityPageRef}
${DestinationCountryPageRef}
${DestinationOverviewPageRef}
${HotelPageRef}
${LoyaltyPageRef}
${StartPageRef}
${PromoCampaignPageRef}
`

View File

@@ -0,0 +1,4 @@
import { mergeRouters } from "../../.."
import { usePointsModalQueryRouter } from "./query"
export const usePointsModalRouter = mergeRouters(usePointsModalQueryRouter)

View File

@@ -0,0 +1,84 @@
import { z } from "zod"
import { transformedImageVaultAssetSchema } from "@scandic-hotels/common/utils/imageVault"
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
import { linkConnectionRefs } from "../schemas/linkConnection"
import { linkUnionSchema, transformPageLink } from "../schemas/pageLinks"
export type UsePointsModalData = z.output<typeof usePointsModalSchema>
export type UsePointsModalRefsData = z.output<typeof usePointsModalRefsSchema>
export const linkConnectionSchema = z
.object({
edges: z.array(
z.object({
node: linkUnionSchema,
})
),
})
.transform((data) => {
if (data.edges.length) {
const linkNode = data.edges[0].node
if (linkNode) {
const link = transformPageLink(linkNode)
if (link && link.url) {
return link.url
}
}
}
return null
})
const usePointsModalItemsSchema = z.object({
image: transformedImageVaultAssetSchema,
link_group: z
.array(
z.object({
link_text: z.string().default(""),
is_contentstack_link: z.boolean(),
illustration: z.nativeEnum(IconName).nullish(),
illustration_size: z
.enum(["small", "medium", "large"])
.nullish()
.default("large"),
external_link: z
.object({
href: z.string().default(""),
})
.optional(),
linkConnection: linkConnectionSchema,
})
)
.transform((links) =>
links.map((link) => ({
text: link.link_text || "",
isExternal: !link.is_contentstack_link,
href: link.is_contentstack_link
? link.linkConnection || ""
: link.external_link?.href || "",
illustration: link.illustration
? {
illustration: link.illustration,
size: link.illustration_size || "large",
}
: undefined,
}))
),
})
export const usePointsModalSchema = z.object({
all_usepointsmodal: z.object({
items: z.array(usePointsModalItemsSchema),
}),
})
export const usePointsModalRefsSchema = z.object({
all_usepointsmodal: z.object({
items: z.array(
z.object({
link_group: z.array(linkConnectionRefs),
})
),
}),
})

View File

@@ -0,0 +1,90 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { router } from "../../.."
import { notFound } from "../../../errors"
import {
GetUsePointsModal,
GetUsePointsModalRefs,
} from "../../../graphql/Query/UsePointsModal.graphql"
import { request } from "../../../graphql/request"
import { contentstackBaseProcedure } from "../../../procedures"
import {
type UsePointsModalData,
type UsePointsModalRefsData,
usePointsModalRefsSchema,
usePointsModalSchema,
} from "./output"
export const usePointsModalQueryRouter = router({
get: contentstackBaseProcedure.query(async ({ ctx }) => {
const { lang, uid } = ctx
const getRefsCounter = createCounter(
"trpc.contentstack",
"usePointsModal.get.refs"
)
const metricsRefs = getRefsCounter.init({
lang,
uid,
})
metricsRefs.start()
const refsResponse = await request<UsePointsModalRefsData>(
GetUsePointsModalRefs,
{ locale: lang },
{
key: `contentstack:usePointsModal:${lang}:refs`,
ttl: "max",
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
metricsRefs.noDataError()
throw notFoundError
}
const validatedRefsData = usePointsModalRefsSchema.safeParse(
refsResponse.data
)
if (!validatedRefsData.success) {
metricsRefs.validationError(validatedRefsData.error)
return null
}
metricsRefs.success()
const getCounter = createCounter("trpc.contentstack", "usePointsModal.get")
const metrics = getCounter.init({
lang,
uid,
})
metrics.start()
const response = await request<UsePointsModalData>(
GetUsePointsModal,
{ locale: lang },
{
key: `contentstack:usePointsModal:${lang}`,
ttl: "max",
}
)
if (!response.data) {
const notFoundError = notFound(response)
metrics.noDataError()
throw notFoundError
}
const validatedResponse = usePointsModalSchema.safeParse(response.data)
if (!validatedResponse.success) {
metrics.validationError(validatedResponse.error)
return null
}
metrics.success()
return validatedResponse.data
}),
})

View File

@@ -20,6 +20,7 @@ import { profilingConsentRouter } from "./profilingConsent"
import { promoCampaignPageRouter } from "./promoCampaignPage"
import { rewardRouter } from "./reward"
import { startPageRouter } from "./startPage"
import { usePointsModalRouter } from "./UsePointsModal"
export const contentstackRouter = router({
accountPage: accountPageRouter,
@@ -43,4 +44,5 @@ export const contentstackRouter = router({
partner: partnerRouter,
promoCampaignPage: promoCampaignPageRouter,
profilingConsent: profilingConsentRouter,
usePointsModal: usePointsModalRouter,
})